设计模式之6大设计原则

单一职责原则/里氏替换原则/依赖倒置原则/接口隔离原则/迪米特法则/开闭原则

Posted by Will Wang on June 1, 2019
本文摘抄自《设计模式之禅》,结合自己的阅读感悟总结了一下。下面是书中的一些金句:
  • “变更才显真功夫”, 业务需求变更永无休止, 技术前进就永无止境, 在发生变更时才能发觉我们的设计或程序是否是松耦合。

单一职责原则

Single Responsibility Principle,简称是SRP。

定义
  • 单一职责原则的定义是: 应该有且仅有一个原因引起类的变更。
  • There should never be more than one reason for a class to change.
总结一下单一职责原则有什么好处:
  1. 类的复杂性降低, 实现什么职责都有清晰明确的定义;
  2. 可读性提高, 复杂性降低, 那当然可读性提高了;
  3. 可维护性提高, 可读性提高, 那当然更容易维护了;
  4. 变更引起的风险降低, 变更是必不可少的, 如果接口的单一职责做得好, 一个接口修 改只对相应的实现类有影响, 对其他的接口无影响, 这对系统的扩展性、 维护性都有非常大 的帮助。
注意
  • 单一职责原则提出了一个编写程序的标准, 用“职责”或“变化原因”来衡量接口或类设计得是否优良, 但是“职责”和“变化原因”都是不可度量的, 因项目而异, 因环境而异。
  • 对于单一职责原则, 我的建议是接口一定要做到单一职责, 类的设计尽量做到只有一个原因引起变化。

里氏替换原则

Liskov Substitution Principle,简称是LSP

在面向对象的语言中, 继承是必不可少的、 非常优秀的语言机制, 它有如下优点:
  1. 代码共享, 减少创建类的工作量, 每个子类都拥有父类的方法和属性;
  2. 提高代码的重用性;
  3. 子类可以形似父类, 但又异于父类, “龙生龙, 凤生凤, 老鼠生来会打洞”是说子拥有父的“种”, “世界上没有两片完全相同的叶子”是指明子与父的不同;
  4. 提高代码的可扩展性, 实现父类的方法就可以“为所欲为”了, 君不见很多开源框架的扩展接口都是通过继承父类来完成的;
  5. 提高产品或项目的开放性。
继承的缺点
  1. 继承是侵入性的。 只要继承, 就必须拥有父类的所有属性和方法;
  2. 降低代码的灵活性。 子类必须拥有父类的属性和方法, 让子类自由的世界中多了些约束;
  3. 增强了耦合性。 当父类的常量、 变量和方法被修改时, 需要考虑子类的修改, 而且在1. 缺乏规范的环境下, 这种修改可能带来非常糟糕的结果——大段的代码需要重构。
里氏替换原则的定义
  • If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
  • Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
  • 通俗点讲, 只要父类能出现的地方子类就可以出现, 而且替换为子类也不会产生任何错误或异常, 使用者可能根本就不需要知道是父类还是子类。 但是, 反过来就不行了, 有子类出现的地方, 父类未必就能适应。
里氏替换原则的含义
  1. 子类必须完全实现父类的方法
    • 在类中调用其他类时务必要使用父类或接口, 如果不能使用父类或接口, 则说明类的设计已经违背了LSP原则。
    • 如果子类不能完整地实现父类的方法, 或者父类的某些方法在子类中已经发生“畸变”, 则建议断开父子继承关系, 采用依赖、 聚集、 组合等关系代替继承。
  2. 子类可以有自己的个性
    • 向下转型(downcast) 是不安全的, 从里氏替换原则来看, 就是有子类出现的地方父类未必就可以出现。
  3. 覆盖或实现父类的方法时输入参数可以被放大
    • Design by Contract(契约设计) :先定义出WSDL接口, 制定好双方的开发协议, 然后再各自实现。
    • 子类中方法的前置条件(如参数类型)必须与超类中被覆写的方法的前置条件相同或者更宽松。
  4. 覆写或实现父类的方法时输出结果可以被缩小
    • 如果是覆写, 父类和子类的同名方法的输入参数是相同的, 两个方法的范围值S小于等于T, 这是覆写的要求, 这才是重中之重, 子类覆写父类的方法, 天经地义。
    • 如果是重载, 则要求方法的输入参数类型或数量不相同, 在里氏替换原则要求下, 就是子类的输入参数宽于或等于父类的输入参数, 也就是说你写的这个方法是不会被调用的, 参考上面讲的前置条件。 - 采用里氏替换原则的目的就是增强程序的健壮性, 版本升级时也可以保持非常好的兼容性。 即使增加子类, 原有的子类还可以继续运行。 在实际项目中, 每个子类对应不同的业务含义, 使用父类作为参数, 传递不同的子类完成不同的业务逻辑, 非常完美!

依赖倒置原则

Dependence Inversion Principle,简称是DIP

定义
  • High level modules should not depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions.
    1. 高层模块不应该依赖低层模块, 两者都应该依赖其抽象;
    2. 抽象不应该依赖细节;
    3. 细节应该依赖抽象。
