设计模式的设计原则
从今天开始,计划把设计模式整理一下,主要是学习 秦小波的《设计模式之禅》中的内容,加上自己的一点想法。
单一职责原则(Single Responsibility Principle 简称SRP)
定义
应该有且仅有一个原因引起类的变更(There should never be more than one reason for a class to change)
优点
- 类的复杂性降低,实现什么职责都有清晰明确的定义;
- 可读性提高,复杂性降低,那当然可读性提高了;
- 可维护性提高,可读性提高,那当然更容易维护了;
- 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助,单一职责适用于接口、类,同时也适用于方法
缺点
- 关于职责的定义比较模糊,没有量化的标准
最佳实践
对于单一职责原则,建议接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。
里氏替换原则(Liskov Substitution Principle 简称LSP)
定义
第一种定义,也是最正宗的定义: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.(如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。)
第二种定义:Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.(所有引用基类的地方必须能透明地使用其子类的对象。)
即:只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应
里氏替换原则为良好的继承定义了一个规范,包括一下四点
- 子类必须完全实现父类的方法
- 子类可以有自己的个性
- 覆盖或实现父类的方法时输入参数可以被放大(就是输入参数的类型宽于父类的类型覆盖范围)
- 覆写或实现父类的方法时输出结果可以被缩小
最佳实践
在项目中,采用里氏替换原则时,尽量避免子类的“个性”,一旦子类有“个性”,这个子类和父类之间的关系就很难调和了,把子类当做父类使用,子类的“个性”被抹杀——委屈了点;把子类单独作为一个业务来使用,则会让代码间的耦合关系变得扑朔迷离——缺乏类替换的标准
依赖倒置原则(Dependence Inversion Principle 简称DIP)
每一个逻辑的实现都是由原子逻辑组成的,不可分割的 原子逻辑就是低层模块,原子逻辑的再组装就是高层模块。抽象就是指接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节,其特点就是可以直接被实例化
定义
- 高层模块不应该依赖低层模块,两者都应该依赖其抽象;
- 抽象不应该依赖细节;
- 细节应该依赖抽象。
依赖倒置原则在PHP语言中的表现为
- 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的;
- 接口或抽象类不依赖于实现类;
- 实现类依赖接口或抽象类
优点
采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风 险,提高代码的可读性和可维护性
依赖的三种实现方式
- 构造函数传递依赖对象,在类中通过构造函数声明依赖对象,按照依赖注入的说法,这种方式叫做构造函数注入,按照这种方式的注入
- Setter方法传递依赖对象,在抽象中设置Setter方法声明依赖关系,依照依赖注入的说法,这是Setter依赖注入,按 照这种方式的注入
- 接口声明依赖对象,在接口的方法中声明依赖对象,该方法也 叫做接口注入。
最佳实践
- 每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备
- 变量的表面类型尽量是接口或者是抽象类
- 任何类都不应该从具体类派生
- 尽量不要覆写基类的方法(如果基类是一个抽象类,而且这个方法已经实现了,子类尽量不要覆写。类间依赖的是抽象,覆写了抽象方法,对依赖的稳定性会产生一定的影响)
- 结合里氏替换原则使用
接口隔离原则(Interface Segregation Principle 简称ISP)
定义
客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上
客户端依赖它需要的接口,客户端需要什么接口就提供什么接口,把不需要的接口剔除掉,那就需要对接口进行细化,保证其纯洁性
接口隔离原则与单一职责的审视角度是不相同的,单一职责要求的是类和接口职责单一,注重的是职责,这是业务逻辑上的划分,而接口隔离原则要求接口的方法尽量少,根据接口隔离原则拆分接口时,首先必须满足单一职责原则
接口隔离原则是对接口进行规范约束,其包含以下4层含义:
- 接口要尽量小
- 接口要高内聚
- 定制服务
- 接口设计是有限度的
最佳实践
一个接口只服务于一个模块或业务逻辑;
通过业务逻辑压缩接口中的public方法;
已经被污染的接口,尽量去修改,如风险大,则采用适配器模式转化处理;
了解环境,拒绝盲从。
迪米特法则(Law of Demeter 简称LoD)
也称为最少知识原则(Least Knowledge Principle,LKP)
一个对象应该对其他对象有最少的了解
只和朋友交流,出现在成员变量、方法的输入输出参数中的类称为朋友类,方法体内的不算。类与类之间的关系是建立在类间的,而不是方法间,因此一个方法尽量不引入一个类中不存在的对象。
朋友间也是有距离的,一个类公开的public属性或方法越多,修改时涉及的面也就越大,变更引起的风险扩散也就越大。因此设计时需要反复衡量是否可以减少public方法或属性。
是自己的就是自己的,如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中。
谨慎使用Serializable
开闭原则(Open Closed Principle 简称OCP)
定义
定义:一个软件实体如类、模块、和函数应该对扩展开放,对修改关闭。
前面五个原则就是指导设计的工具和方法,而开闭原则才是其精神领袖。
重要性:
扩展操作,避免修改单元测试,及回归测试;
提高代码复用性,缩小逻辑粒度,直到一个逻辑不可再拆分为止;
提高可维护性;
面向对象开发的要求。
使用
抽象约束
通过接口或抽象类约束扩展,对扩展进行限定,不允许出现在接口或抽象类不存在的public方法;
参数类型、引用对象尽量使用接口或抽象类,而不是实现类;
抽象层尽量保持稳定,一旦确定不允许修改。
元数据控制模块行为,通过扩展一个子类,修改配置文件完成业务变化,如依赖注入;
制定项目章程;
封装变化,找出预计有变化或不稳定的点,为这些变化点创建稳定的接口
将相同变化封装到一个接口或抽象类中;
不应该有两个不同的变化出现在同一个接口或抽象类中