1.起因
在编程过程中对设计模式无比的向往,而六大原则是通往设计模式的成功的基石,必须要掌握,所以我来了。
2.定义
设计模式 六大原则是对编码过程中对以往大佬编写代码的编程思想提炼为开发指导原则。
3.本文目的
本文能带给读者的帮助。
学习六大原则的概述,缺点是无代码示例。
掌握六大原则的作用,在于帮助你在项目中认识和留意哪些代码写的好提供理论依据。
最重要的是 六大原则的应用,我不敢说看了这篇文章你就多牛逼,至少你认识到自己的编程水平及使用六大原则的编程能力。
4.六大原则
4.1单一职责原则(Single Responsibility Principle,简称SRP)
定义:
一个类只负责一个功能领域中的相应职责,
或者可以定义为: 就一个类而言,应该只有一个引起它变化的原因。
场景描述:
类T负责两个不同的职责:职责P1,职责P2。
当由于职责P1需求发生改变而需要修改类T时,
有可能会导致原本运行正常的职责P2功能发生故障。
补充:
1、需要对真实世界中事物的属性进行业务抽离,形成类的属性和方法,且类中只保留与本类有关的元素。
2、单一职责原则是实现高内聚、低耦合的指导方针,
它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,
而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。
好处:
1.可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;
2.提高类的可读性,提高系统的可维护性;
3.变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。
缺点:
设计者需要根据业务对类设计时进行抽离,设计者必须理解业务后进行设计。
发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。
需注意:
单一职责原则提出了一个编写程序的标准,用“职责”和“变化原因”来衡量接口或类设计得是否优良,
但是“职责”和“变化原因”都是不可度量的,因项目和环境而异。
示例:
SRP适用方法
这要举一个例子了,我们做项目的时候会遇到修改用户信息这样的功能模块,
我们一般的想法是将用户的所有数据都接收过来,比如用户名,信息,密码,家庭地址等等,
然后统一封装到一个User对象中提交到数据库,我们一般都是这么干的,
就如下面这样:
其实这样的方法是不可取的,因为职责不明确,方法不明确,
你到底是要修改密码,还是修改用户名,还是修改地址,还是都要修改?
这样职责不明确的话在与其他项目成员沟通的时候会产生很多麻烦,正确的设计如下:
4.2里氏替换原则(Liskov Substitution Principle,简称LSP)
定义:
所有引用基类(父类)的地方必须能透明地使用其子类的对象。
在使用基类对象的地方可以使用任意其子类对象来替换,且不影响程序运行出错。
场景描述:
类使用过程中使用父类实例的地方都可以使用子类实例来替代,且不会造成程序运行出错。
补充:
只要父类能出现的地方子类就能出现,反之,父类未必能胜任。
编译期针对基类检查,在程序运行时再确定具体子类。
好处:
代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
提高代码的重用性;
子类可以形似父类,但又异于父类;提高代码的可扩展性,实现父类的方法就可以“为所欲为”了;
提高产品或项目的开放性。
子类继承父类,加强了类间的内聚程度。
子类重写父类方法,实现子类自身业务从而实现多态(第一种多态实现)。
缺点:
如果有业务需求增加,需要大量新增父类子类,造成类膨胀。
子类扩展新方法后父类实例无法替换子类对象。
继承是侵入性的。 只要继承,就必须拥有父类的所有属性和方法;
降低代码的灵活性。 子类必须拥有父类的属性和方法;
增强了耦合性。 当父类的常量、变量和方法被修改时,必需要考虑子类的修改,
而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果;大片的代码需要重构。
需注意:
原则实现前提必须满足单一职责原则来设计抽象接口。
如果子类不能完整地实现父类方法,或父类在某些方法在子类中已经发生“畸变”,
则建议断开父子继承关系 采用依赖、聚合、组合等关系代替继承。
示例:
系统需要提供一个发送Email的功能,客户(Customer)可以分为VIP客户(VIPCustomer)和普通客户(CommonCustomer)两类,
原始设计方案如图所示:
在本实例中,可以考虑增加一个新的抽象客户类Customer,
而将CommonCustomer和VIPCustomer类作为其子类,
邮件发送类EmailSender类针对抽象客户类Customer编程,
根据里氏代换原则,能够接受基类对象的地方必然能够接受子类对象,
因此将EmailSender中的send()方法的参数类型改为Customer,
如果需要增加新类型的客户,只需将其作为Customer类的子类即可。
重构后的结构如图所示:
4.3依赖倒置原则(Dependence Inversion Principle,简称DIP)
定义:
高层模块不应该依赖低层模块, 两者都应该依赖其抽象;
抽象不应该依赖细节,细节应该依赖抽象,
其核心思想是:要面向接口编程,不要面向实现编程。
场景描述:
依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,
尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,
以及数据类型的转换等,而不要用具体类来做这些事情。
为了确保该原则的应用,一个具体类应当只实现接口或抽象类中声明过的方法,
而不要给出多余的方法,否则将无法调用到在子类中增加的新方法。
补充:
对有共性的实例行为进行抽象为方法,并在子类或实现类中继承或重写方法(第二种多态实现方法)。
高层模块调用底层模块程序时,使用抽象类型来定义变量类型来装具体实现的抽象类实例。
高层模块就是调用端,底层模块就是具体实现类。
抽象就是指接口或抽象类。
细节就是实现类。
好处:
*** 使用抽象思维可以降低类间的解耦合。
依赖倒置的好处在小型项目中很难体现出来。
在大型项目中可以减少需求变化引起的工作量。
使并行开发可依赖使用底层共用代码,提高开发效率。
缺点:
接口抽象设计门槛高,刚入门的开发者使用容易,设计难度大。
使用场景往往设计到系统架构设计,普通开发者往往察觉不到。
需注意:
原则实现前提必须满足单一职责原则来设计抽象接口。
实际使用序列化过程中,Serializable接口是标记接口无方法定义,但是在序列化与反序列化对象时对象所属类必须实现此接口。
使用clone过程中,Cloneable接口也是标记接口无方法定义,但clone具体实现类需要重写Object的clone方法,返回类实例。
示例:
现需要将存储在TXT或Excel文件中的客户信息转存到数据库中,因此需要进行数据格式转换。
在客户数据操作类中将调用数据格式转换类的方法实现格式转换和数据库插入操作,
初始设计方案结构如图所示:
在本实例中,由于CustomerDAO针对具体数据转换类编程,
因此在增加新的数据转换类或者更换数据转换类时都不得不修改CustomerDAO的源代码。
我们可以通过引入抽象数据转换类解决该问题,在引入抽象数据转换类DataConvertor之后,
CustomerDAO针对抽象类DataConvertor编程,
而将具体数据转换类名存储在配置文件中,符合依赖倒转原则。
根据里氏代换原则,程序运行时,
具体数据转换类对象将替换DataConvertor类型的对象,程序不会出现任何问题。
更换具体数据转换类时无须修改源代码,只需要修改配置文件;
如果需要增加新的具体数据转换类,
只要将新增数据转换类作为DataConvertor的子类并修改配置文件即可,
原有代码无须做任何修改,满足开闭原则。
重构后的结构如图所示:
4.4接口隔离原则(Interface Segregation Principle,简称ISP)
定义:
使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
类间的依赖关系应该建立在最小的接口上。
场景描述:
根据接口隔离原则,当一个接口太大时,
我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。
每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干。
补充:
需在程序设计时对业务整体把控,对业务细分过程中形成接口,接口间关联及接口边界(接口方法)。
建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。
为各个类建立专用的接口,不要试图去建立庞大的接口提供所有依赖它的类。
好处:
接口中保留必要的方法,简化接口设计,减少实现具体类重写方法。
降低类间耦合度,提升模块程序扩展性、稳定性。
为依赖接口的类定制服务。
缺点:
设计此原则时把握简化接口中方法难度大。
普通开发者达不到此境界,有设计在项目应用机会少。
需注意:
1、基于单一职责原则,依赖倒置原则来设计接口隔离原则。
2、接口尽量小,但是要有限度。
对接口进行细化可以提高程序设计灵活性,但是如果过小,
则会造成接口数量过多,使设计复杂化。所以一定要适度。
3、提高内聚,减少对外交互。
使接口用最少的方法去完成最多的事情。
4、为依赖接口的类定制服务。
只暴露给调用的类需要的方法,不需要的方法则隐藏起来。
只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
示例:
Sunny软件公司开发人员针对某CRM系统的客户数据显示模块设计了如下图所示接口,
其中方法dataRead()用于从文件中读取数据,
方法transformToXML()用于将数据转换成XML格式,
方法createChart()用于创建图表,
方法displayChart()用于显示图表,
方法createReport()用于创建文字报表,
方法displayReport()用于显示文字报表。
需要将该接口按照接口隔离原则和单一职责原则进行重构,将其中的一些方法封装在不同的小接口中,
确保每一个接口使用起来都较为方便,并都承担某一单一角色,每个接口中只包含一个客户端(如模块或类)所需的方法即可,
重构后的示图,如下所示:
4.5迪米特法则(Law Of Demeter,简称LOD)
定义:
一个软件实体应当尽可能少地与其他实体发生相互作用。
类间解耦。
场景描述:
应该尽量减少对象之间的交互,
如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,
如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。
简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。
补充:
一个类对自己依赖的类知道的越少越好。
自从我们接触编程开始,软件编程总则:高内聚、低耦合。
无论面向过程编程还是面向对象编程,使各个模块之间的耦合尽量低,才能提高代码复用率。
优点:
类被引用的越少,实现低耦合度代码,降低程序架构复杂度,提升程序的稳定性。
使用继承实现高内聚,提高代码的复用。
尽量使用组合或聚合替代继承。
使用接口替代继承。
缺点:
需要熟悉类间UML的知识。
需要开发者尽量使用组合、聚合、实现、继承来使用编程。
需注意:
对技术要求高,需要要求设计者遵循此原则,建议适量新增开发时间。
示图:
Sunny软件公司所开发CRM系统包含很多业务操作窗口,
在这些窗口中,某些界面控件之间存在复杂的交互关系,一个控件事件的触发将导致多个其他界面控件产生响应,
例如,当一个按钮(Button)被单击时,
对应的列表框(List)、组合框(ComboBox)、
文本框(TextBox)、文本标签(Label)等都将发生改变,
在初始设计方案中,界面控件之间的交互关系可简化为如图所示结构
在本实例中,可以通过引入一个专门用于控制界面控件交互的中间类(Mediator)来降低界面控件之间的耦合度。
引入中间类之后,界面控件之间不再发生直接引用,
而是将请求先转发给中间类,再由中间类来完成对其他控件的调用。
当需要增加或删除新的控件时,只需修改中间类即可,无须修改新增控件或已有控件的源代码,
重构后结构如图所示:
4.6开放封闭原则
定义:
一个软件实体应当对扩展开放,对修改关闭。
即软件实体应尽量在不修改原有代码的情况下进行扩展。
尽量通过扩展软件实体来解决需求变化,而不是通过已有的代码来完成变化。
场景描述:
任何软件都需要面临一个很重要的问题,即它们的需求会随时间的推移而发生变化。
因为变化,升级和维护等原因,如果需要对软件原有代码进行修改,可能会给旧代码引入错误,
也有可能会使我们不得不对整个功能进行重构,
并且需要原有代码经过重新测试,
所以当软件需要变化时,
尽量通过扩展软件实体的行为来实现变化,
而不是通过修改已有的代码来实现使我们需要的。
补充:
项目中业务是变化最大的,技术变化是随着业务变化而变化。
对原有业务有大的业务变动,根据风险评价来判定,使用里氏替换或依赖倒置通过继承或实现具体业务实现,比较稳妥。
优点:
1.开闭原则非常有名,只要是面向对象编程,在开发时都会强调开闭原则。
2.开闭原则是最基础的设计原则,其它的五个设计原则都是开闭原则的具体形态,
也就是说其它的五个设计原则是指导设计的工具和方法,而开闭原则才是其精神领袖。
依照Java语言的称谓,开闭原则是抽象类,而其它的五个原则是具体的实现类。
3.开闭原则可以提高复用性。
4.开闭原则可以提高维护性。
缺点:
修改关闭 适用范围在技术框架、项目公用帮助类 除非修复bug,一般不要轻易去修改业务。
对设计者提出非常高的要求。
需注意:
基于单一职责原则。
切记此原则的适用范围。
第一:抽象约束 定义类及继承、依赖。
第二:元数据(metadata)控件模块行为。 适用注解校验参数
第三:制定项目章程。 开发都遵守,约定大于配置。
第四:封装变化。 确保相同的变化,保留在同一个接口中,如果不是则拆分接口。
示例:
实现画图表的功能,如饼状图和柱状图等,为了支持多种图表显示方式,原始设计方案如图下图所示:
在该代码中,如果需要增加一个新的图表类,如折线图LineChart,
则需要修改ChartDisplay类的display()方法的源代码,增加新的判断逻辑,违反了开闭原则。
现对该系统进行重构,使之符合开闭原则。
1.增加一个抽象图表类AbstractChart,将各种具体图表类作为其子类;
2.ChartDisplay类针对抽象图表类进行编程,由客户端来决定使用哪种具体图表。
重构后结构如图2所示:
4.7总结
单一职责原则告诉我们实现类要职责单一;
里氏替换原则告诉我们不要破坏继承体系;
依赖倒置原则告诉我们要面向接口编程;
接口隔离原则告诉我们在设计接口的时候要精简单一;
迪米特法则告诉我们要降低耦合。
而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。
最后总结一下如何去遵守这六个原则。
对这六个原则的遵守并不是是和否的问题,而是多和少的问题,也就是说,
我们一般不会说有没有遵守,而是说遵守程度的多少。
任何事都是过犹不及,设计模式的六个设计原则也是一样,
制定这六个原则的目的并不是要我们刻板的遵守他们,而需要根据实际情况灵活运用。
对他们的遵守程度只要在一个合理的范围内,就算是良好的设计。
我们用一幅图来说明一下。
4.8 参考博文
6大原则:
https://zhuanlan.zhihu.com/p/33607390
6大原则的实现地址: