入参太多导致的烦恼

代码的坏味道之一:过长参数列表

入参设计的几点建议:

  • 方法的参数列表应该尽量避免重复,减少调用者的使用难度。

何为重复?就是这个参数 A 可以通过另外一个参数 B 轻松获得,所以不应该辛苦调用者再去找出参数 A。这里可以去除参数 A,使获取参数 A 的「责任转移」到方法。

  • 并且参数列表越短就越容易理解。

为什么参数会有很多?过长参数列可能是将多个算法并到一个函数中时发生的。函数中的入参可以用来控制最终选用哪个算法去执行。

入参太多的缺点

  • 太长的参数列难以理解,增加理解成本
  • 太多参数会造成前后不一致、不易使用
  • 传参数数时容易传错(参数移位),增加犯错成本
  • 而且一旦需要更多数据,就不得不修改它。
  • 看起来不优雅(LOL)

比如下面是一个合成海报的方法:入参包含用户的头像、昵称、背景图片地址等等参数

public String createPoster(String nickname, 
                           String headImgUrl, 
                           String userId, 
                           Object headPosition, 
                           Object namePosition, 
                           String backGroundPicUrl);

提供几种优化方法:

  • 查询取代参数:如果可以向某个参数发起查询而获得另一个参数的值,则没有必要传入两个参数。
  • 保持对象完整:如果从现有的数据结构中抽出很多数据项,不如传入整个记录。
  • 引入参数对象::如果有几项参数总是同时出现,使用新的数据结构——引入参数对象。
  • 移除标记参数:如果某个参数被用作区分函数行为的标记,“标记参数”是这样的一种参数:调用者用它来指示被调函数应该执行哪一部分逻辑。在函数实现内部,如果参数值只是作为数据传给其他函数,这就不是标记参数;只有参数值影响了函数内部的控制流,这才是标记参数。
  • 函数组合成类:如果多个函数有同样的几个参数,引入一个类,将这些共同的参数变成这个类的字段。
  • 重载方法:只接受必须的参数,适用于遇到可选参数或者默认参数的场景。

查询取代参数

比如,通过一个用户唯一标识 userId 可以查询到昵称 nickname 以及头像地址 headImgUrl 的信息,就可以去掉这两个参数;那么这个昵称和头像参数其实是重复参数,可以像下面一样去掉:(这里暂时不考虑根据 userId 获取用户头像和昵称的困难程度,如果比较困难,就不应该去掉这两个参数)

public String createPoster(String userId, 
                           Object headPosition, 
                           Object namePosition, 
                           String backGroundPicUrl);

引入参数对象

使用场景:如果有几项参数总是同时出现,使用新的数据结构,参数的参数列表也能缩短,并且经过重构之后,所有使用该数据结构的函数都会通过同样的名字来访问其中的元素,从而提升代码的一致性。

将参数列表提取到 Bean 中管理。创建一个类,用于参数管理。参数类如下:

public class DataHolder {
    private String nickname;
    private String headImgUrl; 
    private String userId; 
    private Object headPosition; 
    private Object namePosition; 
    private String backGroundPicUrl;
    // 省略了构造器 getter and setter
}

使用的时候:

// 改造生成海报的方法
public String createPoster(DataHolder dataHolder);

String nickname = "turato";
String headImgUrl = "http://pic.baidu.com/pic.jpeg";
DataHolder dataHolder() = new DataHolder();
dataHolder.setNickname(nickname);
dataHolder.setHeadImgUrl(headImgUrl);
// 生成海报
createPoster(dataHolder)

但是,仔细想一下,这里是否有改进的空间呢?

当 DataHolder 被很多方法设为入参的时候,设置参数对象 dataHolder 中的值的时候,需要调用很多次 setXXX方法,这里就会产生大量的重复代码,不太优雅。(代码检查的时候,那些黄色的波浪线实在是不能视而不见呀)

进一步优化,使用构造者模式优化:兼顾构造器的一致性,线程安全和 Java Bean 的可读性。

@Data
public class DataHolder {
    private String nickname;
    private String headImgUrl;
    private String userId;
    private Object headPosition;
    private Object namePosition;
    private String backGroundPicUrl;

    public DataHolder() {}
    public DataHolder(Builder builder) {
        this.nickname = builder.nickname;
        this.headImgUrl = builder.headImgUrl;
        this.userId = builder.userId;
        this.headPosition = builder.headPosition;
        this.namePosition = builder.namePosition;
        this.backGroundPicUrl = builder.backGroundPicUrl;
    }

    /**
     * 通过这个静态内部类来构造 DataHolder 对象
     */
    public static class Builder{
        private String nickname;
        private String headImgUrl;
        private String userId;
        private Object headPosition;
        private Object namePosition;
        private String backGroundPicUrl;
        public Builder() {
        }
        // 通过方法来给DataHolder类的属性赋值,
        // 注意,方法要返回 Builder 本身,方便链式调用
        public Builder nickname(String nickname) {
            this.nickname = nickname;
            return this;
        }

