常用的设计模式
为什么要使用设计模式来编写代码
设计模式是一种被反复使用的代码设计经验的总结,合理的使用设计模式,在项目开发时可以给我们提供良好的清晰的项目结构,帮助开发人员更快的理解代码,也可以减少系统的耦合性,是项目可以更好的维护和扩展。在项目运行时可以使我们开发的系统能更高效的运行,合理的分配使用资源,减少内存使用。学习设计模式将有助于初学者更加深入地理解面向对象思想,无论是前端还是后端开发人员都应该熟悉这些设计模式并应用到项目的开发中。
了解设计模式前需要了解的一些概念
一、六大原则
1、开闭原则
一个软件实体应当对扩展开放,对修改关闭。开发人员尽量在不修改原有代码的情况下进行扩展,当然这也不是必须的。为了满足开闭原则,应该适当对系统进行抽象设计。当你需要进行代码的拓展和修改时,很大程度上你只需要修改你的实现来应对新的业务和需求。实现在不修改已有代码的基础上扩展系统的功能,达到开闭原则的要求。
优点:实践开闭原则的优点在于可以在不改动原有代码的前提下给程序扩展功能。增加了程序的可扩展性,同时也降低了程序的维护成本。
2、里氏替换原则
基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,打个比方,如果你需要锯断一个木头,为了达到这目的你可以使用手工锯,但你也可以用在手工据基础上发明的电锯来锯断一个木头,但反过来,如果你想不费劲的锯断一个木头,你不能用手工锯来替代电锯。里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
优点:可以检验继承使用的正确性,约束继承在使用上的泛滥。
3、依赖倒置原则
抽象不应该依赖于具体类,具体类应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。还有很重要的一点是:高层模块不应该依赖低层模块,两者都应该依赖其抽象。很多时候我们往往只考虑到上层结构的抽象,这样子底层的对象的修改对上层结构的影响会变大,考虑这样一个情况,比如一个饮料工厂,刚开始只被要求加工可口可乐,它基于可口可乐的流程,来实现自己的流水线,但如果有一天工厂需要加工芬达呢,那他是不是要对流水线进行大幅调整,但如果工厂在开始设计的时候考虑到可乐,芬达什么的都是饮料,并在这个概念上经行工厂流水线的设计,那么这个工厂就可以低成本的切换加工各种饮料了。依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。
优点:通过抽象来搭建框架,建立类和类的关联,以减少类间的耦合性。而且以抽象搭建的系统要比以具体实现搭建的系统更加稳定,扩展性更高,同时也便于维护。
4、单一职责原则
一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。单一职责原则告诉我们:一个类不能太“累”!在软件系统中,一个类(大到模块,小到方法)承担的职责越多,它被复用的可能性就越小。单一职责原则是实现高内聚、低耦合的指导方针,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。
优点:如果类与方法的职责划分得很清晰,不但可以提高代码的可读性,更实际性地更降低了程序出错的风险,因为清晰的代码会让 bug 无处藏身,也有利于 bug 的追踪,也就是降低了程序的维护成本
5、 迪米特法则(最少知道原则)
迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限。在类的设计上,只要有可能,一个类型应当设计成不变类。在对其他类的引用上,一个对象对其他对象的引用应当降到最低。
优点:实践迪米特法则可以良好地降低类与类之间的耦合,减少类与类之间的关联程度,让类与类之间的协作更加直接
6、接口分离原则
使用多个专门的接口,而不使用单一的总接口,当一个接口太大时,我们需要将它分割成一些更细小的接口。在使用接口隔离原则时,我们需要注意控制接口的粒度,接口不能太小,如果太小会导致系统中接口泛滥,不利于维护。接口也不能太大,太大的接口将违背接口隔离原则,灵活性较差,使用起来很不方便。
优点:避免同一个接口里面包含不同类职责的方法,接口责任划分更加明确,符合高内聚低耦合的思想。
7、六大原则之外(合成复用原则)
尽量使用对象组合,而不是继承来达到复用的目的。 在面向对象设计中,可以通过两种方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承,但首先应该考虑使用组合/聚合,组合/聚合可以使系统更加灵活,降低类与类之间的耦合度。一个类的变化对其他类造成的影响相对较少,其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
二、高内聚低耦合
高内聚低耦合,是软件工程中的概念,是判断软件设计好坏的标准,主要用于程序的面向对象的设计,主要看类的内聚性是否高,耦合度是否低。通常程序结构中各模块的内聚程度越高,模块间的耦合程度就越低。内聚是从功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事,它描述的是模块内的功能联系;耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及通过接口的数据。
1、耦合性
耦合性:也称块间联系。指软件系统结构中各模块间相互联系紧密程度的一种度量。模块之间联系越紧密,其耦合性就越强,模块的独立性则越差。模块间耦合高低取决于模块间接口的复杂性、调用的方式及传递的信息。
耦合可以分为以下几种,它们之间的耦合度由高到低排列如下:
1、内容耦合:一个模块直接访问另一模块的内容,则称这两个模块为内容耦合。若在程序中出现下列情况之一,则说明两个模块之间发生了内容耦合:
1). 一个模块直接访问另一个模块的内部数据。
2). 一个模块不通过正常入口而直接转入到另一个模块的内部。
3). 两个模块有一部分代码重叠(该部分代码具有一定的独立功能)。
4). 一个模块有多个入口。
内容耦合可能在汇编语言中出现。大多数高级语言都已设计成不允许出现内容耦合。这种耦合的耦合性最强,模块独立性最弱。
2、 公共耦合: 一组模块都访问同一个全局数据结构,则称之为公共耦合。 公共数据环境可以是全局数据结构、共享的通信区、内存的公共覆盖区等。如果模块只是向公共数据环境输入数据,或是只从公共数据环境取出数据,这属于比较松散的公共耦合;如果模块既向公共数据环境输入数据又从公共数据环境取出数据,这属于较紧密的公共耦合。 公共耦合会引起以下问题:
1).无法控制各个模块对公共数据的存取,严重影响了软件模块的可靠性和适应性。
2). 使软件的可维护性变差。若一个模块修改了公共数据,则会影响相关模块。
3). 降低了软件的可理解性。不容易清楚知道哪些数据被哪些模块所共享,排错困难。 一般地,仅当模块间共享的数据很多且通过参数传递很不方便时,才使用公共耦合。
3、 外部耦合: 一组模块都访问同一全局简单变量,而且不通过参数表传递该全局变量的信息,则称之为外部耦合。
4、 控制耦合: 模块之间传递的不是数据信息,而是控制信息例如标志、开关量等,一个模块控制了另一个模块的功能
5、标记耦合: 调用模块和被调用模块之间传递数据结构而不是简单数据,同时也称作特征耦合。标记耦合的模块间传递的不是简单变量,而是像高级语言中的数据名、记录名和文件名等数据结果,这些名字即为标记,其实传递的是地址。
6、 数据耦合: 调用模块和被调用模块之间只传递简单的数据项参数。相当于高级语言中的值传递。
7、 非直接耦合: 两个模块之系,它们之间的联系完全是通过主模块的控制和调用来实现的。耦合度最弱,模块独立性最强。
总结:耦合是影响软件复杂程度和设计质量的一个重要因素,为提高模块的独立性,应建立模块间尽可能松散的系统,在设计上我们应采用以下原则:若模块间必须存在耦合,应尽量使用数据耦合,少用控制耦合,慎用或有控制地使用公共耦合,并限制公共耦合的范围,尽量避免内容耦合。
2、内聚性
内聚性:又称块内联系。指模块的功能强度的度量,即一个模块内部各个元素彼此结合的紧密程度的度量。若一个模块内各元素(语名之间、程序段之间)联系的越紧密,则它的内聚性就越高。 所谓高内聚是指一个软件模块是由相关性很强的代码组成,只负责一项任务,也就是常说的单一责任原则。
内聚有如下的种类,它们之间的内聚度由弱到强排列如下:
1、偶然内聚:一个模块内的各处理元素之间没有任何联系,只是偶然地被凑到一起。这种模块也称为巧合内聚,内聚程度最低。
2、逻辑内聚:这种模块把几种相关的功能组合在一起, 每次被调用时,由传送给模块参数来确定该模块应完成哪一种功能 。
3、 时间内聚:把需要同时执行的动作组合在一起形成的模块称为时间内聚模块。
4、 过程内聚:构件或者操作的组合方式是,允许在调用前面的构件或操作之后,马上调用后面的构件或操作,即使两者之间没有数据进行传递。简单的说就是如果一个模块内的处理元素是相关的,而且必须以特定次序执行则称为过程内聚。
5、 通信内聚:指模块内所有处理元素都在同一个数据结构上操作或所有处理功能都通过公用数据而发生关联(有时称之为信息内聚)。即指模块内各个组成部分都使用相同的数据或产生相同的数据结构。
6、 顺序内聚:一个模块中各个处理元素和同一个功能密切相关,而且这些处理必须顺序执行,通常前一个处理元素的输出是后一个处理元素的输入。例如某模块完成工业产值求值的功能,前一个功能元素求总产值,后一个功能元素求平均产值,显然该模块内两部分紧密关联。顺序内聚的内聚度比较高,但缺点是不如功能内聚易于维护。
7、 功能内聚:模块内所有元素的各个组成部分全部都为完成同一个功能而存在,共同完成一个单一的功能,模块已不可再分。即模块仅包括为完成某个功能所必须的所有成分,这些成分紧密联系、缺一不可。 功能内聚是最强的内聚,其优点是它的功能明确。判断一个模块是否功能内聚,一般从模块名称就能看出。如果模块名称只有一个动词和一个特定的目标(单数名词),一般来说就是功能内聚,如:“计算水费”、“计算产值”等模块。功能内聚一般出现在软件结构图的较低层次上。 功能内聚模块的一个重要特点是:他是一个“暗盒”,对于该模块的调用者来说,只需要知道这个模块能做什么,而不需要知道这个模块是如何做的。
总结:在模块划分时,要遵循“一个模块,一个功能”的原则,尽可能使模块达到功能内聚。
3、降低耦合增强内聚的方法
1、少使用类的继承,多用接口隐藏实现的细节。 Java面向对象编程引入接口除了支持多态外, 隐藏实现细节也是其中一个目的。
2、模块的功能化分尽可能的单一,道理也很简单,功能单一的模块供其它模块调用的机会就少。(其实这是高内聚的一种说法,高内聚低耦合一般同时出现)。
3、遵循一个定义只在一个地方出现。
4、少使用全局变量。
5、类属性和方法的声明少用public,多用private关键字。
6、多用设计模式,比如采用MVC的设计模式就可以降低界面与业务逻辑的耦合度。
7、尽量不用“硬编码”的方式写程序,同时也尽量避免直接用SQL语句操作数据库。
8、避免直接操作或调用其它模块或类(内容耦合),如果模块间必须存在耦合,原则上尽量使用数据耦合,少用控制耦合,限制公共耦合的范围,避免使用内容耦合。
9、模块只对外暴露最小限度的接口,形成最低的依赖关系。
10、只要对外接口不变,模块内部的修改,就不得影响其他模块。
11、删除一个模块,应当只影响有依赖关系的其他模块,而不应该影响无关的部分。
参考
2、如何理解高内聚低耦合juejin.cn/post/699202…