“开一闭” 原则(OCP)
经典力学的基石是牛顿三大定律。 而面向对象的可复用设计 (Object Oriented Design, 或 OOD) 的第一块基石,便是所谓的”开-闭“原则 (Open-Closed Principle, 常缩写为OCP)。
“开-闭 ” 原则讲的是:一个软件实体应当对扩展开放, 对修改关闭。 这一原则最早由 Bertrand Meyer [MEYER88]提出, 英文原文是:Software entities should be open for extension, but closed for modification.
这个原则说的是, 在设计一个模块的时候, 应当使这个模块可以在不被修改的前提下被扩展。 换言之, 应当可以在不必修改源代码的情况下改变这个模块的行为。
所有的软件系统都有一个共同的性质, 即对它们的需求都会随时间的推移而发生变化。 在软件系统面临新的需求时, 系统的设计必须是稳定的。 满足 “开-闭” 原则的设计可以给一个软件系统两个无可比拟的优越性:
- 通过扩展已有的软件系统, 可以提供新的行为, 以满足对软件的新需求, 使变化中的软件系统有一定的适应性和灵活性。
- 已有的软件模块,特别是最重要的抽象层模块不能再修改, 这就使变化中的软件系统有一定的稳定性和延续性。
具有这两个优点的软件系统是一个在高层次上实现了复用的系统, 也是一个易于维护 的系统。
里氏代换原则(LSP)
从“开-闭 ” 原则中可以看出面向对象设计的重要原则是创建抽象化,并且从抽象化导出具体化。 具体化可以给出不同的版本, 每个版本都给出不同的实现。
从抽象化到具体化的导出要使用继承关系和这里要引入的里氏代换原则 (Liskov Substitution Principle, 常缩写为 LSP) 。 里氏代换原则由 Barbara Liskov 提出。
里氏代换原则的严格表达是:
如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以Tl定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有变化,那么类型T2是类型T1的子类型。
换言之,一个软件实体如果使用的是一个基类的话,那么一定适用于其子类,而且它根本不能察觉出基类对象和子类对象的区别。
比如,假设有两个类,一个是Base类,另一个是Derived类,而且Derived类是Base类的子类,那么一个方法如果可以接受一个基类对象b的话:
method1(Base b)
那么它必然可以接受一个子类对象d,也即可以有methodl(d)。
里氏代换原则是继承复用的基石。只有当衍生类可以替换掉基类,软件单位的功能不会受到影响时,基类才能真正被复用,而衍生类也才能够在基类的基础上增加新的行为。
反过来的代换不成立。
依赖倒转原则(DIP)
实现 “ 开-闭 ” 原则的关键是抽象化, 并且从抽象化导出具体化实现。 如果说 “ 开-闭 ” 原则是面向对象设计的目标的话, 依赖倒转原则就是这个面向对象设计的主要机制[MARTIN00] 。
依赖倒转原则讲的是: 要依赖于抽象, 不要依赖于具体.
简单地说, 依赖倒转原则 (Dependence Inversion Principle) 要求客户端依赖于抽象耦合。 依赖倒转原则的表述是:
抽象不应当依赖于细节,细节应当依赖于抽象。
(Abstractions should not depend upon details. Details should depend upon abstractions)
依赖倒转原则的另一种表述是:
要针对接口编程, 不要针对实现编程。
(Program to an interface, not an implementation)
第二种表述是[GOF95]一书所强调的。
针对接口编程的意思就是说, 应当使用 Java 接口和抽象 Java 类进行变量的类型声明、参量的类型声明、 方法的返还类型声明, 以及数据类型的转换等。
不要针对实现编程的意思就是说, 不应当使用具体 Java 类进行变量的类型声明、 参 量的类型声明、 方法的返还类型声明, 以及数据类型的转换等。
要保证这一点,一个具体Java类应当只实现Java接口和抽象Java类中声明过的方法,而不应当给出多余的方法。
倒转依赖关系强调一个系统内的实体之间关系的灵活性。基本上,如果设计师希望遵循”开-闭“原则,那么倒转依赖原则便是达到要求的途径。
接口隔离原则(ISP)
接口隔离原则 (Interface Segregation Principle, 常常略写做 ISP) 讲的是: 使用多个专门的接口比使用单一的总接口要好。
换言之, 从一个客户类的角度来讲: 一个类对另外一个类的依赖性应当是建立在最小的接口上的。
人们所说的 “接口” 往往是指两种不同的东西: 一种是指 Java 语言中的有严格定义的 Interface 结构, 比如java.lang.Runnable 就是一个 Java 接口;另一种就是一个类型所具有的方法特征的集合,也称做 “接口”,但仅是一种逻辑上的抽象。
应于这两种不同的用词, 接口隔离原则的表达方式以及含义都有所不同。
过于臃肿的接口是对接口的污染(InterfaceContamination)。
与迪米特法则的关系
迪米特法则要求任何一个软件实体,除非绝对需要,不然不要与外界通信。即使必须进行通信,也应当尽量限制通信的广度和深度。
显然,定制服务原则拒绝向客户端提供不需要提供的行为,是符合迪米特法则的。
合成/聚合复用原则 (CARP)
合成/聚合复用原则 (Composite/Aggregate Reuse Principle, 或 CARP) 经常又叫做合成复用原则 (Composite Reuse Principle 或 CRP) 。 合成/聚合复用原则就是在一个新的对象里面使用一些已有的对象, 使之成为新对象的部分;新的对象通过向这些对象的委派达到复用已有功能的目的。
这个设计原则有另一个更简短的表述: 要尽量使用合成/聚合, 尽量不要使用继承。
合成 (Composite) 一词的使用很广泛, 经常导致混淆。 为避免这些混淆, 不妨先来考察一下 “ 合成” 与 “聚合” 的区别。
合成和聚合的区别
合成 (Composition) 和聚合 (Aggregation) 均是关联 (Association) 的特殊种类。 聚合用来表示 “拥有” 关系或者整体与部分的关系;而合成则用来表示一种强得多的 “拥有” 关系。 在一个合成关系里, 部分和整体的生命周期是一样的。 一个合成的新的对象完全拥有对其组成部分的支配权, 包括它们的创建和湮灭等。 使用程序语言的术语来讲, 组合而成的新对象对组成部分的内存分配、 内存释放有绝对的责任。
更进一步来讲, 一个合成的多重性 (Multiplicity) 不能超过1, 换言之, —个合成关系中的成分对象是不能与另一个合成关系共享的。 一个成分对象在同一个时间内只能属于一个合成关系。 如果一个合成关系湮灭了, 那么所有的成分对象要么自己湮灭所有的成分对象(这种情况较为普遍), 要么就得将这责任交给别人(这种情况较为罕见)。
用 C 程序员较易理解的语言来讲, 合成是值的聚合 (Aggregation by Value), 而通常所说的聚合则是引用的聚合 (Aggregation by Reference) 。
迪米特法则(LoD)
迪米特法则 (Law of Demeter 或简写为 LoD) 又叫做最少知识原则 (Least Knowledge Principle 或简写为 LKP), 就是说,一个对象应当对其他对象有尽可能少的了解。
迪米特法则最初是用来作为面向对象的系统设计风格的一种法则, 于 1987 年秋天由Ian Holland 在美国东北大学 (Northeastern University) 为一个叫做迪米特 (Demeter) 的项目设计提出的, 因此叫做迪米特法则[LIEB89] [LIEB86] 。 这条法则实际上是很多著名系统, 比如火星登录软件系统、 木星的欧罗巴卫星轨道飞船的软件系统的指导设计原则。
迪米特法则的各种表述
没有任何一个其他的OO设计原则像迪米特法则这样有如此之多的表述方式, 下面给出的也只是众多的表述中较有代表性的几种:
- 只与你直接的朋友们通信 (Only talk to your immediate friends)。
- 不要跟 “ 陌生人” 说话 (Don't talk to strangers)。
- 每一个软件单位对其他的单位都只有最少的知识, 而且局限于那些与本单位密切相关的软件单位。
在上面的表述里面, 什么是 “ 直接 ”、”陌生” 和 “ 密切 ” 则被有意地模糊化了, 以便在不同的环境下可以有不同的解释。
狭义的迪米特法则
如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。 如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。
狭义的迪米特法则的缺点
遵循狭义的迪米特法则会产生一个明显的缺点:会在系统里造出大量的小方法,散落在系统的各个角落。这些方法仅仅是传递间接的调用,因此与系统的商务逻辑无关。当设计师试图从一张类图看出总体的架构时,这些小的方法会造成迷惑和困扰。
为了克服狭义的迪米特法则的缺点,可以使用依赖倒转原则,引入一个抽象的类型引用“抽象陌生人”对象,使“某人”依赖于“抽象陌生人”。换言之,就是将“抽象陌生人”变成朋友。
参考资料
《Java与模式》
个人介绍:
高广超:多年一线互联网研发与架构设计经验,擅长设计与落地高可用、高性能、可扩展的互联网架构。
本文首发在 高广超的博客 转载请注明!