第四类:代码容易写错
案例13:在图1-15所示的这段代码里(摘自真实的应用代码),import的是springframework的类库,但参数的顺序是按照apache的类库方式传的,于是出错了。org.apache.commons.beanutils.BeanUtils和org.springframework.beans.BeanUtils两个同名的类有个同名方法copyProperties,差别是前者的参数是copyProperties(Objectdest,Objectorig),后者的参数是copyProperties(Objectorig,Objectdest)。
这段代码在我们团队内部引起了激烈讨论。有些人认为,可以依赖IDE的防错功能,这一功能是能提示当前对应参数名称的。但我们不能依赖IDE的功能,因为我们无法控制开发人员用什么IDE。
最后的共识是:应该设计成如下形式的Copiable接口,也就是说,让方法名称更加容易辨识,copyPropertiesFrom和copyPropertiesTo出错的可能性更小一些:
public interface Copiable {
default void copyPropertiesFrom(Object src) {
// 略
}
default void copyPropertiesTo(Object dest) {
// 略
}
}
▲图1-15容易写错的代码示例
案例14:2018年,某团队连续发生了两次类似的参数传错顺序的问题。
第一次,被调函数是doSignSHA256(StringprivateKey,Stringcontent,StringcharSet),但调用它的代码写成了doSignSHA256(content,privateKey,charSet),第一个参数和第二个参数传反了。
第二次,被调函数是DAO里的一个方法,是自动生成的,原来是selectByBillDate(StringMId,StringcontractId,StringbillDate,StringbizType),调用它的代码是selectByBillDate(mid,contractId,billDate,bizType),没有问题。但是某天开发人员调整了SQL参数的顺序,重新生成了DAO,这个函数就变成了selectByBillDate(StringbillDate,StringcontractId,StringMId,StringbizType),而这个开发人员又忘记修改调用它的代码了,于是mid和billDate传反了。
这类参数传错顺序的问题非常严重,每处可能发生参数传错顺序的地方都隐藏着资损事故的风险。我们抽样统计了一下我们的代码,发现:
每个微服务系统的代码里都有几百个public(公共)方法有两个或两个以上连续的String类型参数,其中大约50%是自动生成的代码(例如DAO)。
每个系统都有几个到几十个public方法有四个或四个以上连续的String类型参数。
个别系统有几百个public方法有四个或四个以上连续的String类型参数。
为了防止此类问题再次发生,建议:
增强静态代码扫描,从入参变量名和函数定义的变量名之间的相似性判断是否有可能传错顺序了。
加强代码的健壮性,增加入参校验。例如,mid和billDate的取值长度是不同的,而且是固定的。
少用String类型,多用强类型(例如Date类型)。
像增强静态代码扫描、提高测试覆盖率、增加防御性代码这些想法是不错的,但往上游走得还不够。往上游走的意思是:当遇到问题时,不但要想“怎么解决问题”,更要想“怎么让问题不用解决”。少用String类型、多用强类型是很好的“往上游走”的思路,用了强类型,一旦顺序传错了,编译就报错了。
针对参数传错顺序的问题,最好的做法是对代码进行自动重构。
自动生成一个参数类,把多个String值包进去;
自动生成强类型,替换String类型。
如下所示,一旦参数传错次序,编译器马上就发现了。
案例15:开发人员在写代码的时候,应该用UrlEncoder类的encode()方法,结果错用了decode()。从中吸取的教训是:encode()和decode()的函数入参定义要有差别,这样一旦用错马上就编译报错了。
案例16:金额放大100倍。
String actualCurrency = map.get(PAY_ACTUAL_CURRENCY);
String actualAmt = map.get(PAY_ACTUAL_AMT);
return new MultiCurrencyMoney(actualAmt, actualCurrency);
上面代码的问题是:map里的PAY_ACTUAL_AMT的单位是分,而MultiCurrencyMoney类的构造函数的第一个参数的单位是元。因此,上面的代码使得金额放大了100倍。
防错设计怎么做?
在系统内部传递金额值,必须在所有场合都严格使用数额、单位和币种三元组。金额值在数据库里的存储也必须严格使用三元组,不要用“数据库里都按分来存”这种口头约定。只有在系统和外界交互的时候,才根据对方系统的API和文件格式约定进行定制的序列化和反序列化。
案例17:使用完ThreadLocal,没有调用remove方法,导致内存泄漏。其实,这和C/C++语言中的malloc()没有free()的问题类似。严格杜绝这种问题的关键就是在代码框架层面做到不需要程序员记住要调用free()或remove方法。