读书笔记——设计模式常用的七大原则
参考《设计模式之禅(第二版)》
单一职责原则
SRP(Single Responsibility Principle)原则
基本介绍
定义:There should never be more than one reason for a class to change,即应该有且仅有一个原因引起类的变更
对类来说,一个类应该只负责一项职责。
如类A负责两个不同职责——职责1、职责2,因职责1需求变更而改变类A,可能造成职责2执行错误,所以应当将类A拆解成类A1、A2。
单一职责原则注意事项和细节
- 降低类的复杂的,一个类只负责一项职责
- 提高类的可读性、可维护性
- 降低变更引起的风险
- 通常情况下,编写类应当遵守单一职责原则
- 只有当逻辑足够简单时,才可以在代码级别违反单一职责原则,如只有在类中方法较少时只在方法级别维持单一职责原则
案例
用户信息维护类的设计。
先看下面初步设计的类图
很明显,这个接口设计得有问题,用户的属性和用户的行为没有分开
将上述接口进行分解
- 用户的信息抽取成一个
BO
(Business Object,业务对象) - 行为抽取成一个
Biz
(Business Logic,业务逻辑)
分解后的类图如下
分清楚职责后的代码案例如下
1 | IUserInfo userInfo = new UserInfo(); |
在实际的使用中,更倾向于使用两个不同的类或接口:一个是IUserBO
,一个是IUserBiz
,如下图所示
以上便满足了单一职责原则
分析
使用单一职责原则的好处
- 类的复杂性降低,实现什么职责都有清晰明确的定义
- 可读性提高,复杂性降低,那当然可读性提高了
- 可维护性提高,可读性提高,那当然更容易维护了
- 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助
注意事项
注:单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或 类设计得是否优良,但是“职责”和“变化原因”都是不可度量的,因项目而异,因环境而异
对于接口,在设计的时候一定要做到单一,但是对于实现类就需要多方面考虑了
生搬硬套单一职责原则会引起类的剧增,给维护带来非常多的麻烦,而且过分细分类的职责也会人为地增加系统的复杂性,如本来一个类可以实现的行为硬要拆成两个类,然后再使用聚合或组合的方式耦合在一起,人为制造了系统的复杂性。
总结
对于单一职责原则,我的建议是接口一定要做到单一职责,类的设计尽量做到只有一个 原因引起变化
接口隔离原则
Interface Segregation Principle
基本介绍
定义:
- Clients should not be forced to depend upon interfaces that they don’t use,即客户端不应该依赖它不需要的接口
- The dependency of one class to another one should depend on the smallest possible interface,即类间的依赖关系应该建立在最小的接口上
案例
情景:现有接口InterfaceDemo
,包含one(), two(), three(), four(), five()
五个方法,类B
和D
实现该接口;类A
通过接口依赖于类B
,但只使用one(), two(), three()
三个方法,而类C
通过接口依赖于类D
,但只使用one(), four(), five()
三个方法。
具体实现如下:
1 | // 接口 |
问题:违反接口隔离原则,接口InterfaceDemo
对于类A
和类C
来说不是最小接口, 那么类B
和类D
必须去实现他们不需要的方法。
解决方法:将接口InterfaceDemo
拆分为独立的几个接口, 类A
和类C
分别与它们需要的接口建立依赖关系,也就是采用接口隔离原则。
具体实现:将InterfaceDemo
拆分为独立的三个接口InterfaceDemo1, InterfaceDemo2, InterfaceDemo3
,类B
和D
实现相应接口,具体代码如下。
1 | // 接口,将原先违反原则的接口拆分成三个接口 |
依赖倒转原则
Dependence Inversion Principle
基本介绍
高层模块不应该依赖于低层模块,二者都应该依赖其抽象
抽象不应该依赖细节,细节应该依赖抽象
依赖倒转(倒置)的中心思想是面向接口编程
依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定得多,所以以抽象为基础搭建的架构比以细节为基础搭建的架构要稳定的多
注:抽象指的是接口或抽象类,细节就是具体的实现类
使用接口或抽象类的目的是制定好规范,而不涉及具体的操作,把展示细节的任务交给他们的实现类完成
分析:每一个逻辑的实现都是由原子逻辑组成的,不可分割的原子逻辑就是低层模块,原子逻辑的再组装就是高层模块。
案例
情景:Person
接收消息的功能
初步实现如下:
1 | public class DependenceInversion { |
问题:违反依赖倒转原则,若加入消息有多种种类,如邮件、微信、短信等,此时要追加接收微信功能时Person
要追加接收方法
解决方法:设定一个抽象类,让邮件、微信、消息等实现该接口,然后使得Person类依赖于该接口即可解决问题
具提实现如下:
1 | public class DependenceInversion { |
依赖倒转原则的注意事项和细节
- 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好
- 变量的声明类型尽量是抽象类或接口, 这样我们的变量引用和实际对象间,就存在 一个缓冲层,利于程序扩展和优化
- 继承时遵循里氏替换原则
扩展
依赖关系传递的三种方式:
- 接口传递,即上述接收消息功能的实现
- 构造方法传递,在创建对象时注入依赖
Setter
方法传入,创建完对象后通过属性对应的Setter
方法实现注入
里氏替换原则
Liskov Substitution Principle
继承复用的基石,只有当基类随便怎么改动子类都不受此影响,那么基类才能真正被复用
使用继承的有点与缺点
- 优点:
- 提高代码的重用性,代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性
- 提高代码的可扩展性,子类可形似于父类,但异于父类,保留自我的特性
- 提高产品或项目的开放性
- 缺点:侵入性、不够灵活、高耦合
- 继承是具有侵入性的,只要继承就必须拥有父类的所有方法和属性,在一定程度上约束了子类,降低了代码的灵活性
- 增加了耦合,当父类的常量、变量或者方法被修改了,需要考虑子类的修改,所以一旦父类有了变动,很可能需要重构大量的代码
问题提出:在编程中,如何正确的使用继承? 遵循里氏替换原则。
基本介绍
里氏替换原则(LSP原则,Liskov Substitution Principle)于在1988年,由麻省理工学院的以为姓里的女士提出的
定义:如果对每个类型为T1的对象O1,都有类型为T2的对象O2,使得以T1定义的所有程序P在所有的对象O1都代换成O2时,程序P的行为没有发生变化,那么类型T2是类型T1 的子类型。换句话说,所有引用基类的地方必须能透明地使用其子类的对象。
遵循里氏替换原则
- 在使用继承时,子类必须实现父类的抽象方法,但不得重写父类的非抽象方法,即子类中尽量不要重写父类的方法
- 继承实际上增强了两个类耦合性,在适当的情况下,可以通过聚合,组合,依赖来解决问题
理解:只要父类能出现的地方,子类就可以出现,并且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但反之未要求。
案例
大家都打过CS吧,非常经典的FPS类游戏,接下来描述一下里面用到的枪
枪的主要职责是射击,如何射击在各个具体的子类中定义,如手枪是单发射程比较近,步枪威力大射程远等
在士兵类中定义了一个方法killEnemy
,使用枪来杀敌人, 具体使用什么枪来杀敌人,调用的时候才知道
1 | // 枪的抽象类 |
如果增加一种类型的枪——玩具枪,该如何定义呢?
ToyGun
的实现代码如下
1 | public class ToyGun extends AbstractGun { |
很明显,此时使用ToyGun
以及不能正确的完成功能了(因为无法射击,无法killEnemy
),
这是上述的设计就已经违反了LSP原则了,在类中调用其他类时务必要使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了LSP原则
解决方法:如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。
ToyGun
脱离继承,建立一个独立的父类,为了实现代码复用,可以与AbastractGun
建立关联委托关系
这样便解决了问题
开闭原则
Open Closed Principle,即OCP原则
基本介绍
- 编程中最基础、最重要的设计原则
- 要求一个软件实体如类,对模块和函数应该扩展开放(对提供方),对修改关闭(对使用方)。用抽象构建框架,用实现扩展细节
- 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化
- 编程中遵循其它原则,以及使用设计模式的目的就是遵循开闭原则
案例
情景:使用GraphicEditor
类来绘制图形,根据注入drawShap()
方法的具体图形来绘制具体图形。
调试程序如下:
1 | public class OpenClosed { |
其中解决方法一实现如下:
1 | class GraphicEditor { |
在一个情景下,要增加对三角形绘制功能的支持,在解决方法一的设计情况下,要改动的部分较大,而且要求改使用方(GraphicEditor
),违反了开闭原则,对扩展开放(提供方),对修改关闭(使用方),明显可扩展性不好。
解决方法:将具体图形的绘制方法的定义放入图形基类Shape()
中,具体的绘制方法交由子类实现,最终注入GraphicEditor
即可,具体实现如下:
1 | class GraphicEditor { |
这样在不修改使用方的情况下就可以较为轻易的增加/扩展功能。
迪米特法则
Demeter Principle,最少知识原则,目的在于降低类之间的耦合
基本介绍
一个对象应该对其他对象保持最少的了解
类与类关系越密切,耦合度越大
一个类对自己依赖的类知道的越少越好
对于被依赖的类不管内部逻辑多么复杂,都尽量将逻辑封装在类的内部,只对外提供
public
方法,不对外泄露任何其它信息迪米特法则还有个更简单的定义:只与直接的朋友通信
直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合(依赖,关联,组合,聚合等)关系,就称这两个对象之间是朋友关系
其中,称类中出现的成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友
也就是说,陌生的类最好不要以局部变量的形式出现在类的内部
目标
迪米特法则的目的在于降低类之间的耦合
由于每个类尽量减少对其他类的依赖,因此该法则很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。
迪米特法则不希望类之间建立直接的联系。如果真的有需要建立联系,也希望能通过它的友元类来转达。
缺点
应用迪米特法则有可能造成的后果——系统中存在大量的中介类,这些类的存在完全是为了传递类之间的相互调用关系——这在一定程度上增加了系统的复杂度
案例
和陌生人交流
情景:甲和朋友认识,朋友和陌生人认识,而甲和陌生人不认识,甲可以直接和朋友交流,朋友可以直接和陌生人交流,甲必须通过朋友和陌生人交流
很容易想到的一种实现方式代码如下
1 | class Jia { |
分析:这样子看上去是完成了情景中的要求,但是Jia
类的chatToStranger()
方法包含陌生的类Stranger
的局部变量,所以,不符合迪米特法则
一种更好的实现是将陌生人Stranger
变为Friend
的依赖,然后直接通过Friend
与陌生人交流即可,具体如下
1 | class Jia { |
迪米特法则注意事项和细节
- 迪米特法则的核心是降低类之间的耦合
- 但是注意:由于每个类都减少了不必要的依赖,从而降低类间的耦合关系, 而并不是要求完全没有依赖关系
合成复用原则
Composite Reuse Principle
基本介绍
原则是尽量使用合成/聚合的方式,而不是使用继承
设计原则核心思想
- 找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起
- 针对接口编程,而不是针对实现编程
- 为了交互对象之间的松耦合设计而努力