设计模式的七大基本原则
1 基本介绍
设计原则名称 | 定义 | 使用频率 |
---|---|---|
单一职责原则 | 一个类只负责一个功能领域中的相应职责 | ✨✨✨✨ |
开闭原则 | 软件实体应对外扩展开放,而对修改关闭 | ✨✨✨✨✨ |
里氏代换原则 | 所有引用基类对象的地方都能够透明地使用其子类的对象 | ✨✨✨✨✨ |
依赖倒转原则 | 抽象不应该依赖于细节,细节应该依赖于抽象 | ✨✨✨✨✨ |
接口隔离原则 | 使用多个专门的接口,而不使用单一的总接口 | ✨✨ |
合成复用原则 | 尽量使用对象组合,而不是继承来达到复用的目的 | ✨✨✨✨ |
迪米特法则 | 一个软件实体应当尽可能少地与其他实体发生相互作用 | ✨✨✨ |
2 单一职责原则
从字面意思来看,就是一个类应该做到“专注”,如果一个类承担的职责越多,可复用性将会越小。
比如说,见下图
该类承担了,getConnection()
方法用于连接数据库, findCustomers()
用于查询所有的客户信息,createChart()
用于创建图表,displayChart()
用于显示图表。很明显,这样会导致耦合度过高。
改进后的图是这样的:
- DBUtil:负责连接数据库
- CustomerDAO:负责操作数据库中的Customer表
- CustomerDataChart:负责图表的生成和显示
3 开闭原则
这个原则简单来说就是:软件实体应该尽量在不修改原有代码的基础上进行扩展
软件实体可以是一个模块,一个由多个类组成的局部类或一个独立的类
该原则的好处在于:使得软件拥有更好的稳定性和延续性
举个栗子:
一个系统需要展示不同的图表,它是按如下设计的
1 |
|
可见上述代码和设计,如果后续要添加折线图,则还要新增判断逻辑。
修改如下:
添加一个抽象图表类AbstractChart
,当要显示某种图表时,则注入对应的对象即可。
4 里氏替换原则
该原则是指:在软件中将一个基类对象替换成它的子类对象,程序将不会产生任 何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。
这样说感觉有点抽象,举个栗子:我喜欢动物,那我一定喜欢狗,因为狗是动物的子类;但是我喜欢狗,不能据此断定我喜欢动物,因为我并不喜欢老鼠,虽然它也是动物
里氏替换原则是实现开闭原则的重要方式,因为一般代码都是使用基类类型来对对象定义,在运行时确定其子类类型,用子类对象来替换父类对象。
该原则需要注意以下问题:
- 子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法
- 尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现
举个发邮件的栗子
可以发现,无论是普通用户还是VIP,其实发邮件都是相同的,也就是这里会出现代码重复,为了减少代码重复,可以用改用则进行重构
重构后,增加一个抽象类
5 依赖倒转原则
依赖倒转原则是指:抽象不应该依赖细节,细节应该依赖于抽象。说人话就是,要面向接口/抽象类编程,而不是面向实现编程
这也就要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类。
因为使用抽象层进行编程,可以提高系统的灵活性,对抽象层进行扩展,并修改 配置文件,而无须修改原有系统的源代码,在不修改的情况下来扩展系统的功能,满足开闭原则的要求。
常用的注入方式有三种:构造注入,设值注入,接口注入。
举个栗子来理解下
将存储在TXT或EXCEL文件中的客户信息存到数据库中,需要进行格式转换。
但是这有个问题,每次更改数据转化类,都需要修改CustomerDAO的源代码,系统扩展性较差。
重构方式如下:
引入抽象数据转换类DataConvertor
,CustomerDAO
针对抽象类DataConvertor
编程,而将具体数据转换类名存储在配置文件中,符合依赖倒转原则。
根据里氏代换原则,程序运行时,具体数据转换类对象将替换DataConvertor类型的对象,程序不会 出现任何问题。更换具体数据转换类时无须修改源代码,只需要修改配置文件;如果需要增 加新的具体数据转换类,只要将新增数据转换类作为DataConvertor的子类并修改配置文件即 可,原有代码无须做任何修改,满足开闭原则。
可以发现,3,4,5三种原则是密切联系的,开闭原则是目标,里氏替换原则是基础,依赖倒转原则是手段。只是分析的角度不同而已。
6 接口隔离原则
该原则是指:使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
简单来说,就是接口应该尽量细化,同时接口中的方法应该尽量少,每个接口只包含一个客户端所需的方法即可。
举个数据显示的栗子:
具体意义就不解释了,函数名很清晰
可以发现,如果需要创建和显示图表,除了需实现与图表相关的方法外,还需要实现创建和显示文字报表的方法,否则程序编译时将报错等。。
重构后
重构后的接口粒度更细了,但要注意的是接口不能太小,如果太小会导致系 统中接口泛滥,不利于维护;接口也不能太大,太大的接口将违背接口隔离原则,灵活性较差,使用起来很不方便。
7 合成复用原则
该原则的含义是:尽量使用对象组合,而不是继承来达到复用的目的。
简言之:复用时要尽量使用组合/聚合关系(关联关系),少用继承。
在设计时:
- 使用组合/聚合可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少
- 其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则
继承的“危害”
通过继承来进行复用的主要问题在于继承复用会破坏系统的封装性,因为继承会将基类的实 现细节暴露给子类,由于基类的内部细节通常对子类来说是可见的,所以这种复用又称“白箱”复用,如果基类发生改变,那么子类的实现也不得不发生改变;从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性;而且继承只能在有限的环境中使用 (如类没有声明为不能被继承)。
一般来说,如果两个类之间是“Has-A”
的关系应使用组合或聚合,如果是“Is-A”
关系可使用继承。"Is-A"
是严格的分类学意义上的定义,意思是一个类是另一个类的”一种”;而"Has-A"
则不同,它表示某一个角色具有某一项责任。
再来看一个连接数据库栗子
该栗子是指,通过继承工具类的方式来连接MYSQL数据库,而如果系统决定升级为Oracle数据库,因此需要增加一个新的OracleDBUtil类来连接Oracle数据库,由于在初始设计方案中CustomerDAO和DBUtil之间是继承关系,因此在更换数据库连接方式时需要修改CustomerDAO类的源代码,将CustomerDAO作为OracleDBUtil的子类,这将违反开闭原则。
重构后
CustomerDAO
和DBUtil
之间的关系由继承关系变为关联关系。由于CustomerDAO
针对DBUtil
编程,根据里氏代换原则,DBUtil
子类的对象可以覆盖
DBUtil
对象,只需在CustomerDAO
中注入子类对象即可使用子类所扩展的方法。
8 迪米特原则
该原则是指:一个软件实体应当尽可能少地与其他实体发生相互作用。
迪米特法则还有几种定义形式,包括:不要和“陌生人”说话、只与你的直接朋友通信等,在迪米特法则中,对于一个对象,其朋友包括以下几类:
- 当前对象本身
- 以参数形式传入到当前对象方法中的对象
- 当前对象的成员对象
- 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友
- 当前对象所创建的对象
在将迪米特法则运用到系统设计中时,要注意下面的几点:
- 在类的划分上,应当尽量创建松 耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及;
- 在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限;
- 在类的设计上,只要有可能,一个类型应当设计成不变类;
- 在对其他类的引用上,一个对象对其他对象的引用应当降到最低。
举个栗子
一个业务窗口中有很多交互关系,可以发现这些关系十分复杂,导致在该窗口中增加新的界面控件时需要修改与之交互的其他控件的源代码,系统扩展性较差,也不便于增加和删除新控件。
重构后
引入一个专门用于控制界面控件交互的中间类(Mediator)来降低界面控件之间的耦合度。引入中间类之后,界面控件之间不再发生直接引用,而是将请求先转发给中间类,再由中间类来完成对其他控件的调用。当需要增加或删除新的控件时,只需修改中间类即可,无须修改新增控件或已有控件的源代码。