二十三种设计模式修炼手册
不知不觉,在开发这条道路上摸爬打滚也有些年头了,偶尔回头看看以前写的代码,真可谓粗糙至极。当然了,那时候还是小白,代码写得难看些情有可原,不过现在可不能再用以前的标准去衡量自己了,因此掌握一些高级架构技巧是必须的,设计模式正是一个很好的敲门砖。
在我看来,设计模式不仅仅只是一套模板,要想掌握设计模式并做到举一反三,必须深入理解其中的思想,这个模式是为了解决什么问题?解决的思路是什么?代码的实现又如何?如果问题细节发生了微小的变化又该如何处理?所以说思考很重要,不能死记硬背,一定要多想。
写下这篇文章,是为了梳理自己的知识点,做个记录。如果有来人看到了,并且对你有帮助的话,我也会很开心,因为知识是要传播的,大家都乐于分享自己的见解,才能共同进步。
设计模式的定义
模式一词起源于建筑业,描述了解决问题的核心方法。通过这种方式,可以多次重用那些已有的解决方案,无须重复相同的工作。
模式可以应用于不同的领域,软件模式是将模式的一般概念应用于软件开发领域,可以被认为是对软件开发中某一特定问题的解法的某种统一表示。软件模式并非仅限于设计模式,还包括架构模式、分析模式和过程模式等,在软件生存期的每一个阶段都存在着一些被认同的模式。
在软件模式领域,目前研究最深入的是设计模式。设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,使用这些设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。
设计模式的基本要素
设计模式一般有如下几个基本要素:模式名称、问题、目的、解决方案、效果、实例代码和其他相关设计模式,其中的关键元素包括以下四个方面:
模式名称:通过一两个词来描述模式的问题、解决方案和效果,以更好地理解模式并方便开发人员之间的交流。
问题:描述了应该在何时使用模式,它包含了设计中存在的问题以及问题存在的原因。有时候问题描述可能会包含使用该模式时必须满足的一系列先决条件。
解决方案:描述了设计模式的组成部分,以及这些组成部分之间的相互关系,各自职责和协作方式。解决方案并不描述一个特定而具体的设计或实现,而是提供设计问题的抽象描述和怎样用一个具有一般意义的元素组合(类或者对象)来解决这个问题。
效果:描述模式应有的效果以及在使用模式时应权衡的问题。效果主要包含模式的优缺点分析,因此需要综合考虑模式的效果。
设计模式的分类
设计模式一般有两种分类方式,一种是根据目的分类(模式是用来做什么的),另一种则是根据范围分类(模式是用来处理类之间的关系还是处理对象之间的关系)。根据这两种分类分别有如下两张表供参考:
范围\目的 | 创建型模式 | 结构型模式 | 行为型模式 |
---|---|---|---|
类模式 | 工厂方法模式 | (类)适配器模式 | 解释器模式 模板方法模式 |
对象模式 | 抽象工厂模式 建造者模式 原型模式 单例模式 |
(对象)适配器模式 桥接模式 组合模式 装饰模式 外观模式 享元模式 代理模式 |
职责链模式 命令模式 迭代器模式 中介者模式 备忘录模式 观察者模式 状态模式 策略模式 访问者模式 |
下面简单对二十三种设计模式进行说明
模式类别 | 模式名称 | 模式说明 |
---|---|---|
创建型模式 | ||
抽象工厂模式 | 提供了一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类 | |
工厂方法模式 | 该类的实例化操作延迟到子类中完成,即由子类来决定究竟该实例化(创建)哪一个类 | |
建造者模式 | 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示 | |
原型模式 | 通过给出一个原型对象来指明要创建对象的类型,然后通过复制这个原型对象来创建更多同类型的对象 | |
单例模式 | 确保在系统中某一个类只有一个实例,可以自行实例化并向整个系统提供这个实例 | |
结构型模式 | ||
适配器模式 | 将一个接口转换成客户希望的另一个接口,从而使接口不兼容的那些类可以一起工作 | |
桥接模式 | 将抽象部分与它的实现部分分离,使它们都可以独立地变化 | |
组合模式 | 组合多个对象形成树形结构以表示“整体-部分”的结构层次 | |
装饰模式 | 动态地给一个对象增加一些额外的职责 | |
外观模式 | 为复杂子系统提供一个一致的接口 | |
享元模式 | 通过运用共享技术有效地支持大量细粒度对象的复用 | |
代理模式 | 给某一个对象提供一个引用,并由代理对象控制对原对象的引用 | |
结构型模式 | ||
职责链模式 | 避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理为止 | |
职责链模式 | 避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理为止 | |
命令模式 | 将一个请求封装为一个对象,从而使得请求调用者和请求接收者解耦 | |
解释器模式 | 描述如何为语言定义一个语法,如何在该语言中表示一个句子,以及如何解释这些句子 | |
迭代器模式 | 提供了一种方法来访问聚合对象,而不用暴露这个对象的内部表示 | |
中介者模式 | 通过一个中介对象来封装一系列的对象交互,使得各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互 | |
备忘录模式 | 在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态 | |
观察者模式 | 定义了对象间的一种一对多依赖关系,使得当每一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新 | |
状态模式 | 允许将一个对象在其内部状态改变时改变它的行为 | |
策略模式 | 定义一系列算法,并将每一个算法封装在一个类中,让它们可以相互替换,策略模式让算法独立于使用它的客户而变化 | |
模板方法模式 | 定义一个操作中算法的骨架,而将一些步骤延迟到子类中 | |
访问者模式 | 表示将一个作用于某对象结构中的各元素操作,它使得用户可以在不改变各元素的类的前提下定义作用于这些元素的新操作 |
以上就是设计模式的简单介绍,下面是一些补充知识,如果已经掌握了可以不看。这些知识虽然不属于设计模式范畴,但对于我们理解设计模式有莫大的好处。
统一建模语言
统一建模语言(UML)是一种可视化的标准建模语言,通过UML可以构造软件系统的蓝图。在设计模式中,使用UML来分析和设计每一个模式的结构,描述每一个模式实例,帮助我们深入理解设计模式。
比如要盖一栋房子,需要先设计图纸,设计图纸就是一种设计语言,也就是模型语言。在一个现代化工程中,人们要沟通和协作,就必须使用标准的工业化设计语言,通过建模进行描述,把所要设计的结构和系统的行为联系起来,对系统的结构进行可视化控制。
UML 结构
UML 是由图形符号表达的建模语言,其主要包括以下几个部分:
视图
使用不同的视图从不同角度来描述软件系统,包括:
用户视图:以用户观点表示系统的目标,它是所有视图的核心,该视图描述系统的需求
结构视图:表示系统的静态行为和静态元素,如包、类与对象,以及它们之间的关系
行为视图:表示系统的动态行为,描述组成元素如对象在系统运行时的交互关系
实现视图:表示系统中逻辑元素的分布,描述系统中物理文件以及它们之间的关系
环境视图:表示系统中物理元素的分布,描述系统中硬件设备以及它们之间的关系
图
提供了十三种与上述五种视图相对。在设计模式的学习中,重点关注类图、顺序图和状态图即可。
用例图:对应用户视图。在用例图中,使用用例来表示系统的功能需求,用例图表示多个外部执行者与系统用例之间以及用例与用例之间的关系。
类图:对应于结构视图。类图使用类来描述系统的静态结构,类图包括类和它们之间的关系。
对象图:对应于结构视图。对象图用于表示类的对象实例之间的关系。
包图:对应于结构视图。描述包与包之间的关系。
组合结构图:对应于结构视图。表示一个类的内部结构。
状态图:对应于行为视图。描述一系列对象的状态及状态之间的转换。
活动图:对应于行为视图。表示系统中各种活动的次序。
顺序图:又称时序图或序列图,对应于行为视图。表示对交互,重点表示对象之间发送消息的时间顺序。
定时图:对应于行为视图。定时图采用一种带数字刻度的时间轴来描述消息的顺序,相比顺序图来说更加精确。
交互概览图:对应于行为视图。可以把交互概览图理解为细化的活动图,在其中的活动都通过一些小型的顺序图来表示。
组件图:又称构件图,对应于实现视图。描述每个功能所在组件位置以及它们之间的位置。
部署图:又称实施图,对应于环境视图。描述软件中各个组件驻留的硬件位置以及这些硬件之间的交互关系。
模型元素
模型元素包括事物以及事物之间的联系。事物代表任何可以定义的东西,事物之间的关系把事物联系在一起,组成有意义的结构模式
通信机制
为模型元素提供额外的注释、修饰和语义。
面向对象设计原则
面向对象设计原则是学习设计模式的基础,每一种设计模式都符合某一种或多种面向对象设计原则。在软件开发中使用这些原则可以提高软件的可维护性和可复用性,让我们可以设计出更加灵活也更容易扩展的软件设计,实现可维护性复用的目标。
单一职责原则
一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中
一个类承担的职责越多,被复用的可能性越小,并且相当于将这些职责耦合在一起。因此需要将这些职责进行分离,实现高内聚、低耦合的指导方针。
开闭原则
一个软件实体应当对扩展开放,对修改关闭。也就是说在设计一个模块,应当使这个模块可以在不被修改的前提下被扩展。
在开闭原则的定义中,软件实体可以是一个软件模块、一个由多个类组成的局部结构或一个类。
软件的需求会随着时间推移发生变化,如果软件设计符合开闭原则,就可以在扩展时无须修改现有代码,保证稳定性与延续性。
抽象化是满足开闭原则的关键,通过定义一个相对稳定的抽象层,将不同的实现行为在具体实现层中实现。如果需要修改,无须改动抽象层,只需增加新的实体类来实现新的业务功能即可。
里氏代换原则
所有引用基类(父类)的地方必须能透明地使用其子类的对象。说白了就是:在软件中如果能使用其基类对象,那么一定能使用其子类对象。把基类都替换成它的子类,程序不会产生任何错误。但反过来则不成立,如果一个软件实体使用的是一个子类,那么它不一定能使用基类。
里氏代换原则是实现开闭原则的重要方式之一,在程序中尽量使用基类类型来定义对象,而在运行时再确定其子类类型,用子类对象来替代父类对象。
依赖倒转原则
高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。即代码要依赖于抽象的类,而不依赖于具体的类,要针对接口编程,不要针对实现编程。
如果说开闭原则是面向对象设计的目标的话,那么依赖倒转原则就是面向对象设计的主要手段。下面介绍依赖倒转原则中经常提到的两个概念。
类之间的耦合:在面向对象系统中,两个类之间通常可以发生三种不同的耦合关系(依赖关系)
1. 零耦合关系:两个类之间没有任何耦合关系 2. 具体耦合关系:两个具体类之间存在一个类对另一个具体类实例的直接引用 3. 抽象耦合关系:发生在一个具体类和抽象类之间,也可以发生在两个抽象类之间。依赖倒转原则要求客户端依赖于抽象耦合。
依赖注入:简单来说,依赖注入就是将一个类的对象传入另一个类,注入时应该注入父类对象,而在程序运行时再通过子类对象来覆盖父类对象。依赖注入有三种方式
1. 构造注入:通过构造函数注入实例变量 2. 设值注入:通过Setter方法注入实例变量 3. 接口注入:通过接口方法注入实例变量
接口隔离原则
一旦一个接口太大,则需要将它分割成一些更小的接口,使用该接口的客户端仅需知道与之相关的方法即可。
这里的接口往往有两种不同的含义:
一种是指一个类型所具有的方法的特征的集合,仅仅是逻辑上的概念,接口的划分将直接带来类型的划分。此时可以把接口理解成角色,一个接口只代表一个角色,每个角色都有它特有的一个接口,此时这个原则叫做角色隔离原则。
另一种是指接口仅仅提高客户端需要的行为,即所需的方法。接口应该尽量细化,接口中的方法进来少,每个接口只包含一个客户端所需的角色。
合成复用原则
尽量使用组合对象,而不是继承来达到复用的目的。通俗来说,合成复用原则就是指一个新的对象里通过关联关系(包括组合关系和聚合关系)来使用一些已有对象,使之成为新对象的一部分;新对象通过委派调用已有对象的方法达到复用已有功能的目的。
通过继承来实现复用很简单,子类可以覆盖父类方法,易于扩展。但会破坏系统的封装性,因为继承会将基类的实现细节暴露给子类,这种复用又称为“白箱复用”。
通过组合/聚合来复用是将一个类的对象作为另一个类的对象的一部分。新对象可以调用已有对象的功能,这种复用又称为“黑箱复用”。
迪米特法则
指一个软件实体应尽可能少的与其他实体发生相互作用。当一个模块修改时,就会尽量少的影响其他模块,这是对软件实体之间通信的限制,它要求软件实体之间通信的宽度和深度。
在迪米特法则中,对于一个对象,其朋友包括以下几类:
当前对象本身(this)
以参数形式传入到当前对象方法中的对象
当前对象的成员对象
如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友
当前对象所创建的对象
任何对象如果满足上面的条件之一,就是当前对象的“朋友”,否则就是“陌生人”。
狭义的迪米特法则:如果两个类之间不必彼此通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。狭义的迪米特法则可以降低类之间的耦合,但也会造成系统不同模块之间通信效率降低,使得系统的不同模块之间不容易协调。
广义的迪米特法则:指对象之间的信息流量、流向以及信息的影响的控制,主要是对信息隐藏的控制。信息的隐藏可以使各个子系统之间脱耦,每一个模块不依赖于其他模块存在,因此每一个模块都可以独立地在其他地方使用。