前言
本系列主要记录了对【设计模式之禅】的学习总结,并例举了如何使用 Swift 语言实现。记录设计模式的六大原则:单一职责原则,开闭原则,里氏替换原则,迪米特法则,接口隔离原则,依赖倒置原则。
初识六大原则
- 单一职责原则(Single Responsibility Principle,SRP)
- 开闭原则(Open Closed Principle, OCP)
- 里氏替换原则(Liskov Substitution Principle, LSP)
- 迪米特法则(Law of Demeter, LoD)
- 接口隔离原则(Interface Segregation Principles, ISP)
- 依赖倒置原则(Dependence inversion Principle, DIP)
把上面 6 个原则的首字母(里氏替换原则和迪米特法则的首字母重复,只取一个)联合起来就是 SOLID(solid,稳定的),其代表的含义也就是把这 6 个原则结合使用的好处:建立稳定、灵活、健壮的设计。而开闭原则又是重中之重,是基础的原则,是其他 5 大原则的精神领袖。
单一职责原则(Single Responsibility Principle,SRP)
单一职责原则的定义:应该有且仅有一个原因引起类的变更。
单一职责的好处:
- 类的复杂性降低,实现什么职责都有清晰明确的定义。
- 可读性提高。复杂性降低,那当然可读性提高了。
- 可维护性提高。可读性提高,那当然更容易维护了。
- 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。
单一指责适用于接口、类,同时也适用于方法。一个方法尽可能只做一件事。不要让被人猜测这个方法可能是用来处理什么逻辑的。 接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。
里氏替换原则(Liskov Substitution Principle, LSP)
有两种定义:
第一种定义,也是最正宗的定义:如果对每一个类型为 S 的对象 o1,都有类型为 T 的对象 o2,使得以 T 定义的所有程序 P 在所有的对象 o2 都代换成 o1 时(o2 -> o1),程序 P 的行为没有发生变化,那么类型 S 是类型 T 的子类型。
第二种定义:所有引用基类的地方必须能透明地使用其子类对象。
第二种定义是最清晰明确的,通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类,但是反过来就不行了,有子类出现的地方,父类未必能适应。
其定义包含了 4 层含义:
- 子类必须完全实现父类的方法
- 子类可以有自己的个性
- 覆盖或实现父类的方法时输入参数可以被放大
- 覆盖或实现父类的方法时输出结果可以被缩小
在项目中,采用里氏替换原则时,尽量避免子类的“个性”,一旦子类有“个性”,这个子类和父类之间的关系就很难调和了,把子类当作父类使用,子类的“个性”被抹杀————委屈了点;把子类单独作为一个业务来使用,则会让代码间的耦合关系变得扑朔迷离——缺乏类替换的标准。
依赖倒置原则(Dependence inversion Principle, DIP)
其原始定义为:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。其核心思想是:要面向接口编程,不要面向实现编程。
采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。
抽象是对实现的约束,对依赖者而言,也是一种契约,不仅仅约束自己,还同时约束自己与外部的关系,其目的是保证所有的细节不脱离契约的范畴,确保约束双方按照既定的契约(抽象)共同发展,只要抽象这根基线在,细节就脱离不了这个圈圈,始终让你的对象做到“言必信,行必果”。
对象的依赖关系有三种方式来传递:
- 构造函数传递依赖对象。
- Setter 方法传递依赖对象。
- 接口声明依赖对象。
依赖倒置原则的本质就是通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合。只要遵循一下几个规则,即可使用这个规则:
- 每个类尽量都有接口或抽象类,或者抽象类和接口都具备。
- 变量的表面类型尽量是接口或者是抽象类。
- 任何类都不应该从具体类派生。
- 尽量不要覆写基类的方法。
- 结合里氏替换原则使用。
结合里氏替换原则,父类出现的地方子类就能出现,再结合依赖倒置原则,我们可以得出一个通俗的规则:接口负责定义 public 属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化。
接口隔离原则(Interface Segregation Principles, ISP)
原始定义:客户端不应该依赖它不需要的接口。另一种定义:类间的依赖关系应该建立在最小的接口上。
首先需要说明,接口分为两种:
- 实例接口。
- 类接口。
我们可以把这两个定义概括为一句话:建立单一接口,不要建立臃肿庞大的接口,再通俗一点讲:接口尽量细化,同时接口中的方法尽量少。
接口隔离原则是对接口进行规范约束,包含以下四层含义:
- 接口要尽量小。
- 接口要高内聚。
- 定制服务。
- 接口设计是有限度的。
接口隔离原则是对接口的定义,同时也是对类的定义,接口和类尽量使用原子接口或原子类来组装。在实践中可以根据以下几个规则来划分原子:
- 一个接口只服务于一个子模块或业务逻辑。
- 通过业务逻辑压缩接口中的 public 方法,接口时常去回顾,尽量让接口达到“满身筋骨肉”,而不是“肥嘟嘟”的一大堆方法。
- 已经被污染了的接口,尽量去修改,若变更的风险较大,则采用适配器模式进行转化处理。
- 了解环境,拒绝盲从。
迪米特法则(Law of Demeter, LoD)
迪米特法则也称为最少知识原则(Least Knowledge Principle, LKP),虽然名字不同,但描述的是同一个规则:一个对象应该对其他对象有最少的了解。通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没关系,那是你的事,我就知道你提供的这么多 public 方法,我就调用这么多,其他的我一概不关心。
包含以下四层含义:
- 只和朋友交流。
- 朋友间也是有距离的。
- 是自己的就是自己的。如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,就放置在本类中。
- 谨慎使用 Serializable。
迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高。
开闭原则(Open Closed Principle, OCP)
开闭原则的定义:一个软件实体如类、模块、函数应该对扩展开放,对修改关闭。其含义是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。
这里的软件实体包括以下几个部分:
- 项目或软件产品中按照一定的逻辑规则划分的模块。
- 抽象和类。
- 方法。
重要性:
- 对测试的影响;只需要对扩展的代码进行测试即可。
- 可以提高复用率。
- 可以提高可维护性。
- 面向对象开发的要求。
实现方法:
- 抽象约束。
- 元数据(metadata)控制模块行为。
- 制定项目章程。
- 封装变化。
在使用过程中也要主要以下几个问题:
- 开闭原则也只是一个原则。
- 项目规章非常重要。
- 预知变化。