引言
为避免软件危机,我们引入了软件工程学,而在软件工程学中,我们常面临的一个问题就是软件维护问题。软件维护涉及面很广,细化到代码编写方面,那便对代码规范有了要求,而设计模式则是一门面向编程规范的学问。在1995年,GoF(Gang of Four,四人组/四人帮)出版了《设计模式:可复用面向对象软件的基础》一书,提出了23种设计模式,而这也成了如今非常经典的软件设计与程序编写规范。在最近几年中,笔者已学过四遍了,算不上太多心得体会,但也多少有些收获,因此将其记录下来以备查阅。本文将对设计模式进行一个精要的讲解,所涉及内容不会太详细,小白慎入。(注:本文中部分文字和图片来自网络,参考资料统一列于文章最后,侵删)
预备知识
要学习设计模式,首先要会一门程序设计语言,以面向对象者入手为佳,本文以java来展开。其次,要会UML的类图,关于其网上资料很多,在此不做赘述。第三点,就是一些颇为经典的软件设计原则,这里将其列出并作简单介绍:
- 单一职责原则:一个类只干一件事——低耦合,高内聚
- 开闭原则:对扩展开放,对修改关闭
- 里氏代换原则:能接受基类的地方必能接受子类
- 依赖倒转原则:依赖于抽象而非具体类——针对抽象编程
- 接口隔离原则:用多个专门的接口取代单个统一的接口——降低耦合
- 合成复用原则:多用组合/聚合,少用继承
- 迪米特法则:若两个类不彼此直接通信,则此二者不能直接发生作用,需引入第三个类
设计模式总体介绍
如下图所示,设计模式一共可分为三类模式,分别是创建型模式、结构型模式、行为型模式。
范围目的 | 创建型模式 | 结构型模式 | 行为型模式 |
---|---|---|---|
类模式 | 工厂方法模式 | (类)适配器模式 | 解释器模式 模板方法模式 |
对象模式 | 抽象工厂模式 建造者模式 原型模式 单例模式 |
(对象)适配器模式 桥接模式 组合模式 装饰模式 外观模式 享元模式 代理模式 |
职责链模式 命令模式 迭代器模式 中介者模式 备忘录模式 观察者模式 状态模式 策略模式 访问者模式 |
各类模式的关系如下图所示:
创建型模式
工厂方法模式
Factory Method Pattern: Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.
将工厂和产品都抽象化了,但是具体的生产仍是由一个具体工厂生产一个具体产品。但是需要注意的是,一个具体工厂只生产一个具体产品,即一个工厂类只负责生产一类产品对象,这虽然符合了开闭原则,但是每增加一个产品时都需要新创建一个具体产品类和一个具体工厂类。
抽象工厂模式
工厂模式每个具体工厂只能生产一类对象,这在需要新增加产品类时会比较麻烦,因此抽象工厂模式中一个工厂类可以创建多类对象。
Abstract Factory Pattern: Provide an interface for creating families of related or dependent objects without specifying their concrete classes.
譬如说某个具体工厂为海尔工厂,则其既可以生产海尔电视机,也可以生产海尔冰箱。当需要生产TCL的电视机和电冰箱时,只需添加对应的具体工厂和具体产品即可,但是如果需要添加一个新的产品,比如说海尔需要生产手机了,那么这时还得修改其对应的具体工厂,因此可以看出其对于开闭原则也不能很好地遵循。
建造者模式
建造者模式讲究一步一步创建一个产品,创建的步骤是相同的,但是创建的产品的类型却可以不同。
Builder Pattern: Separate the construction of a complex object from its representation so that the same construction process can create different representations.
客户类只需调用构造者类的getResult
方法即可,根据具体类中每个部分的构建方式不同,getResult
返回的内容也不同,譬如肯德基的套餐,不同的套餐都有食物和饮料,但每个套餐提供的食物和饮料的种类却是不同的。
增加新的具体构建者不需要修改现有代码,符合开闭原则。但是如果产品的内部变化较大,则会需要很多个具体构建者,会使得类的结构比较臃肿。
原型模式
原型模式说明白点就是对象的自我拷贝,返回一个和自己一模一样的对象,只需根据需求来设置深拷贝和浅拷贝即可。
Prototype Pattern: Specify the kind of objects to create using a prototypical instance, and create new objects by copying this prototype.
单例模式
单例模式即为让类只能产生一个对象,无论谁来访问都只能访问到同一个对象。其原理就是将构造函数设置为私有函数,由一个公有静态接口来返回类的唯一实例。
Singleton Pattern: Ensure a class has only one instance and provide a global point of access to it.
单例模式分为饿汉式和懒汉式,饿汉式即在类加载时就创建了这个类的唯一实例,而懒汉式则在外部第一次获取该类的对象时才创建对象,上图所示即为懒汉式。
使用单例模式的思路可以设计有限多例模式,在此不做赘述。
结构型模式
结构型模式讨论的是如何将类或对象结合在一起形成更大的结构,可以相应地分为类结构型模式和对象结构型模式。
适配器模式
顾名思义,一般电脑充电器都会自带一个电源适配器,以将220V的电压适配到电脑电源所能接受的电压。适配器模式亦然,一个现有类已经实现了一些能满足客户端所要求的功能,但提供的接口不符合客户的需求,比如接口名和客户要求的接口名不同,因此这时可以设计一个适配器程序,由客户端只需调用适配器中符合客户要求的接口,而在适配器内部则代替客户端去调用那个现有类的函数。
Adapter Pattern: Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.
适配器模式使得目标类与客户类解耦,使得这个调用过程成了透明的过程,也增加了程序的可扩展性与可维护性,即便修改了目标类的实现代码,或者干脆换了一个类作为目标类,也只需要修改适配器类的内部代码即可,而不需对客户类进行任何修改。但是往往一个适配器类只能为一个客户端类服务,这也在一定程度上会使得代码比较臃肿。
桥接模式
桥接模式用于一个物体有两个或更多不同类型的属性的时候,用于化全连接的乘法为两两组合的加法。听起来很抽象,那就举个栗子。比如要创建一个图形,形状上有圆形、矩形、三角形,颜色上有红色、绿色、蓝色,因此如果为每一种形状都提供一个单独的颜色的版本的话,就需要9种不同的类,而如果使用桥接模式,则只需要单独定义3种图形和3种颜色,即一共6种,这在更复杂的系统中能显著减少类的数量。
其设计思想如下图,上面为采用全连接的思想,而下面则采用的是桥接模式的思想。
Bridge Pattern: Decouple an abstraction from its implementation so that the two can vary independently.
桥接模式的类图如下:
桥接模式提高了系统的可扩展性,扩展任何一个维度都不需要修改原有的系统,符合开闭原则,且其分离了抽象接口与其具体实现,使得具体类的实现对用户透明,但是其会增加系统的理解与设计难度,且要求抽象出系统中某两个单独变化的维度,因此其使用具有一定的局限性。
组合模式
组合模式包含两类对象,分别是容器对象和叶子对象,而其目的就是可以透明地处理容器对象和叶子对象。比如对于文件和文件夹的遍历等。
Composite Pattern: Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.
组合模式比较简单,就不多解释了,它分为透明组合模式和安全组合模式,透明组合模式是在抽象构建中声明所有要使用的操作,而安全组合模式中抽象构建中仅声明公有操作。
装饰模式
为一个类或对象增加行为,常采用的有继承和关联两种方法,然而继承的话,对现有类的扩充不能动态进行控制,而关联就不同,可以设计一个装饰类来按需求动态地扩充被嵌入对象的行为,因此装饰模式采用的是关联机制来对类进行扩充。
Decorator Pattern: Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
需要注意的是,装饰类的接口与被装饰类的接口要保持一致,这样客户端在使用的时候才能在不修改接口的情况下调用装饰类的实现。装饰模式比使用继承更加灵活,而且也符合开闭原则、合成复用原则等。抽象模式可以分为透明模式和半透明模式,透明模式中用户完全针对抽象编程,这点作简单了解即可。
外观模式
外观模式为每一个子系统提供了一个对外通信的统一的接口(外观类),客户类只需知道这个接口即可,而具体的复杂的这个子系统的实现过程则由外观类去实现。其能显著降低系统复杂度,提高客户端使用的便捷性。
Facade Pattern: Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.
用外观模式举个例子,即定义一个总开关作为外观类,其提供打开和关闭两种操作,而打开或关闭开关时,其自动打开或关闭了电灯、冰箱、电视等一系列的开关,而作为客户而言,只需打开这一个开关,子系统(外观类)就能为客户做一系列复杂的操作,而这些操作对客户而言都是透明的。但是当新增子系统时,可能需要修改外观类或客户端的源代码,这违背了开闭原则。需要注意的是,外观类的用意是为子系统提供一个集中化和简化的管理接口,因此不要在外观类中为子系统添加新的行为,那是装饰模式才干的事。
享元模式
享元模式很好理解,它的目的就是实现对象的共享。试想在开发中,加入需要经常是用到某一个字符串,比如s='abc'
,那么就可以将这个对象丢入对象池中,每次访问时都从对象池中将其取出即可,而不用在每次使用前都new一次,这样会造成内存空间的浪费。
根据对象信息的性质不同,可以将信息分为两类,一个为内部状态,这个是固定在享元对象中不会变的信息,即可以共享的信息,而外部状态则是会随着环境改变而改变的信息,即不可共享的部分。
Flyweight Pattern: Use sharing to support large numbers of fine-grained objects efficiently.
享元模式支持大量细粒度对象的复用,能极大减小内存中对象的数量,当系统中有大量对象且这些对象消耗大量内存、且这些对象的状态大部分可以外部化时,享元模式能起到很显著的作用。
代理模式
当客户端与目标对象之间不能直接通信时,往往采用代理模式,定义一个代理对象作为中介,根据实际需求为去掉一些客户不能看到的内容或者添加一些用户需要的额外服务,也包括数据加密等等。
Proxy Pattern: Provide a surrogate or placeholder for another object to control access to it.
代理模式能协调客户端和目标程序之间的通信,并对其数据进行加工处理,在一定程度上降低了系统的耦合度,但是因为添加了一个代理,因此代理在进行数据处理时往往会耗费一些额外的时间,而比如远程代理,更是会带来一些额外的通信时延,且有的代理模式的实现也较为复杂。
行为型模式
行为型模式控制类和对象之间相互协作的任务分配和流程控制,它分为对象行为型模式和类行为型模式,而通过合成复用原则可知,对象行为型模式比类行为型模式更加灵活,因此对象行为型模式使用范围往往更广。
职责链模式
该模式思路为将请求发送者与接收者分离开来,即请求发送者不能直接接触到最终的接收者,所有发送者的请求都只能被送到一个职责链的开始节点,而职责链中每个节点都具有接收请求和处理请求的能力,但是每个节点能处理的请求都是有限制的,比如请假系统,每一级领导最高职能批多少天的假,超出他的审批权限则需要他将这个请求汇报给上级处理,这样一条请求链就是职责链。
Chain of Responsibility Pattern: Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.
职责链模式的使用能显著降低系统耦合度,简化对象间的连接复杂度,也使得请求的处理相对于客户而言是透明的,增加了稳定性和灵活性。但是不能保证请求一定被处理,且由于请求需要一层层传递下去,可能会对系统性能产生影响。职责链模式分为纯的和不纯的职责链模式。纯的职责链模式要求每一个职责链节点只能选择处理或者将请求传递给下一个节点,不允许在进行了部分处理后又交给下一个节点处理,且纯的职责链节点必须能处理所有可能的请求,而在不纯的职责链中,允许一个请求最终不被任何接收者处理。
命令模式
命令模式将请求的发送与处理完全解耦,请求发送者只需要知道发送指令的接口即可,并不需要知道是谁在什么时候执行了具体什么操作。
Command Pattern: Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
命令模式中调用者只需调用Command抽象类中的执行操作函数,而具体操作则由具体命令类来执行。它可以降低系统耦合度,也提高了系统可扩充性,易于设计组合命令,但是也可能会造成系统中具体命令类过多的情况。使用命令模式可以实现撤销功能,即在具体命令类中记录上一次的操作,从而实现撤销。
解释器模式
解释器模式平日里用的机会不多,它是设计了一个句子的解释器,学过编译原理的朋友们可能对词法分析、语法分析等概念还有概念,而解释器模式则设计了一个解释器去解释一个特定的句子,比如简单的四则运算,难一点的自定义代码解析等。
Interpreter Pattern: Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.
使用解释器可以很容易地改变和扩充文法,且易于增加新的解释表达式的方法,但是使用场景实在少见,执行效率较低,无特殊需求的话仅做了解即可。
迭代器模式
迭代器模式就是为一个集合定义一个遍历访问的机制,如java的List、Set等的遍历,不过这些遍历都是系统一经定义好了的,我们也可以对自己设计的集合定义遍历的方式,也可以对已有的集合定义我们所需的迭代方式。
Iterator Pattern: Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
迭代器模式支持对同一个集合定义不同的遍历方式,且新增便利方式或聚合类都不需要修改其他代码,符合开闭原则。但是新增聚合类则意味着要新增迭代器类,这会造成类的个数的成对增加,增加了系统复杂度。该模式使得对无需了解聚合对象的内部表示即可遍历一个聚合对象,只需一个统一的接口即可。
中介者模式
中介者模式的目的就是设计一个单独的中介对象来管理所有其他对象之间的相互做作用,而非让对象间自行组织和作用,它减少了对象间两两之间的引用关系,使系统成为了一个松耦合的系统。如果让对象两两间直接通信或作用,则一个对象发送变化时可能需要引起另一个对象行为的改变,这会造成系统非常复杂且难以维护。举个具体的例子,比如工厂中设备A、B、C的启动过程构成一个循环,即当启动A时A会自动启动B,而后B会自动启动C,而当启动B时B会自动启动C,而后C自动启动A,等等这样构成一个循环,本来是没问题,但是如果B设备坏了,则启动A后设备C也不会自动启动,而如果采用一个中介者,由中介者对其他设备进行启动,就没有这种问题了。
Mediator Pattern: Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.
中介者模式简化了对象间的直接交互,但是容易造成中介者类十分复杂,难以维护。
备忘录模式
备忘录模式就相当于github上的一次提交,每提交一次都会保留一份备份。而备忘录模式也是在每一次执行备份操作的时候,保存一个对象的内部状态,并提供恢复机制,将对象还原到其之前保存的状态。
Memento Pattern: Without violating encapsulation, capture and externalize an object’s internal state so that the object can be restored to this state later.
备忘录模式提供了一种备份与恢复机制,但是如果备份较频繁,则会加大系统资源的消耗,它也可以用作实现撤销操作,且其功能相较命令模式而言要强大得多,也更易于实现。
观察者模式
该模式又称发布-订阅模式,其目的是当一个对象(被观察者)状态发送改变时,通知其他依赖于它的对象(观察者)它的状态的改变,并视情况对观察者的状态进行调整。
Observer Pattern: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
该模式可以实现表示层和数据层数据的分离,当表示层数据发生变化时,这个变化可以自动传播到数据层中,如果用在注册表单中,则体现在每修改一次用户名,则可以自动异步在后台判断该用户名是否和已存用户名重名。同时一个对象可以有多个观察者,这意味着这个对象的状态发生改变后需要通知所有其他观察者,这对系统性能有点影响,且如果观察关系较为复杂,则可能存在循环观察,这点需要引起重视。
状态模式
状态模式规定一个对象在其内部状态不同的时候,同样的操作会执行不同的行为。比如同一个函数中,会判断属于什么状态再去执行相应的功能。
State Pattern: Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.
从类图可以看出,每一个状态对应一个具体类,而具体操作由具体状态类来定义,这就将状态和操作从原始类中抽离了出来,这意味着新增一个具体状态时不需要修改原始代码,似乎符合开闭原则,但是它对开闭原则的支持并不太好,但是对于能够实现状态转换的类而言,每新增一个状态都需要修改类中进行状态转换的相关代码。
策略模式
策略模式指在实现某一个既定目标的时候,有多个不同的可选方法,比如排序算法,可以视情况动态选择快排、归并等算法,而不需要硬编码在原始程序中。
Strategy Pattern: Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
策略模式很好地支持了开闭原则,但是它要求客户端知道所有的策略类,且策略模式会需要生成很多个策略类,可以使用享元模式在一定程度上减少策略类的个数。
模板方法模式
该方法的目的是基于继承的思路来增加代码的复用。它定义了一个抽象父类,并实现了部分公有方法,而另一些方法则仅定义了接口,留给子类去实现。
Template Method Pattern: Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.
模板方法模式能很大程度上提高代码的复用率,但是对于每个不同的实现都需要单独定义一个子类,这会使得子类数量很多,难以维护。
访问者模式
该模式是针对数据或对象的访问而言,不同的用户对同一个对象或数据进行访问时往往有不同的访问权限,或会采取不同的操作手段,而访问者模式则是用来对访问的一个控制,它能使得在不修改原始代码的前提下新增数据访问或操作的方式。
Visitor Pattern: Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.
该模式使得新增访问操作变得很容易,所有元素相关操作都集中到了访问对象中。但是这也使得新增数据项就很困难,新增数据项需要修改所有的访问对象的代码,使得维护相当困难。
以上就是23种设计模式的简单总结,若有尚不清晰之处,日后慢慢补充。