特点
  1. 模块间的依赖通过抽象发生, 实现类之间不发生直接的依赖关系, 其依赖关系是通过接口或抽象类产生的;
  2. 接口或抽象类不依赖于实现类;
  3. 实现类依赖接口或抽象类。
  • 设计是否具备稳定性, 只要适当地“松松土”, 观察“设计的蓝图”是否还可以茁壮地成长就可以得出结论, 稳定性较高的设计, 在周围环境频繁变化的时候, 依然可以做到“我自岿然不动”。
  • 在Java中, 只要定义变量就必然要有类型, 一个变量可以有两种类型: 表面类型和实际类型, 表面类型是在定义的时候赋予的类型, 实际类型是对象的类型, 如zhangSan的表面类型是IDriver, 实际类型是Driver。
  • 抽象是对实现的约束, 对依赖者而言, 也是一种契约, 不仅仅约束自己, 还同时约束自己与外部的关系, 其目的是保证所有的细节不脱离契约的范畴, 确保约束双方按照既定的契约(抽象) 共同发展, 只要抽象这根基线在, 细节就脱离不了这个圈圈, 始终让你的对象做到“言必信, 行必果”。
对象的依赖关系有三种方式来传递
  1. 构造函数传递依赖对象
  2. Setter方法传递依赖对象
  3. 接口声明依赖对象
依赖倒置原则的本质就是通过抽象(接口或抽象类) 使各个类或模块的实现彼此独立,不互相影响, 实现模块间的松耦合
  1. 每个类尽量都有接口或抽象类, 或者抽象类和接口两者都具备
  2. 变量的表面类型尽量是接口或者是抽象类
  3. 任何类都不应该从具体类派生
  4. 尽量不要覆写基类的方法
  5. 结合里氏替换原则使用

接口隔离原则

英语:interface-segregation principles, 缩写:ISP

定义
  • 建立单一接口, 不要建立臃肿庞大的接口。 再通俗一点讲: 接口尽量细化, 同时接口中的方法尽量少。
接口隔离原则是对接口进行规范约束
  • 接口要尽量小
    • 根据接口隔离原则拆分接口时, 首先必须满足单一职责原则。
  • 接口要高内聚
    • 高内聚就是提高接口、 类、 模块的处理能力, 减少对外的交互。
    • 要求在接口中尽量少公布public方法, 接口是对外的承诺, 承诺越少对系统的开发越有利, 变更的风险也就越少, 同时也有利于降低成本。
  • 定制服务
    • 定制服务就是单独为一个个体提供优良的服务。
  • 接口设计是有限度的
    • 接口设计一定要注意适度, 这个“度”如何来判断呢? 根据经验和常识判断, 没有一个固化或可测量的标准。
最佳实践
  1. 一个接口只服务于一个子模块或业务逻辑;
  2. 通过业务逻辑压缩接口中的public方法,接口时常去回顾,尽量让接口达到“满身筋骨肉”,而不是“肥嘟嘟”的一大堆方法;
  3. 已经被污染了的接口,尽量去修改,若变更的风险较大,则采用适配器模式进行转化处理;
  4. 了解环境,拒绝盲从。每个项目或产品都有特定的环境因素,别看到大师是这样做的你就照抄。千万别,环境不同,接口拆分的标准就不同。深入了解业务逻辑,最好的接口设计就出自你的手中!

迪米特法则

迪米特法则(Law of Demeter, LoD) 也称为最少知识原则(Least Knowledge Principle, LKP)

定义
  • 一个对象应该对其他对象有最少的了解。
迪米特法则对类的低耦合提出了明确的要求
  • 一个类只和朋友交流,不与陌生类交流,不要出现getA().getB().getC().getD()这种情况(在一种极端的情况下允许出现这种访问,即每一个点号后面的返回类型都相同),类与类之间的关系是建立在类间的,而不是方法间,因此一个方法尽量不引入一个类中不存在的对象,当然,JDK API提供的类除外。
  • 迪米特法则要求类“羞涩”一点,尽量不要对外公布太多的public方法和非静态的public变量,尽量内敛,多使用private、package-private、protected等访问权限。
  • 如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中。
  • 谨慎使用Serializable
总结
  • 迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高。其要求的结果就是产生了大量的中转或跳转类,导致系统的复杂性提高,同时也为维护带来了难度。读者在采用迪米特法则时需要反复权衡,既做到让结构清晰,又做到高内聚低耦合。

开闭原则

The Open/Closed Principle (OCP)

定义
  • Software entities like classes,modules and functions should be open for extension but closed for modifications.
  • 一个软件实体如类、 模块和函数应该对扩展开放, 对修改关闭。
what
  • 开闭原则对扩展开放, 对修改关闭, 并不意味着不做任何修改, 低层模块的变更, 必然要有高层模块进行耦合, 否则就是一个孤立无意义的代码片段。
why
  1. 开闭原则对测试的影响
  2. 开闭原则可以提高复用性
  3. 开闭原则可以提高可维护性
  4. 面向对象开发的要求
how
  1. 抽象约束
  2. 元数据(metadata) 控制模块行为
  3. 制定项目章程
  4. 封装变化
总结
  • 建立稳定、 灵活、 健壮的设计, 而开闭原则又是重中之重, 是最基础的原则, 是其他5大原则的精神领袖。
    • 开闭原则也只是一个原则
    • 项目规章非常重要
    • 预知变化
  • 开闭原则是一个终极目标, 任何人包括大师级人物都无法百分之百做到, 但朝这个方向努力, 可以非常显著地改善一个系统的架构, 真正做到“拥抱变化”。