前言
归纳总结是一个非常好的习惯,老话说“温故而知新”,无论是对于工作还是学习,在写了这一系列博文后确实深有感触。作为一名开发者,设计模式每天都在我们的工作中出现,但真正完整的总结一遍,才能知道自己到底理解到了什么程度。
一、23种设计模式
设计模式总的来说可以分为以下三类,共二十三种。之前已经陆续把每种模式单独写了一篇简单的文章,点击下面的模式可以直接访问对应的文章。
创建型模式,五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,七种:适配器模式、装饰者模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,十一种:策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
二、6种设计原则(SOLID)
1、单一职责原则( Single Responsibility Principle)
定义:就一个类而言,应该有且仅有一个引起它的变化的原因。
软件设计真正要做的许多内容,就是发现职责并把这些职责互相分离。
如果你能想到多于一个动机去改变一个类,那这个类就具有多于一个的职责。
单一职责原则的优点:
- 类的复杂性降低,实现什么职责都有明确的定义;
- 逻辑变得简单,类的可读性提高了,而且,因为逻辑简单,代码的可维护性也提高了;
- 变更的风险降低,因为只会在单一的类中的修改。
2、开闭原则(Open Closed Principle)
定义:一个软件实体如类、模块和函数应该可以扩展,但不可以修改。
面对新需求,对程序的改动应该是通过增加代码实现的,而不是修改现有代码来实现。
开闭原则是面向对象设计的核心所在,遵循开闭原则的最好手段就是抽象。但开发人员应该仅对程序中频繁出现变化的那些部分做出抽象,如果对于应用程序中每个部分都做刻意的抽象并不是个好主意。拒绝不成熟的抽象和抽象本身一样重要。
3、里氏替换原则(Liskov Substitution Principle)
定义:子类型必须能够替换掉它们的父类型。
只有当子类可以替换掉父类,并且程序功能不受影响时,父类才可以真正的被复用,而子类也可以在父类的基础上增加新行为。
面向对象的三大特征是封装、继承和多态,三者之间却并不 “和谐“。因为继承有很多缺点,当子类继承父类时,虽然可以复用父类的代码,但是父类的属性和方法对子类都是透明的,子类可以随意修改父类的成员。如果需求变更,子类对父类的方法进行了一些复写的时候,其他的子类可能就需要随之改变,这在一定程度上就违反了封装的原则,解决的方案就是引入里氏替换原则。
里氏替换原则为良好的继承定义了一个规范,它包含了如下含义:
1、子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。
2、子类可以有自己的个性,可以有自己的属性和方法。
3、子类覆盖或重载父类的方法时输入参数可以被放大。
4、子类覆盖或重载父类的方法时输出结果可以被缩小,也就是说返回值要小于或等于父类的方法返回值。
确保程序遵循里氏替换原则可以要求我们的程序建立抽象,通过抽象去建立规范,然后用实现去扩展细节,所以,它跟开闭原则往往是相互依存的。
4、依赖倒置原则(Dependence Inversion Principle)
定义:高层模块不应该依赖底层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象;
不可分割的原子逻辑就是底层模块,原子逻辑的再组装就是高层模块。抽象就是指接口或抽象类,两者都不能被实例化;而细节就是实现接口或继承抽象类产生的类,也就是可以被实例化的实现类。
依赖倒置原则是指模块间的依赖是通过抽象来发生关系的,其依赖关系是通过接口是来实现的,实现类之间不发生直接的依赖就是俗称的面向接口编程。
依赖倒置原则可以说是面向对象编程的标志,编程时考虑的如何针对抽象编程而不是针对实现细节编程,即程序中所有依赖关系都是终止于抽象类或接口,那就是面向对象设计,反之就是面向过程化的设计了。
5、接口隔离原则(Interface Segregation Principle)
定义:客户端不应该依赖它不需要的接口。意思就是客户端需要什么接口就提供什么接口,把不需要的接口剔除掉,这就需要对接口进行细化,保证接口的纯洁性。换成另一种说法就是,类间的依赖关系应该建立在最小的接口上,也就是建立单一的接口。
建立单一接口,这不是单一职责原则吗?
其实不是,单一职责原则要求的是类和接口职责单一,注重的是职责,一个职责的接口是可以有多个方法的,而接口隔离原则要求的是接口的方法尽量少,模块尽量单一,如果需要提供给客户端很多的模块,那么就要相应的定义多个接口,不要把所有的模块功能都定义在一个接口中,那样会显得很臃肿。
注意:原则虽说是原则,但它们并不是强制性的,更多的是建议。遵照这些原则固然能帮助我们更好的规范我们的系统设计和代码习惯,但并不是所有的场景都适用。
接口隔离原则,在现实系统开发中,我们很难完全遵守一个模块一个接口的设计,否则业务多了就会出现代码设计过度的情况,让整个系统变得过于庞大,增加了系统的复杂度,甚至影响自己的项目进度,得不偿失。
记住:在合适的场景选择合适的技术!
6、迪米特原则(Law of Demeter)
定义:如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的互相引用。如果中间一个类需要调用另一个类的某一个方法,可以通过第三者转发这个调用。一个对象应该对其他对象有最少的了解。也被称为最少知识原则。
该原则首先强调的是在类的结构设计上,应该尽可能低的设计成员的访问权限。其根本思想是强调了类的松耦合,类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会波及有关系的类。
也就是说,一个类应该对自己需要耦合或调用的类知道的最少,类与类之间的关系越密切,耦合度越大,那么类的变化对其耦合的类的影响也会越大,这也是我们面向对象设计的核心原则:低耦合,高内聚。
什么是直接的朋友?
每个对象都必然与其他对象有耦合关系,两个对象的耦合就成为朋友关系,这种关系的类型很多,例如组合、聚合、依赖、关联等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。
朋友:
- 当前对象本身(this)
- 以参量形式传入到当前对象方法中的对象
- 当前对象的实例变量直接引用的对象
- 当前对象的实例变量如果是一个聚集,那么聚集中的元素也都是朋友
- 当前对象所创建的对象
三、9种职责分配原则(GRASP)
职责的概念:
GRASP是通用职责分配软件模式(General Responsibility Assignment Software Patterns)有9种模式:
- 创建者(Creator)
- 信息专家(Information Expert)
- 低耦合(Low coupling)
- 控制器(Controller)
- 高内聚(High Cohesion)
- 多态性(Polymorphism)
- 纯虚构(Pure Fabrication)
- 间接性(Indirection)
- 防止变异(Protected Variations)
所谓模式,就是命名的问题-解决方案对。分为三部分:模式名称、问题、解决方案。
1、信息专家(Information Expert)
信息专家(Information Expert)
问题:给对象分配职责的基本原则是什么?
解决方案:把职责分配给具有完成职责所必需信息的类。
信息专家就是指具有完成职责所必需信息的类,所以信息专家模式就是把一个职责分配给信息专家。单一的职责由专家去解决。
- 如何实现高内聚和低耦合,关键在于如何给类分配职责。我们要遵循的原则就是把职责分配给信息专家。
- 信息专家模式是面向对象设计的最基本原则。
- 设计对象(类)的时候,如果某个类拥有完成某个职责所需要的所有信息,那么这个职责就应该分配给这个类来实现。这时,这个类就是相对于这个职责的信息专家。
对象的职责:
行为职责:
- 自身执行一些行为,如创建对象和计算。
- 初始化其他对象中的动作。
- 控制和协调其他对象中的活动。
认知职责:
- 对私有封装数据的认知。
- 对相关对象的认知。
- 对其能够导出或计算出事物的认知。
优点:
- 支持低耦合,因为对象使用自身信息完成任务,所以支持了低耦合
相关模式:
- 低耦合
- 高内聚
2、高聚合度/高内聚(High Cohesion)
高内聚(High Cohesion)
问题:如何将复杂性保持在可控范围内?
解决方案:分配一个职责时要保持高聚合性。
- 内聚度是对元素职责的相关性和集中程度的度量。内聚低的类通常表示粗粒度的抽象,或承担了本应该委托给其他对象的职责。
- 高内聚指的是类中方法和方法之间职责的相关性,也就是类自己能做的事情自己做。功能性紧密相关的职责(也就是方法)应该放在一个类里,并共同完成有限的功能,那么就是高内聚合。
- 高内聚和低耦合往往是伴随在一起的,低耦合减少和类和类之间的联系,高内聚也可以说是一种隔离,就像大厦由很多砖头、钢筋、混凝土组成,每一个部分(类)都有自己独立的职责和特性,每一个部分内部发生了问题,也不会影响其他部分,因为高内聚的对象之间是隔离开的。
- 低聚合度的类一般难以理解、难以重用、难以维护、容易受到外界细微变化的影响
优点:
- 增加了设计的清晰性和易于理解
- 简化了软件的升级和维护工作
- 通常能够支持低耦合
- 细粒度高相关功能的重用性增强,高内聚类一般能够被用于专门目的
相关模式:
- 低耦合
说明:
- 高内聚的类的方法数量较少,在功能性上有非常强的关联,而且不需要做太多的工作。
- 如果任务的规模比较大的话,应该将任务所涉及到的各项职责按照关联的强度分配到各个类中,然后让各个类的对象进行相互协作,共同完成这项任务。
- 高内聚的类优势明显,它易于理解、维护和复用。
- 高度相关的功能性与少量操作相结合,也可以简化维护和改进的工作。
- 细粒度的、高度相关的功能性类可以提高复用的潜力。
一个高内聚的类所包含的方法通常很少,功能之间的关联度很高,并且承担的职责不是太大,如果任务量太大,它通常需要其他类的协助。
3、低耦合度/低耦合(Low coupling)
低耦合(Low coupling)
问题:怎么降低依赖性,减少变化带来的影响,提高重用性
解决方案:分配职责时,使耦合性尽可能的低。
注意: 高耦合不是问题,问题是依赖了不稳的元素,一旦不稳定的元素改变,耦合的类就将改变。
关于低耦合一些基本原则:
- Don’t Talk to Strangers
- A已经和B有关联,如果分配A的职责给B不合适的话(违反信息专家模式),那么就把B的职责分配给A。
- 两个不同模块的内部类之间不能直接连接,否则很容易悲剧。
优点:
- 不受其他构件变化的影响。
- 易于隔离考察和理解。
- 便于复用。
相关模式:
- 防止变异
4、创建者(Creator)
创建者(Creator)
问题:应该由谁来创建某类的实例
解决方案:满足以下条件,应该由A类实例来创建B类的实例。A就称为创建者。
- A包含或者聚合了B
- A记录B
- A直接使用B
- A具有B的初始化数据(构造函数带参数)
优点:
- 支持低耦合, 因为A本身对于其创建者B就是可见的(即本身就有关联)所以不会增加耦合性
相关模式:
- 低耦合
- 整体-部分
- GoF设计模式:创建型模式中单例模式、工厂模式、原型模式、构造器模式都是GRASP创建者模式的扩展。
5、控制者(Controller)
控制者(Controller)
问题:谁来负责处理一个系统事件
解决方案:把职责分配给能代表以下选择之一的类(控制者):
- 【虚包控制者】整个”系统”,整个企业组织,真实世界中参与职责(角色控制者)的主动对象类
- 【角色控制者】根对象,运行软件的设备或主要子系统(这些是外观控制器的所有变体)。
- 【用例控制者】用例场景中所有事件的人工处理者,通常命名为Handler,Session。
系统事件是有外部参与者发起的高层事件,是一个外部输入事件。这些事件
对应于系统操作。
控制者是处理系统事件的非用户界面类。控制者定义了处理系统操作的方法。
关于控制器类,有如下原则:
- 系统事件的接收与处理通常由一个高级类来代替。
- 一个子系统会有很多控制器类,分别处理不同的事务。
- 一个控制者不会完成大量的系统操作工作,控制者只是协调或者控制这些活动,负责“转发”到多个不
同系统操作类去完成实际的系统操作【委派】。
控制器是UI层之上的第一个对象,他负责接收和处理系统操作信息。
优点:
- 提供了可复用和接口可插拔的潜力(把职责分配给应用/领域层的对象,而不是UI层的对象)
- 理解用例的状况
相关模式:
- 命令
- 外观
- 层
- 纯虚构
6、多态性(Polymorphism)
多态性(Polymorphism)
问题:如何处理基于类型的选择?如何创建可插拔的软件构件?
解决方案:当相关选择或行为随类型有所不同时,使用多态操作,分配给那些行为变化的类型。
- 原则上来说是通过多态操作把基于类型的可变行为的定义职责分配给发生该行为的类。放到Java中来
实现,就是定义一个公共的接口,然后不同的实现。 - 不要使用is-else处理类型的选择。
优点:
- 易于新变化所需要的扩展
- 无需影响客户端,便能够引入新的实现
相关模式:
- 防止变异
- 策略模式
7、纯虚构(Pure Fabrication)
纯虚构(Pure Fabrication)
问题:当不想违背高内聚和低耦合或者其他目标,但是基于专家模式所提供的方案又不合适时,哪些对象
应该承担这一职责。
解决方案:人为的制造一个类分配一组高内聚的职责,该类并不代表问题领域里面的概念,是需要的类,用以支持高内聚和低耦合。
优点:
- 支持高内聚
- 增加可复用性
相关模式:
- 低耦合
- 高内聚
- 信息专家(违反)
- 适配器、访问者、观察者模式
在设计类的时候,通常都是尽量于现实世界中的对象保持一致,那么我们从现实世界的对象抽象出来的类就叫做问题领域的类,这些类就承承担了问题领域内的职责。但是,当这些类需要进行一些非现实存在的操作时(比如说操作数据库),这些操作演变成了非问题领域的职责。这些非领域问题的职责还要分配给问题领域内的类吗?
按照信息专家等模式的原则,这样的分配是不合理的,这样会违反高内聚低耦合的基本原则。那么要怎么还分配这些非问题领域的职责呢?
Pure Fabrication模式提倡把这些非问题领域的职责分配给那些人工生成的或者容易实现此类型职责的概念类。
Domain Class的概念:我们设计对象的时候应该尽量保持与现实世界里的对象一致。这种与现实世界里的对象保持一致的从业务分析中抽象出来的类叫做“Domain Class”。它相当于问题领域里的类。
纯虚构模式强调的是职责应该置于何处。一般来说,纯虚构模式会通过表示解析或者行为解析来确定出一些纯虚构类,用于放置某一类职责。理想状况下,分配给这种虚构物的职责是要支持高内聚低耦合的,从而使整个系统处于一种良好的设计之中。
- 纯虚构必须设计成很好的重用性。这些类往往有细粒度的职责。
- 纯虚构基于功能来划分。不是基于对象。
- 纯虚构一般被考虑成一个体系结构中高层面向对象服务层的一部分。
8、中介者/间接性(Indirection)
间接性(Indirection)
问题:为了避免两个或者多个事物之间直接耦合,应该如何分配职责?如何使对象解耦,以支持低耦合并提高复用性潜力?
解决方案:将职责分配给中介对象,是其作为其他构件或服务之间的媒介,以避免它们之间的直接耦合,中介实现了其他构件之间的间接性。
优点:
- 支持了低耦合
相关模式:
- 低耦合
- 防止变异
- 纯虚构
- 适配器、桥接、中介者、外观、观察者模式、大量中介者中间对象都是纯虚构
中介模式是GRASP模式中解决类的关联问题的模式。一般来说,不同模块之间的内部类一定不能直连,但是模块间一定是有联系的,那么该把“关联”责任分配给哪些类呢?中介模式所提倡的解决方案:
- 当多个类之间存在复杂的消息交互(关联)时,提倡类之间不直接进行消息交互处理(非直接),而是导入第三方类,把责任(多个类之间的关联责任)分配给第三方类,降低类之间的耦合程度。
应用中介模式的好处:
- 高内聚。通过把“关联”的功能分散到第三方类,原来的类可以更加关注自身功能的实现。
- 低耦合。原本关联类之间不直接关联,降低类之间的耦合性。
- 高重用性。第三方类对“关联”功能的集中处理,与原来的类对自身功能的专注,有利于类的重用。
9、防止变异(Protected Variations)
防止变异(Protected Variations)/保护变化
问题:如何设计对象,子系统和系统,使其内部的变化或不稳定性不会对其他元素产生不良影响?
解决方案:识别变化或不稳定的地方,分配职责用以在变化地方创建稳定的接口。
- 防止变异模式意在设计稳定的接口来应对将来可能发生的变化或其它不安定的因素。在面向对象设计中,面向接口编程符合PV模式。
应用PV模式的好处:
- 提高系统对变化的应对能力。一旦系统的可预见的不安定因素发生变化(比如追加功能等),只需要生成一个已有的稳定接口的实现类就可以了,无需修改原来的类。
- 高内聚。具体的功能在各子类中实现,各类的内部功能具有高度聚集性。
- 低耦合。用户类只跟稳定接口通信,减少了跟其它陌生对象的关联的机会,降低了类之间的耦合性。
在方法中,只应该给以下对象发送消息
- this对象(自身)
- 方法的参数
- This对象的属性
- 作为this对象属性的集合中的元素
- 在方法中创建的对象
两种变化方法(热点):
- 变化点:现有、当前系统或需求中的变化
- 进化点:预测将来可能会产生的变化点,但并不存在于现有需求中
大部分设计原则和模式都是PV机制:包括多态、接口、间接性、数据封装以及许多GoF设计模式。
10、GRASP总结
在某些情况下,信息专家模式也许并不适用。这通常是由于内聚与耦合的问题所产生的。比如许多后台系统都有把模型(Model)类的数据存入数据库的功能。这一职责的履行所必需的信息显然是存在于各个模型类中,按照信息专家模式给出的建议,应该让这些模型类来完成把自身的数据保存到数据库中的功能。但是,这样的设计会导致内聚与耦合方面的问题。首先,所有的模型类都必须包含与数据库处理相关的逻辑,如与JDBC相关的处理逻辑。这使得模型类由于其他职责的存在而降低了它的内聚。其次,所有的模型类都引入了与JDBC的耦合关系,使得系统的耦合度上升。甚至,这种设计也会导致大量重复、冗余的数据库逻辑存在于整个系统中的各个角落。因此,在这种情况下,信息专家模式需要我们结合整个系统的耦合和内聚做出另外的考虑。
没有绝对的度量标准来衡量耦合程度的高低。使用低耦合模式的目的是为了创建一个可灵活伸缩的、可维护的、可扩展的系统。在这个目的之下,低耦合不能脱离信息专家和高内聚等其他模式孤立地考虑,而是应该同时权衡耦合与内聚。高耦合本身也并不是问题之所在,问题是与某些方面不稳定的元素之间的高耦合,这种高耦合会严重影响系统将来的维护性和扩展性。比如所有的Java系统都能安全地将自己去Java库(java.lang,java.util等)进行耦合,因为Java库是稳定的,与Java库的耦合不会给系统的灵活性、维护性、扩展性带来什么问题。
在一个设计灵活的OO系统中,对象的创建方式往往非常复杂。有些系统需要为了更好的性能而集中创建或者使用回收的实例(线程池、连接池、对象池等);有些系统需要根据某些条件来创建一族类的实例;甚至有些框架系统在框架的编写过程中根本不知道需要实例化哪一个类等等。在这些情况下,最好的方法是将创建类的实例的职责委派给抽象工厂(Abstract Factory)、具体工厂(ConcreteFactory)、创建器(Builder)等等辅助类,而不是创建者模式所建议的类。
控制器设计中的常见缺陷是分配的职责过多。这时,控制器会具有不好的低内聚。因而存在这样一条准则:正常情况下,控制器应当把需要完成的工作委派给其他的对象。控制器只是协调或控制这些活动,本身并不完成大量工作。在带UI的系统中,诸如“窗口”(Windows)、“视图”(View)或“文档”(Document)之类的UI类主要负责显示的功能,并不属于控制器。这些类不应该完成与系统事件相关的任务。通常情况下,它们接收这些事件,并将其委派给控制器,即委派模式。
在使用纯虚构模式时,不能毫无限制地对系统中的各种行为进行解析并纯虚构。如此往往会导致系统中大量的行为对象的存在,这样会对耦合产生不良的影响。
不要测试对象的类型,也不要使用条件逻辑来执行基于类型的不同选择。当相关选择或行为随类型(类)有所不同时,使用多态操作为变化的行为类型分配职责。
PV是非常基础的软件设计原则,几乎所有的软件或架构设计技巧都是防止变化的特例。它促成了大部分编程和设计的模式和机制,用来提供灵活性和防止变化。除了数据封装、接口、多态、间接性等机制是PV的核心机制之外,没有一种固定的或者是通用的办法能够防止一切变化的产生。因此PV的实现依赖的是一系列的OO设计方面的经验性原则,用以产生一个设计良好的高内聚、低耦合的系统。
高内聚与低耦合是在整个系统的设计过程中,需要不断地去考虑和评估。在少数情况下,较低的内聚也是被接受的。比如,为了方便专门的数据库逻辑人员统一管理SQL语句,系统中往往可以将与SQL语句相关的操作都放在一个独立的全能类中;另外,出于性能的考虑,在RPC中使用一个粗粒度的RPC服务器类,既可以减少服务器上对象的数目,可以减少网络请求和连接的数目,从而提高系统的性能。这些都需要从系统的全局出发,结合多种设计的原则进行权衡考虑。
模式都有使用的环境,没有万能的模式,不要刻意追求某种设计模式。
四、总结
这里我们再简单总结几个概念
OO(object-oriented):面向对象
OOA(object-oriented analysis):面向对象分析
OOD(object-oriented design):面向对象设计
OOP(object-oriented programming):面向对象编程
其实不难看出,面向对象的概念是一切的基础,然后再从分析到设计,再到编程,这样下来是一个完整的面向对象从设计到实现的过程。
比如我们常说的封装、继承、多态,这些就是面向对象的基本概念,然后本文中提到的设计原则(SOLID)和职责分配原则(GRASP),都是基于这些概念来总结出的用于分析和设计的思想,最终的设计模式是遵循了这些思想和原则,用代码将这些抽象的思想和原则变成现实。
五、感想
关于我这次写的这些内容,其实网上已经有很多不错的博文了,为什么我还要再写一次呢?
因为谁总结,收获是谁的。把这些设计模式全自己写一遍再总结写到文章中,是一个非常有收获的过程,远比看懂了一篇博客收获的多。
面向对象这个概念说起来简单,但实则包含了上面这么多的思想和原则,并且我总结到文章中的也只是九牛一毛。这次总结这些文章最大的感受就是对面向对象这个概念又有了新的理解,并且我相信过两三年后再回看自己写的这些文章,仍然会觉得现在写的非常不足。
我们不要拘泥于哪种模式或哪种原则,更不要为了设计而设计,这也是我多次写到的。面向对象是非常灵活的一种思想,上面的这些概念也好,原则也好,也只是一个参考,给我们漫长的编码之路指了一个大方向,最终的编码之路还是要自己走,所以一定要多学习,多思考,多总结。
本次这些文章主要参考的是《大话设计模式》这本书,还有一些其他的分享资料,并且参考了一些博主的相关博文,地址如下:
https://blog.csdn.net/zhangerqing/article/details/8194653
https://blog.csdn.net/jason0539/article/details/44956775
https://www.cnblogs.com/yeya/p/10655760.html