这是我参与更文挑战的第26天,活动详情查看: 更文挑战
背景
这是之前参加的一个工程师交流会上别人分享的一个小议题,做了一些笔记,后面整理资料的时候又从网上搜集了一些做补充,今天分享一下
代码的可测性
测试性不好的代码特征
缺陷1: 构造函数做了实际工作
- 构造函数或域声明中出现new字眼
- 构造函数或域声明中调用静态方法
- 构造函数做了分配域字段之外的事情
- 构造函数中,对象初始化工作没有完成彻底(小心初始化方法)
- 构造函数中,出现了控制流(基于条件或循环的逻辑)
- 在构造函数内构造复杂的对象图,而不是使用工厂(factory)模式或构造器(builder)模式
- 增加或使用初始化块
缺陷2: 滥用协作类
- 引入了对象,却不直接使用(而是用于通往其它对象)
- 不遵守迪米特法则:对象图中,方法引用链包含了多个下标点(.)
- 可疑命名:如context,environment,principal,container或manager
缺陷3: 脆弱的全局变量&单例(Singleton)
- 增加或使用单例
- 增加或使用静态域字段或静态方法
- 增加或使用静态初始化块
- 增加或使用寄存器
- 增加或使用服务定位器
缺陷4: 类做事太多
- 总结该类的作用时,得使用以及之类的描述语
- 团队新成员很难读懂和快速接手该类
- 该类的某些域字段只用到部分方法中
- 该类的某些静态方法仅针对参数操作
构造函数
一个常见的测试性不好的地方就是在构造函数中做了实际工作,在构造函数中执行功能相当于让对象实例化难上加难,或者说给对象模拟增加困难
例1: 构造函数或域声明中出现了new字眼
class House {
House() {
kitchen = new Kitchen();
bedroom = new Bedroom();
}
}
上面代码的问题:
- 混杂了对象图的创建与功能逻辑,对象图是指创建不同的实例对象,如何在这些对象之间进行逻辑处理则是独立的
- 上面的代码或许容易实例化,但如果Kitchen代表的是某类高成本事项,如文件/数据库存取等,那么它就不太易测,因为难以使用模拟对象类替代Kitchen或Bedroom
- 设计很脆弱,因为不能将House里的kitchen或bedroom的行为以多态的方式进行替换
良好的代码风格是不要在构造函数中创建协作对象,而是将已创建的对象作为参数传递进去
class House {
House(Kitchen &k, Bedroom &b) {
kitchen = k;
bedroom = b;
}
}
在随后测试时,可以先在外围创建协作对象,然后在创建House对象时,通过参数传入
例2: 构造函数拿到一个部分初始化的对象,而且必须设置它
class Garden {
Garden(Gardener joe) {
joe.setWorkday(new TwelveHourWorkday());
joe.setBoots(new BootsWithMassiveStaticInitBlock());
this.joe = joe;
}
}
- 建立对象(为Garden创建和配置协作对象Gardener)的工作不应当由Garden来做,若配置和实例化混杂在构造函数中,对象就变得更不友好,并且与特定的实体对象图结构绑定,这样就使得代码难以修改,而且不易测
- Garden需要Gardner,但配置garnder不是Garden的职责
- Garden的workday在构造函数中被特别设定了,因此迫使让Joe每个workday工作12小时,像这样的强制依赖是不友好的
良好的代码风格是把已经完全初始化的协作对象传进需要的类中
class Garden {
Garden(Gardener joe) {
this.joe = joe;
}
}
例3: 构造函数违反迪米特法则(一个实体要尽可能的只与和它最近的实体进行通讯)
class AccountView {
User user;
AccountView() {
user = RPCClient.getInstance().getUser();
}
}
- 上面的代码只需要User对象,但是却访问了全局作用域中的RPCClient的单例(singleton)
- 上面的代码不仅在构造函数中做了实际工作,破坏了静态方法的无缝性,而且违反了迪米特法则
- 为了测试上面的代码,就必须创建一个RPCClient实例(静态方法不可避免,也无法模拟),但是被测试类不需要RPCClient,只需要User,但是为了测试,就必须做这些额外的环境布置,导致可测性不好
- 所有需要构造AccountView类的测试都必须处理这些问题,比如AccountServlet可能需要AccountView,而在测试AccountServlet时,RPCClient实例的预布置工作都要做
良好的代码风格是只传入直接需要的东西,比如协作对象User,这样在测试时,只需要创建真实的或模拟的User对象,这种设计更加灵活而且更具可测性
class AccountView {
User user;
AccountView(User user) {
this.user = user;
}
}
User getUser(RPCClient rpcClient) {
return rpcClient.getUser();
}
RPCClient getRPCClient() {
return new RPCClient();
}
例4: 在构造函数中创建并不需要的第三方对象
class Car {
Engine engine;
Car(File file) {
String model = readEngineModel(file);
engine = new EngineFactory().create(model);
}
}
- 上面代码为了制造自己的引擎engine,要求一辆汽车Car去获取一个引擎工厂EngineFactory,这是难以理解的,汽车得到的应该是造好的引擎,而不是去弄清如何制造引擎
- 构造函数并不需要直接访问第三方对象,而只是需要第三方对象创建的东西
- 上面代码在构造函数中创建第三方对象,无法注入也无法覆盖,但却致使代码更加脆弱,因为无法换掉factory对象,也不能试图缓存它,而且一旦新的Car对象被创建,就无法阻止第三方对象的继续运行
- 所有需要构造Car类的测试都必须处理这个问题,比如有个Garage的测试需要Car,为了顺利调用到Car的构造函数,就不得不去创建新的EngineFactory
良好的代码风格是去掉第三方对象,用简单的变量赋值取代它们在构造函数中的工作,将预先配置好的变量加入到构造函数的成员变量中,让别的对象(factory,builder或Guice Provider)为构造函数参数完成构造工作; 将主体对象从对象图构建职责中解脱出来,可得到一个更灵活,维护性更好的设计
class Car {
Engien engine;
Car(Engien engine) {
this.engine = engine;
}
}
Engien getEngien(EngineFactory engineFactory, String model) {
return engineFactory.create(model);
}