        public Builder headImgUrl(String headImgUrl) {
            this.headImgUrl = headImgUrl;
            return this;
        }

        public Builder userId(String userId) {
            this.userId = userId;
            return this;
        }

        public Builder headPosition(Object headPosition) {
            this.headPosition = headPosition;
            return this;
        }

        public Builder namePosition(Object namePosition) {
            this.namePosition = namePosition;
            return this;
        }

        public Builder backGroundPicUrl(String backGroundPicUrl) {
            this.backGroundPicUrl = backGroundPicUrl;
            return this;
        }
        public DataHolder build(){
            return new DataHolder(this);
        }
    }
}

简单试试:

public class test {
	@Test
	public void test() {
        // gson 序列化
		Gson gson = new Gson();
		DataHolder dataHolder = new DataHolder.Builder().nickname("turato").userId("10086").build();
		System.out.println(gson.toJson(dataHolder));
        // {"nickname":"turato","userId":"10086"}
	}

}

移除标记参数

为何要移除标记参数?

  • 标记参数却隐藏了函数调用中存在的差异性,违反单一职责,增大了使用和理解成本

如何移除标记参数?可以针对每一种流程,建立一个明确的方法。

下面《重构》中的一个例子:计算物流时间,通过是否加急会影响发货时间。

  • 订单状态:
@AllArgsConstructor
public enum DeliveryStateEnum {
    // 已下单
    ORDERED(1),
    // 货物已经准备好
    PREPARED(2);
    @Getter
    Integer state;
}
  • 计算发货时间:
    public Date deliveryDate(OrderBO anOrder, Boolean isRush) {
        // 是否加急
        if (isRush) {
            // 天
            int deliveryTimeDay = 0;
            if (DeliveryStateEnum.ORDERED.getState().equals(anOrder.getDeliveryState())) {
                deliveryTimeDay = 1;
            } else if (DeliveryStateEnum.PREPARED.getState().equals(anOrder.getDeliveryState())) {
                deliveryTimeDay = 2;
            }
            Calendar calendar = new GregorianCalendar();
            calendar.setTime(anOrder.getPlacedOn());
            // 日期往后推N天
            calendar.add(Calendar.DATE, deliveryTimeDay + 1);
            return calendar.getTime();
        } else {
            // 天
            int deliveryTimeDay = 1;
            if (DeliveryStateEnum.ORDERED.getState().equals(anOrder.getDeliveryState())) {
                deliveryTimeDay = 2;
            } else if (DeliveryStateEnum.PREPARED.getState().equals(anOrder.getDeliveryState())) {
                deliveryTimeDay = 3;
            }
            Calendar calendar = new GregorianCalendar();
            calendar.setTime(anOrder.getPlacedOn());
            // 日期往后推N天
            calendar.add(Calendar.DATE, deliveryTimeDay + 2);
            return calendar.getTime();
        }
    }

实战:去除标记参数。
将标记参数对外隐藏,使用两个方法用加急和不加急发货时间的计算:

  • rushDeliveryDate
  • regularDeliveryDate

这样的话,调用者不必关心标记参数,直接根据情况调用这两个方法即可,减少理解成本。

   private Date deliveryDate(OrderBO anOrder, Boolean isRush) {
        // 是否加急
        if (isRush) {
            // 天
            int deliveryTimeDay = 0;
            if (DeliveryStateEnum.ORDERED.getState().equals(anOrder.getDeliveryState())) {
                deliveryTimeDay = 1;
            } else if (DeliveryStateEnum.PREPARED.getState().equals(anOrder.getDeliveryState())) {
                deliveryTimeDay = 2;
            }
            Calendar calendar = new GregorianCalendar();
            calendar.setTime(anOrder.getPlacedOn());
            // 日期往后推N天
            calendar.add(Calendar.DATE, deliveryTimeDay + 1);
            return calendar.getTime();
        } else {
            // 天
            int deliveryTimeDay = 1;
            if (DeliveryStateEnum.ORDERED.getState().equals(anOrder.getDeliveryState())) {
                deliveryTimeDay = 2;
            } else if (DeliveryStateEnum.PREPARED.getState().equals(anOrder.getDeliveryState())) {
                deliveryTimeDay = 3;
            }
            Calendar calendar = new GregorianCalendar();
            calendar.setTime(anOrder.getPlacedOn());
            // 日期往后推N天
            calendar.add(Calendar.DATE, deliveryTimeDay + 2);
            return calendar.getTime();
        }
    }

    /**
     * 加急
     * @param anOrder
     * @return
     */
    public Date rushDeliveryDate(OrderBO anOrder) {
        return deliveryDate(anOrder, true);
    }

    /**
     * 不加急,常规
     * @param anOrder
     * @return
     */
    public Date regularDeliveryDate(OrderBO anOrder) {
        return deliveryDate(anOrder, false);
    }

参考

[1] 重构:改善既有代码的设计

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值