如何同时提高一个软件系统的可维护性(Maintainability)和可复用性(Reuseability)是面向对象的设计要解决的核心问题。
面向对象设计中,设计模式被提及的比较多,然而,大家对在设计模式背后、更深层的、更具有普遍性的、共同的思想原则提及却较少。但它们是比设计模式本身更加基本和单纯的设计思想。
面向对象的设计原则包括:“开-闭”原则(OCP)、里氏代换原则(LSP)、依赖倒转原则(DIP)、接口隔离原则(ISP)、合成/聚合复用原则(CARP)、迪米特法则(LoD)。
这些面向对象设计原则是提高软件兄的可维护性和可复用性的指导性原则。在有现成的设计模式的地方,设计模式就是这些设计原则在具体问题的体现;在没有现成的设计模式的地方,这些设计原则也一样适用,同样可以对系统设计发挥指导作用,并对新模式的研究提供向导。
一个好的系统设计应该达到三个目标:
1)可扩展性(Extensibility)
新的功能可以很容易地加入到系统中
2)灵活性(Flexibility)
可以允许代码修改平稳地发生,而不会波及到很多其他的模块
3)可插入性(Pluggability)
可以很容易地将一个类抽出去,同时将另一个有同样接口的类加入进来
复用的好处:1)较高的生产效率;2)较高的软件质量;3)恰当使用可改善系统的可维护性
1)代码的剪贴复用;2)算法的复用;3)数据结构的复用
对设计目标的支持。
1)系统的可扩展性是由“开-闭”原则、里氏代换原则、依赖倒转原则和组合/聚合复用原则所保证;
2)系统的灵活性是由“开-闭”原则、迪米特法则、接口隔离原则所保证;
每个模块相对于其他模块独立存在,并只保持与其他模块尽可能少的通信
3)系统的可插入性由“开-闭”原则、里氏代换原则、组合/聚合复用原以及依赖倒转原则保证
面向对象的可复用设计的第一块基石是“开-闭”原则。
“开-闭”原则讲的是:一个软件应当对扩展开放,对修改关闭。
也就是说,在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展。换言之,应当可以再不必修改源代码的情况下改变这个模块的行为。
满足“开-闭”原则的设计可以给一个软件系统两个无可比拟的优越性:
1)通过扩展已有的系统,可以提供新的行为,以满足对软件的新需求,使变化中的软件系统有一定的适应性和灵活性;
2)已有的软件模块,特别是最重要的抽象层模块不能再修改,这就使变化中的软件系统有一定的稳定性和延续性。
怎么做到“开-闭”原则?
1)抽象化是关键。在抽象层定义好接口,抽象层预见了可能的扩展,使得抽象层不需修改,从而满足“对修改关闭”;同时,由于从抽象层导出一个或多个新的具体类可以改变系统的行为,因此系统的设计对扩展是开放的,这就满足了“开-闭“原则的第一条。
2)对可变性的封装原则:找到一个系统的可变因素,将之封装起来。
①对可变性的封装不应该散落在代码的很多角落,而应当被封装到一个对象里面。继承可以看做是一种封装变化的方法。
②一种可变性不应当与另一种可变性混合在一起。
从“开-闭”原则中可以看出面向对象的重要原则是创建抽象化,并且从抽象化导出具体化。具体化可以给出不同的版本,每一个版本都给出不同的实现。
从抽象化到具体化的导出要使用继承关系和里氏代换原则。
里氏代换原则:一个系统如果使用的是一个基类的话,那么一定适用于其子类,而且它根本不能觉察出基类对象和子类对象的区别。反之不成立。
里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体体现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
要依赖于抽象,不要依赖于实现。抽象不应当依赖于细节;细节应当依赖于抽象;针对接口编程,不要针对实现编程。
实现“开-闭”原则的关键是抽象化,并且从抽象化导出具体化实现。“开-闭”原则是目标,而达到这一目标的手段是依赖倒转原则。换言之,要实现“开-闭”原则,就应当坚持依赖倒转原则。违反依赖倒转原则,就不可能达到“开-闭”原则的要求。
使用多个专门的接口比使用单一的总接口要好;应当为客户端提供尽可能小的单独的接口,而不要提供大的总接口。
在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象的委派达到复用已有功能的目的。
尽量使用合成/聚合,而不要使用继承。
一个对象应当对其他对象有尽可能少的了解;一个软件实体应当与尽可能少的其他实体发生相互作用。
如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。
一个遵循迪米特法则设计出来的系统在功能需要扩展时,会相对更容易地做到对修改的关闭。也就是说,迪米特法则是一条通向“开-闭”原则的道路。