代码的坏味道之一:过长参数列表
入参设计的几点建议:
- 方法的参数列表应该尽量避免重复,减少调用者的使用难度。
何为重复?就是这个参数 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] 重构:改善既有代码的设计