上面谈到代码的可测试性,如果只是到了编程阶段才谈可测试性,可能就太迟了。就像足球,到了青年阶段再抓,足球水平很难得到质的提高,而是要从娃娃抓起。可测试性也要从设计抓起,也就是人们经常提到的“Designfor testability”。 设计驱动产品开发,即设计决定了代码 。通过设计可确保系统结构的简单性,也只有通过设计,才能定义模块的接口具有清晰的输入和输出,以保证系统的可观察性和可控制性。
MVC设计模式是在设计阶段如何保证可测试性的一个典型例子。除了MVC,还有其他一些设计模式,也能很好地保证系统结构的简单性、可控制性或可观察性。在设计上改善软件的可测试性,主要是通过设立观察点、控制点、驱动装置、隔离装置等来实现的。而且,在改善软件系统可测试性的同时,不应该降低系统的性能、安全性和可靠性。通过设计来提高可测试性,有如下一些具体的做法。
1) 测试驱动设计方法,例如:先确定验收测试用例,再设计具体的功能;先确定性能、可靠性等测试用例,再考虑如何实施架构设计,以满足不同特性的要求。
2) 选用开放、成熟的设计模式和框架,在一定程度上就能保证系统结构的低耦合性,单一的依赖关系,具有较高的可测试性。
3) 数据显示与控制分离,通过分层,增加了系统的可观察性和可控制性。这样,就可以通过接口调用,分别完成相应的业务逻辑、数据处理等的测试。
4) 可控制性设计,包括业务流程、模块、场景、全局变量、接口等的可控制性设计,即在外部提供适当的方法、途径直接或间接控制相应的模块、全局变量和接口等。这些途径可能包括设立XML配置文件、暴露API接口、统一接口操作等。
5) 针对模块,尽量分解到相对稳定、规模合适的程度,以确保模块的独立性和稳定性,有利于独立开展对模块的测试活动。
6) 易理解性设计,包括明确的设计标准、规范的设计文档、明确的接口及其参数的定义,使设计有据可依,层次清晰,设计文档易读。
另外,更重要的是要遵守设计原则,著名的6项面向对象的设计原则如下。
1) 开 - 闭原则(Open-Close Principle,OCP): 一个软件实体应当对扩展开放,对修改关闭。这是最重要、最为抽象的原则,也是为了保证可复用性。换个角度讲,它就是“对可变性封装原则”,即一种可变性不应当散落在代码的很多角落里,而应当被封装到一个对象里面,并且一种可变性不应当与另一种可变性混合在一起,即类图的继承结构一般不应超过两层。
2) 里氏代换原则(Liskov Substituition Principle,LSP): 这是一种很严格的原则,要求“子类必须能够替换基类,否则不应当设计为其子类”,即子类只能去扩展基类,而不是隐藏或覆盖基类。
3) 依赖倒换原则(Dependence Inversion Principle,DIP): “设计要依赖于抽象而不是具体化”。人的思维实际上是很抽象的,我们先将整个问题都构思出来,在开始时不要过于关注具体的细节、不必太多依赖于实现。所以面向抽象设计是符合人的思维的,也很好地支持上面的OCP原则。
4) 接口隔离原则(Interface Segregation Principle,ISP): 使用多个专门的接口比使用单一的接口要好。一个类对另外一个类的依赖性应当建立在最小接口上。如果客户端只需要某一些方法的话,那么就应当向客户端提供这些需要的方法,而不要提供不需要的方法。提供接口意味着向客户端做出承诺,但过多的承诺会给系统的维护造成不必要的负担。
5) 合成、聚合复用原则(Composition/Aggregation Reuse Principle,CARP): 要尽量使用合成、聚合,尽量不要使用继承。在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分,新的对象通过向这些对象的委派,达到复用已有功能的目的。因为成分对象的内部细节是新对象所看不到的,而这种复用可以在运行时间内动态进行,新对象可以动态地引用与成分对象类型相同的对象。合成、聚合可以应用到任何环境中去,而继承只能应用到一些有限环境中去,这就是CARP原则的优势。
6) 迪米特法则或最少知识原则(Law of Demeter 或 Least KnowlegdePrinciple,LoD 或 LKP): 一个对象应当尽可能少地去了解其他对象,主要用意是控制信息的过载,即松耦合(Loosely-Coupled)的法则。类之间的耦合越弱,就越有利于复用。在类的结构设计上,每一个类都应当尽量降低成员的访问权限。一个类不应当public自己的属性,而应当提供取值和赋值的方法,让外界间接访问自己的属性。在类的设计上,只要有可能,一个类应当设计成不变类。在对其他对象的引用上,一个类对其他对象的引用应该降到最低。
更多的设计原则可以参考Object-Oriented Design Heuristics《OOD启思录》一书
。