您现在的位置是:首页 >技术杂谈 >面向对象设计中的七大设计原则与二十三种设计模式网站首页技术杂谈
面向对象设计中的七大设计原则与二十三种设计模式
目录
七大设计原则
面向对象设计原则总的来说就是:人们在不断进行系统设计、代码设计时总结出的一些具有实践意义的经验准则,能够帮助程序设计人员更好地设计、重构与优化代码。本文主要对七个设计原则进行详细描述。
单一职责原则
内涵与目的
单一职责原则指的是将某一个模块、一个类或者一个方法中的功能点(职责)设计的越少越好,以减少其被复用的次数以及可能性,通过提高模块的内聚性来减少模块间的耦合程度,保证软件实体只有一个引起它变化的原因。
涉及的知识点
- 类的职责主要包括两个方面:数据职责与行为职责,数据职责通过其属性进行描述,而行为职责则通过其方法进行体现。
- 单一职责原则是实现高内聚、低耦合的指导方针。它是最简单但是又最难运用的原则。
例子
例如现在有个接口类IPhone,它包含了四个方法。可以发现该接口类的职责可以被拆分为两个模块:通讯协议管理模块与通讯数据传输模块。
public interface IPhone {
void dial(String phoneNumber); //根据协议建立连接
void chat(Object o); //通话
void answer(Object o); //回应
void hangUp(); //通话完毕
}
根据单一职责原则,可以将IPhone接口类拆分为以下两个接口类。
public interface IConnectionManagaer {
void dial(String phoneNumber); //根据协议建立连接
void hangUp(); //通话完毕
}
public interface IDataTransfer {
void chat(Object o, IConnectionManager cm);
void answer(Object o, IConnectionManager cm);
}
开闭原则
定义
开闭原则中的“开闭”指的是对扩展开放而对修改关闭,具体意思是当需要扩展一个实体的功能时,能够在不修改原先模块代码的前提下进行功能的扩展。
该原则的关键是抽象化。意味着我们需要找到模块中的可变因素,通过抽象化对这些可变因素进行封装,而其他模块只需要引用抽象层即可。
实现
在面向对象设计中,我们可以基于抽象化将相似的类进行统一封装。例如在Java中,可以通过abstract与interface关键字将具有相似功能逻辑的类进行抽象,让这些类extends或者implement这些抽象类即可。
当我们需要扩展功能时,只需要增加这些抽象类的子类即可,原先的代码均不需要发生改变。
例子
例如现在有一个图表展示类(ChartDisplay),这个类可以根据display方法中传入的参数来创建不同的具体图表类(PieChart、BarChart)。此时若我想要增加一个新的图表类(LinearChart),我就需要在ChartDisplay类的display方法中增加新的判断逻辑。这明显不符合开闭原则定义(需要修改原先代码)。
我现在在上述类图中对这些具体图表Chart类进行抽象化,增加一个AbstractChart类,让这些具体实现类均实现该抽象类。此时当我在新增一个LinearChart类时,原先的代码不需要做任何修改。我只需要每次在ChartDisplay类中的setChart方法中传入不同的具体图表类(如PieChart)即可,在它的display方法中通过chart.display()实现具体的代码逻辑。
当然,客户端调用ChartDisplay类的业务逻辑代码肯定要改(每次构建一个新的图表便需要传入不同的图表子类),这里的对修改关闭主要指的是业务流程代码不需要进行更改。
依赖倒转原则
定义
依赖倒转指的是,高层模块不应该直接依赖于底层模块,应该依赖于底层模块的抽象层。在Java中,模块之间的依赖关系应体现对接口类或者抽象类的依赖,而不应依赖于它们的子类(实现类)。
依赖倒转原则关键是要针对接口编程,而不是针对实现编程。
传统过程式设计
面向对象设计
与开闭原则的联系
可以发现,开闭原则与依赖倒转原则都强调了抽象化。可以说依赖倒转原则是开闭原则的一种重要的实现方式。
如果说开闭原则是面向对象设计的目标,那么依赖倒转原则就是面向对象设计的主要手段。
例子
以先前的图表为例,高层模块(ChartDisplay)不应该直接与底层模块(PieChart、BarChart)进行关联,这样会造成二者紧耦合,不利于代码扩展。
所以我们可以对这些具体的图表实现类进行抽象(AbstractChart),高层模块只需要依赖于抽象类即可,不需要直接与底层模块进行关联。
里氏替换原则
定义与意义
里氏替换其实是让子类可以直接替换或者向上转型成基类,而使得原先的代码业务逻辑并没有发生冲突,程序依然能够正常运行。可以说里氏替换原则是为了更好地实现依赖倒转原则与开闭原则,指导我们如何对代码进行抽象化。
通俗的说就是子类可以扩展父类的功能,但不能改变父类原有的功能。
四层含义
- 子类必须完全实现父类的抽象方法;
- 子类可以有自己的个性;
- 覆盖或实现父类的方法时输入参数可以被放大;
- 覆盖或实现父类的方法时输出结果可以被缩小。
对于后面两层的含义的理解,我找到了一个比较好的例子予以说明。例如父类有一个方法正整数 operate(正整数)
,那么子类的override可以是正偶数 operate(整数)
,首先输入参数变为整数,代表子类扩展了父类的功能,从只接收正整数变为了可以接收负整数;而返回参数从正整数变为了正偶数,并没有影响父类原有的功能,因为正偶数是正整数的一部分。如果子类的override是整数 operate(正偶数)
,则说明子类缩小了父类可以处理的参数范围,改变了父类原有的功能,不符合里氏替换原则;而返回参数由正整数变为整数,则最终客户端的调用结果有可能为负整数,从而出现逻辑错误,也不符合里氏替换原则。
例子
从下述类图中可以发现,该类图符合开闭原则与依赖倒转原则,也符合单一职责原则。
那么它是否符合里氏替换原则呢?里氏替换原则定义中的基类在这里指的是AbstractGun
,而子类就是HandGun(手枪)、Rifle(步枪)、MachineGun(机枪)、ToyGun(玩具枪)
。结合代码语义与现实场景中可以知道,ToyGun
并不能使Soldier
士兵类执行killEnemy
方法。而前三者(手枪、步枪、机枪)可以使Soldier
执行killEnemy
方法。
所以当客户端(Client)调用Soldier类时,如果传入的参数是ToyGun,则代码的逻辑便出现了问题,从而说明了该代码设计中子类并不能直接替换为基类。所以该类图并不符合
里氏替换原则。
所以为了满足里氏替换原则,我们可以将ToyGun单独进行抽象,增加一个AbstractToy
抽象类。这样从代码语义与业务逻辑的角度来看,各个子类在替换各自的基类时,并不会产生逻辑上的冲突。
接口隔离原则
定义
客户端不应该依赖于它不需要或者不使用的接口方法。当一个接口中定义的方法较多,我们可以将其拆分为粒度更小的接口,而客户端只需要依赖于那些它真正需要使用的接口。
接口隔离原则可以看成是单一职责原则在抽象化角度上的扩展。
例子
在下述类图中,假设ClientA只使用operatorA()方法,ClientB只使用operatorB()方法,ClientC只使用operatorC()方法,那么该类图便不符合接口隔离原则,因为该类图中定义了一个AbstractService(胖接口)来服务所有的客户端类。我们便可以根据接口隔离原则对下述类图进行优化重构。
以下是重构之后的类图,将原先的AbstractService
拆分成三个粒度更小的接口,分别服务于三个Client,以此来满足接口隔离原则。这种重构方法在服务行业又被称为定制服务。
合成复用原则
定义
合成复用原则指的是需要将类之间的继承关系
转变为组合或聚合关系
来达到复用的目的。
继承复用与组合/聚合复用的区别
继承复用(“白箱”
复用):实现简单(超类的功能自动进入子类),易于扩展(实现多个子类,重写容易)。当需要调用某个类的方法时,通过继承该类的方式来直接调用该类的方法。但是不够灵活,调用的父类的方法在程序运行过程中是静态的,子类不能动态指定具体的实现。还破坏了系统的模块封装性,因为子类可以通过重写机制修改父类的方法逻辑,并且可能会违反里氏替换原则,从而导致代码维护上的困难,造成父类和子类的紧耦合。另外,若父类的实现方法发生改变,则导致其实现子类均需要发生变化(例如改变方法传入的参数)。
组合/聚合复用(“黑箱”
复用):耦合度相对较低,灵活性较高,可以在程序运行时基于抽象化动态指定成员对象的具体实现类。不过若所调用的成员对象的方法也发生改变,那么所有调用过该方法的类都需要发生变化,这点与继承复用相类似。
涉及的知识点
- 组合与聚合都是对象建模中关联(Association)关系的一种;
- 组合是一种更强的关联,部分组成整体,而且不可分割,部分不能脱离整体而单独存在;
- 组合/聚合和继承都是实现复用的两个途径;
- 尽量使用组合/聚合,而不是使用继承;
例子
例如现在StudentDAO与TeacherDAO都需要调用DBUtil中的getConnection()方法,而在下述类图中,是通过继承复用的形式进行调用。那么我们可以将其重构为组合复用形式。
重构之后的类图如下,StudentDAO与TeacherDAO均有DBUtil成员变量,通过组合关系调用getConnection()方法,体现了合成复用原则。同时DBUtil还实现了抽象化,两个DAO类便可以动态指定新的数据库连接而不需要修改DAO类的代码,符合开闭原则与依赖倒转原则。
何时可以使用继承复用?
只有满足以下三点时,才可以使用继承复用。
- 当子类与父类之间是
“Is”
的关系,而不是“Has”的关系。“Is”的关系符合继承关系语义,“Has”的关系应当用组合/聚合来描述; - 永远不会出现需要将该子类转为另一个父类的子类的情况,否则违背开闭原则(例如上面例子中出现需要新增一个数据库连接的需求),这也说明父类和子类不是“Is”的关系。
- 子类不应大量重写父类的方法,否则没有继承复用的必要,不如直接重写一个全新的类。
迪米特法则(最少知识原则)
定义
一个软件实体应该尽可能少的与其他实体相互作用,只与“朋友”交流,而不与“陌生人”交流,以此减少实体间的耦合度。
狭义的迪米特法则
强调可以通过创建中介者类(第三者类)来协助两个不必直接通信的类进行信息传输,但会导致通讯效率降低。
广义的迪米特法则
强调一个类或者一个模块需要控制对外的信息隐藏,从而独立各个模块之间的开发、优化、使用和修改,促进模块复用,让各个模块脱耦。
如何控制信息隐藏
- 应该尽可能创建耦合度较低的类;
- 每个类应当尽可能降低成员变量、成员方法的访问权限。
- 尽可能将一个类设计成不变类,也就是类中的属性尽可能不发生更改。
- 尽可能降低一个对象对其他对象的引用次数。
可交互的“朋友”
- 当前对象本身(this);
- 形参;
- 当前对象的成员变量;
- 当前对象的成员变量集合中的所有元素;
- 当前对象所创建的对象。注意,一个对象应尽可能少创建其他对象。
例子1
现在有如下类图与Friend
、Someone
类的Java实现代码。
public class Someone {
void operate(Friend friend) {
friend.getStranger().operate();
}
}
public class Friend {
private Stranger stranger = new Stranger();
public Stranger getStranger() {
return this.stranger;
}
}
从上述类图与代码中可以看出,Friend
类与Stranger
类是“朋友”关系,Friend
类与Someone
类也为“朋友”关系,但Someone
类与Stranger
类为“陌生人”关系,由迪米特法则可知,Someone
类不能直接调用Stranger
类的operate
方法。一种解决方法就是调用转发,将Someone
类对Stranger
的方法调用转变为Friend
对Stranger
的方法调用。
以下是重构后的类图与Java代码实现。
public class Someone {
void operate(Friend friend) {
friend.operateByStranger();
}
}
public class Friend {
private Stranger stranger = new Stranger();
public void operateByStranger() {
this.stranger.operate();
}
}
例子2
在下述类图中,多个Form类对多个DAO类的关联关系较为复杂,针对该问题可以创建一个中介控制层,以该控制层代替Form来引用对应的DAO,将调用的过程进行封装。
以下是重构后的类图。
二十三种设计模式
模式的经典定义
Alexander给出的关于模式的定义如下:
A pattern is a
solution
to aproblem
in acontext
.
一个模式的提出首先要有Context
,即前提条件约束与应用场景。在特定的应用场景下还要有对应的Problem
,需要明确我们需要解决什么样的问题,有明确的目标。针对该问题所提出的具有实践意义的Solution
才能称为模式。
软件模式
1990年,软件工程界开始关注Christopher Alexander等在这一住宅、公共建筑与城市规划领域的重大突破,最早将该模式的思想引入软件工程方法学的是1991-1992年以“四人组(Gang of Four,
GoF
,分别是Erich Gamma, Richard Helm, Ralph Johnson和John Vlissides)”自称的四位著名软件工程学者,他们在1994年归纳发表了23种在软件开发中使用频率较高的设计模式,旨在用模式来统一沟通面向对象方法在分析、设计和实现间的鸿沟
。
软件模式
可以认为是在一定条件下对软件开发这一特定“问题”的“解法”的某种统一表示。
设计模式
定义
设计模式(Design Pattern)
是一套被反复使用、多数人知晓的、经过分类编目的代码设计经验的总结,使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。
分类
三大类设计模式
根据Problem
将二十三种设计模式分为创建型、结构型行为型三种设计模式。
类模式与对象模式
类模式
主要指的是在构建代码时,类与类之间的关系是通过继承形式完成的;而对象模式
指的是类与类之间的关系是通过成员对象之间的引用、调用来完成。
创建型设计模式
简单工厂模式
模式动机
- 考虑一个简单的软件应用场景,一个软件系统可以提供多个外观不同的按钮(如圆形按钮、矩形按钮、菱形按钮等),这些按钮都源自同一个基类,不过在继承基类后不同的子类修改了部分属性从而使得它们可以呈现不同的外观,如果我们希望在使用这些按钮时,不需要知道这些具体按钮类的名字,只需要知道表示该按钮类的一个参数,并提供一个调用方便的方法,把该参数传入方法即可返回一个相应的按钮对象,此时,就可以使用简单工厂模式。
模式结构
角色
- Factory 工厂角色
- Product 抽象产品角色
- ConcreteProduct 具体产品角色
模式优点
- 工厂类含有必要的判断逻辑,可以决定在什么时候创建哪一个产品类的实例,客户端可以免除直接创建产品对象的责任,而仅仅“消费”产品;简单工厂模式通过这种做法实现了对责任的分割,它提供了专门的工厂类用于创建对象。
- 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可,对于一些复杂的类名,通过简单工厂模式可以减少使用者的记忆量。
- 通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。
模式缺点
- 由于工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。
- 使用简单工厂模式将会增加系统中类的个数,在一定程序上增加了系统的复杂度和理解难度。
- 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。不符合开闭原则。
- 简单工厂模式由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构。
适用环境
- 工厂类负责创建的对象比较少:由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。
- 客户端只知道传入工厂类的参数,对于如何创建对象不关心:客户端既不需要关心创建细节,甚至连类名都不需要记住,只需要知道类型所对应的参数。
工厂方法模式
模式动机
- 通过定义抽象工厂类来解决简单工厂模式中不符合开闭原则的问题。
模式结构
角色
- Product:抽象产品
- ConcreteProduct:具体产品
- Factory:抽象工厂
- ConcreteFactory:具体工厂
模式优点
- 在工厂方法模式中,工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节,用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。
- 基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够使工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。工厂方法模式之所以又被称为多态工厂模式,是因为所有的具体工厂类都具有同一抽象父类。
- 使用工厂方法模式的另一个优点是在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了。这样,系统的可扩展性也就变得非常好,完全符合“开闭原则”。
模式缺点
- 在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
- 由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到DOM、反射等技术,增加了系统的实现难度。
适用环境
- 一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。
- 一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。
- 将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定,可将具体工厂类的类名存储在配置文件或数据库中。
抽象工厂模式
模式动机
- 解决工厂方法中产品结构单一问题,一个工厂可以创建多个产品等级结构中的产品。
模式结构
角色
- AbstractFactory:抽象工厂
- ConcreteFactory:具体工厂
- AbstractProduct:抽象产品
- Product:具体产品
模式优点
- 抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易。所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。另外,应用抽象工厂模式可以实现高内聚低耦合的设计目的,因此抽象工厂模式得到了广泛的应用。
- 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。这对一些需要根据当前环境来决定其行为的软件系统来说,是一种非常实用的设计模式。
- 增加新的具体工厂和产品族很方便,无须修改已有系统,符合“开闭原则”。
模式缺点
- 在添加新的产品对象时,难以扩展抽象工厂来生产新种类的产品,这是因为在抽象工厂角色中规定了所有可能被创建的产品集合,要支持新种类的产品就意味着要对该接口进行扩展,而这将涉及到对抽象工厂角色及其所有子类的修改,显然会带来较大的不便。
- 开闭原则的倾斜性(增加新的工厂和产品族容易,增加新的产品等级结构麻烦)
适用环境
- 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是重要的。
系统中有多于一个的产品族,而每次只使用其中某一产品族。 - 属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。
- 系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现。
建造者模式
模式动机
- 在软件开发中,也存在大量类似汽车一样的复杂对象,它们拥有一系列成员属性,这些成员属性中有些是引用类型的成员对象。而且在这些复杂对象中,还可能存在一些限制条件,如某些属性没有赋值则复杂对象不能作为一个完整的产品使用;有些属性的赋值必须按照某个顺序,一个属性没有赋值之前,另一个属性可能无法赋值等。
- 复杂对象相当于一辆有待建造的汽车,而对象的属性相当于汽车的部件,建造产品的过程就相当于组合部件的过程。由于组合部件的过程很复杂,因此,这些部件的组合过程往往被“外部化”到一个称作建造者的对象里,建造者返还给客户端的是一个已经建造完毕的完整产品对象,而用户无须关心该对象所包含的属性以及它们的组装方式,这就是建造者模式的模式动机。
模式结构
模式优点
- 在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
- 每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象。
- 可以更加精细地控制产品的创建过程。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
- 增加新的具体建造者无须修改原有类库的代码,指挥者类针对抽象建造者类编程,系统扩展方便,符合“开闭原则”。
模式缺点
- 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
- 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。
适用环境
- 需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性。
- 需要生成的产品对象的属性相互依赖,需要指定其生成顺序。
- 对象的创建过程独立于创建该对象的类。在建造者模式中引入了指挥者类,将创建过程封装在指挥者类中,而不在建造者类中。
- 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品。
模式简化
- 省略抽象建造者角色。可以考虑使用静态内部建造者类。
- 省略指挥者角色。与静态内部建造者类结合使用。
原型模式
模型动机
- 在面向对象系统中,使用原型模式来复制一个对象自身,从而克隆出多个与原型对象一模一样的对象。
- 在软件系统中,有些对象的创建过程较为复杂,而且有时候需要频繁创建,原型模式通过给出一个原型对象来指明所要创建的对象的类型,然后用复制这个原型对象的办法创建出更多同类型的对象,这就是原型模式的意图所在。
模式结构
角色
- Prototype:抽象原型类
- ConcretePrototype:具体原型类
- Client:客户类
模式优点
- 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过一个已有实例可以提高新实例的创建效率。
- 可以动态增加或减少产品类。
- 原型模式提供了简化的创建结构。
- 可以使用深克隆的方式保存对象的状态。
模式缺点
- 需要为每一个类配备一个克隆方法,而且这个克隆方法需要对类的功能进行通盘考虑,这对全新的类来说不是很难,但对已有的类进行改造时,不一定是件容易的事,必须修改其源代码,违背了“开闭原则”。
- 在实现深克隆时需要编写较为复杂的代码。
适用环境
- 创建新对象成本较大,新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其属性稍作修改。
- 如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占内存不大的时候,也可以使用原型模式配合备忘录模式来应用。相反,如果对象的状态变化很大,或者对象占用的内存很大,那么采用状态模式会比原型模式更好。
- 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。
浅克隆与深克隆的区别
在浅克隆中,改变源对象的引用类型变量(除String),则克隆对象对应的引用类型变量也随之改变;而在深克隆中,改变源对象的引用类型变量,克隆对象对应的引用类型变量不会发生改变。
单例模式
模式动机
- 如何保证一个类只有一个实例并且这个实例易于被访问呢?定义一个全局变量可以确保对象随时都可以被访问,但不能防止我们实例化多个对象。
- 一个更好的解决办法是让类自身负责保存它的唯一实例。这个类可以保证没有其他实例被创建,并且它可以提供一个访问该实例的方法。这就是单例模式的模式动机。
模式结构
饿汉式单例类
懒汉式单例类
角色
- Singleton:单例
模式优点
- 提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它,并为设计及开发团队提供了共享的概念。
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
- 允许可变数目的实例。我们可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例。
模式缺点
- 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
- 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。
适用环境
- 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象。
- 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
- 在一个系统中要求一个类只有一个实例时才应当使用单例模式。反过来,如果一个类可以有几个实例共存,就需要对单例模式进行改进,使之成为多例模式。
结构型设计模式
适配器模式
模式动机
- 在适配器模式中可以定义一个包装类,包装不兼容接口的对象,这个包装类指的就是适配器(Adapter),它所包装的对象就是适配者(Adaptee),即被适配的类。
- 适配器提供客户类需要的接口,适配器的实现就是把客户类的请求转化为对适配者的相应接口的调用。也就是说:当客户类调用适配器的方法时,在适配器类的内部将调用适配者类的方法,而这个过程对客户类是透明的,客户类并不直接访问适配者类。因此,适配器可以使由于接口不兼容而不能交互的类可以一起工作。这就是适配器模式的模式动机。
模式结构
类适配器
对象适配器
双向适配器
角色
- Target 目标抽象类
- Adapter 适配器类
- Adaptee 适配者类
- Client 客户类
模式优点
- 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,而无须修改原有代码。
- 增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性。
- 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。
类适配模式优点
- 由于适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强。(重写方法)
对象适配模式优点
- 一个对象适配器可以把多个不同的适配者适配到同一个目标,也就是说,同一个适配器可以把适配者类和它的子类都适配到目标接口。
模式缺点
类适配模式缺点
- 对于Java、C#等不支持多重继承的语言,一次最多只能适配一个适配者类,而且目标抽象类只能为抽象类,不能为具体类,其使用有一定的局限性,不能将一个适配者类和它的子类都适配到目标接口。(一次extends只有一个类)
对象适配模式缺点
- 与类适配器模式相比,要想置换适配者类的方法就不容易。如果一定要置换掉适配者类的一个或多个方法,就只好先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂。
适用环境
- 系统需要使用现有的类,而这些类的接口不符合系统的需要。
- 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。
桥接模式
模式动机
- 对于有两个变化维度(即两个变化的原因)的系统,采用方案二来进行设计系统中类的个数更少,且系统扩展更为方便。设计方案二即是桥接模式的应用。桥接模式将继承关系转换为关联关系,从而降低了类与类之间的耦合,减少了代码编写量。
模式结构
角色
- Abstraction:抽象类
- RefinedAbstraction:扩充抽象类
- Implementor:实现类接口
- ConcreteImplementor:具体实现类
模式优点
- 分离抽象接口及其实现部分。
- 桥接模式有时类似于多继承方案,但是多继承方案违背了类的单一职责原则(即一个类只有一个变化的原因),复用性比较差,而且多继承结构中类的个数非常庞大,桥接模式是比多继承方案更好的解决方法。
- 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。
- 实现细节对客户透明,可以对用户隐藏实现细节。
模式缺点
- 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
- 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性。
适用环境
- 如果一个系统需要在构件的抽象化角色(整体类)和具体化(各维度)角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
- 抽象化角色和实现化角色可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
- 一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
- 虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者。
- 对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
组合模式
模式动机
- 对于树形结构,当容器对象(如文件夹)的某一个方法被调用时,将遍历整个树形结构,寻找也包含这个方法的成员对象(可以是容器对象,也可以是叶子对象,如子文件夹和文件)并调用执行。(递归调用)
- 由于容器对象和叶子对象在功能上的区别,在使用这些对象的客户端代码中必须有区别地对待容器对象和叶子对象,而实际上大多数情况下客户端希望一致地处理它们,因为对于这些对象的区别对待将会使得程序非常复杂。
- 组合模式描述了如何将容器对象和叶子对象进行递归组合,使得用户在使用时无须对它们进行区分,可以一致地对待容器对象和叶子对象,这就是组合模式的模式动机。
模式结构
角色
- Component: 抽象构件
- Leaf: 叶子构件
- Composite: 容器构件
- Client: 客户类
模式优点
- 可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,使得增加新构件也更容易。
- 客户端调用简单,客户端可以一致的使用组合结构或其中单个对象。
- 定义了包含叶子对象和容器对象的类层次结构,叶子对象可以被组合成更复杂的容器对象,而这个容器对象又可以被组合,这样不断递归下去,可以形成复杂的树形结构。
- 更容易在组合体内加入对象构件,客户端不必因为加入了新的对象构件而更改原有代码。
模式缺点
- 使设计变得更加抽象,对象的业务规则如果很复杂,则实现组合模式具有很大挑战性,而且不是所有的方法都与叶子对象子类都有关联。
- 增加新构件时可能会产生一些问题,很难对容器中的构件类型进行限制。
适用环境
- 需要表示一个对象整体或部分层次,在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,可以一致地对待它们。
- 让客户能够忽略不同对象层次的变化,客户端可以针对抽象构件编程,无须关心对象层次结构的细节。
- 对象的结构是动态的并且复杂程度不一样,但客户需要一致地处理它们。
模式扩展
透明组合模式
安全组合模式
代理模式
模式动机
- 在某些情况下,一个客户不想或者不能直接引用一个对象,此时可以通过一个称之为“代理”的第三者来实现间接引用。代理对象可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象去掉客户不能看到的内容和服务或者添加客户需要的额外服务。
- 通过引入一个新的对象(如小图片和远程代理对象Proxy)来实现对真实对象的操作或者将新的对象作为真实对象的一个替身,这种实现机制即为代理模式,通过引入代理对象来间接访问一个对象,这就是代理模式的模式动机。
模式结构
角色
- Subject: 抽象主题角色
- Proxy: 代理主题角色
- RealSubject: 真实主题角色
模式优点
- 代理模式能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。
- 远程代理使得客户端可以访问在远程机器上的对象,远程机器可能具有更好的计算性能与处理速度,可以快速响应并处理客户端请求。
- 虚拟代理通过使用一个小对象来代表一个大对象,可以减少系统资源的消耗,对系统进行优化并提高运行速度。
- 保护代理可以控制对真实对象的使用权限。
模式缺点
- 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
- 实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
适用环境
远程(Remote)代理
:为一个位于不同的地址空间的对象提供一个本地的代理对象,这个不同的地址空间可以是在同一台主机中,也可是在另一台主机中,远程代理又叫做大使(Ambassador)。虚拟(Virtual)代理
:如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。Copy-on-Write代理
:它是虚拟代理的一种,把复制(克隆)操作延迟到只有在客户端真正需要时才执行。一般来说,对象的深克隆是一个开销较大的操作,Copy-on-Write代理可以让这个操作延迟,只有对象被用到的时候才被克隆。保护(Protect or Access)代理
:控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。缓冲(Cache)代理
:为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。防火墙(Firewall)代理
:保护目标不让恶意用户接近。同步化(Synchronization)代理
:使几个用户能够同时使用一个对象而没有冲突。智能引用(Smart Reference)代理
:当一个对象被引用时,提供一些额外的操作,如将此对象被调用的次数记录下来等。
享元模式
模式动机
- 面向对象技术可以很好地解决一些灵活性或可扩展性问题,但在很多情况下需要在系统中增加类和对象的个数。当对象数量太多时,将导致运行代价过高,带来性能下降等问题。
- 享元模式正是为解决这一类问题而诞生的。享元模式通过共享技术实现相同或相似对象的重用。
- 在享元模式中可以共享的相同内容称为内部状态(Intrinsic State),而那些需要外部环境来设置的不能共享的内容称为外部状态(Extrinsic State),由于区分了内部状态和外部状态,因此可以通过设置不同的外部状态使得相同的对象可以具有一些不同的特征,而相同的内部状态是可以共享的。
- 在享元模式中通常会出现工厂模式,需要创建一个享元工厂来负责维护一个享元池(Flyweight Pool)用于存储具有相同内部状态的享元对象。
- 在享元模式中共享的是享元对象的内部状态,外部状态需要通过环境来设置。在实际使用中,能够共享的内部状态是有限的,因此享元对象一般都设计为较小的对象,它所包含的内部状态较少,这种对象也称为细粒度对象。享元模式的目的就是使用共享技术来实现大量细粒度对象的复用。
模式结构
角色
- Flyweight: 抽象享元类
- ConcreteFlyweight: 具体享元类
- UnsharedConcreteFlyweight: 非共享具体享元类
- FlyweightFactory: 享元工厂类
模式优点
- 享元模式的优点在于它可以极大减少内存中对象的数量,使得相同对象或相似对象在内存中只保存一份。
- 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享
模式缺点
- 享元模式使得系统更加复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。
- 为了使对象可以共享,享元模式需要将享元对象的状态外部化,而读取外部状态使得运行时间变长。
适用环境
- 一个系统有大量相同或者相似的对象,由于这类对象的大量使用,造成内存的大量耗费。
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
- 使用享元模式需要维护一个存储享元对象的享元池,而这需要耗费资源,因此,应当在多次重复使用享元对象时才值得使用享元模式。
装饰模式
模式动机
- 一般有两种方式可以实现给一个类或对象增加行为:
- 继承机制,使用继承机制是给现有类添加功能的一种有效途径,通过继承一个现有类可以使得子类在拥有自身方法的同时还拥有父类的方法。但是这种方法是静态的,用户不能控制增加行为的方式和时机。
- 关联机制,即将一个类的对象嵌入另一个对象中,由另一个对象来决定是否调用嵌入对象的行为以便扩展自己的行为,我们称这个嵌入的对象为装饰器(Decorator)。
- 装饰模式以对客户透明的方式动态地给一个对象附加上更多的责任,换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。这就是装饰模式的模式动机。
模式结构
角色
- Component: 抽象构件
- ConcreteComponent: 具体构件
- Decorator: 抽象装饰类
- ConcreteDecorator: 具体装饰类
模式优点
- 装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。
- 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的装饰器,从而实现不同的行为。
- 通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更为强大的对象。
- 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开闭原则”。
模式缺点
- 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度。
- 这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。
适用环境
- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
- 需要动态地给一个对象增加功能,这些功能也可以动态地被撤销。
- 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类定义不能继承(如final类)。
外观模式
模式动机
- 引入外观角色之后,用户只需要直接与外观角色交互,用户与子系统之间的复杂关系由外观角色来实现,从而降低了系统的耦合度。
模式结构
角色
- Facade: 外观角色
- SubSystem:子系统角色
模式优点
- 对客户屏蔽子系统组件,减少了客户处理的对象数目并使得子系统使用起来更加容易。通过引入外观模式,客户代码将变得很简单,与之关联的对象也很少。
- 实现了子系统与客户之间的松耦合关系,这使得子系统的组件变化不会影响到调用它的客户类,只需要调整外观类即可。
- 降低了大型软件系统中的编译依赖性,并简化了系统在不同平台之间的移植过程,因为编译一个子系统一般不需要编译所有其他的子系统。一个子系统的修改对其他子系统没有任何影响,而且子系统内部变化也不会影响到外观对象。
- 只是提供了一个访问子系统的统一入口,并不影响用户直接使用子系统类。
模式缺点
- 不能很好地限制客户使用子系统类,如果对客户访问子系统类做太多的限制则减少了可变性和灵活性。
- 在不引入抽象外观类的情况下,增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。
适用环境
- 当要为一个复杂子系统提供一个简单接口时可以使用外观模式。该接口可以满足大多数用户的需求,而且用户也可以越过外观类直接访问子系统。
- 客户程序与多个子系统之间存在很大的依赖性。引入外观类将子系统与客户以及其他子系统解耦,可以提高子系统的独立性和可移植性。
- 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度。
行为型设计模式
观察者模式(发布—订阅模式)
模式动机
- 建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应做出反应。在此,发生改变的对象称为观察目标,而被通知的对象称为观察者,一个观察目标可以对应多个观察者,而且这些观察者之间没有相互联系,可以根据需要增加和删除观察者,使得系统更易于扩展。
角色
- Subject 目标
- ConcreteSubject 具体目标
- Observer 观察者
- ConcreteObserver 具体观察者
例子
当Cat
执行cry
方法时,Mouse
与Dog
均要对Cat
发出的通知作出响应。
模式优点
- 观察者模式可以
实现表示层和数据逻辑层的分离
,并定义了稳定的消息更新传递机制,抽象了更新接口,使得可以有各种各样不同的表示层作为具体观察者角色; - 观察者模式在观察目标和观察者之
建立一个抽象的耦合
; - 支持广播通信;
- 符合“开闭原则”。
模式缺点
- 如果一个观察目标对象有很多直接和间接的观察者的话,
将所有的观察者都通知到会花费很多时间
。 - 如果在观察者和观察目标之间有
循环依赖
的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃
。 - 观察者模式没有相应的机制让观察者知道所观察的目标对象是
怎么发生变化
的,而仅仅只是知道观察目标发生了变化
。
适用环境
- 一个抽象模型有两个方面,其中
一个方面依赖于另一个方面
。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。 - 一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。
- 一个对象必须通知其他对象,而并
不知道这些对象是谁
。 - 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。这里也可以使用职责链模式。
策略模式(政策模式)
模式动机
- 完成一项任务,往往可以有多种不同的方式,每一种方式称为一个策略,我们可以根据环境或者条件的不同选择不同的策略来完成该项任务。
- 在软件开发中也常常遇到类似的情况,实现某一个功能有多个途径,此时可以使用一种设计模式来使得系统可以灵活地选择解决途径,也能够方便地增加新的解决途径。
硬编码
方式使得代码耦合度高,硬编码实现类的代码较为复杂,维护困难。- 不能在客户端中直接包含算法(策略)与策略选择代码。
- 可以定义一些独立的策略类封装具体的算法逻辑,为了保证策略的一致性,还需要定义一个抽象策略类。
模式结构
角色
- Context 环境类
- Strategy 抽象策略类
- ConcreteStrategy 具体策略类
模式优点
- 对“开闭原则”的完美支持;
- 提供了管理相关的算法族的办法;
- 提供了可以替换继承关系的办法;
- 避免使用多重条件转移语句(if-elif-elif…)。该点也算是体现了开闭原则。
模式缺点
- 客户端
必须知道所有的策略类
,并自行决定使用哪一个策略类。 - 策略模式
将造成产生很多策略类
,可以通过使用享元模式在一定程度上减少对象的数量。
适用环境
- 如果在一个系统里面有许多类,它们之间的
区别仅在于它们的行为
,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。 - 一个系统需要动态地在几种算法中选择一种。
- 如果一个对象需要用到很多的策略,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
- 不希望客户端知道复杂的、与算法相关的数据结构,在具体策略类中封装算法和相关的数据结构,提高算法的保密性与安全性。
中介者模式
模式动机
- 系统结构复杂:对象之间存在大量的相互关联和调用,若有一个对象发生变化,则需要跟踪和该对象关联的其他所有对象,并进行适当处理。
- 对象可重用性差:由于一个对象和其他对象具有很强的关联,若没有其他对象的支持,一个对象很难被另一个系统或模块重用,这些对象表现出来更像一个不可分割的整体,职责较为混乱。
- 系统扩展性低:增加一个新的对象需要在原有相关对象上增加引用,增加新的引用关系也需要调整原有对象,系统耦合度很高,对象操作很不灵活,扩展性差。
- 根据“单一职责原则”,我们应该尽量将对象细化,使其只负责或呈现单一的职责。
- 为了减少对象两两之间复杂的引用关系,使之成为一个松耦合的系统,我们需要使用中介者模式。
模式结构
角色
- Mediator 抽象中介者
- ConcreteMediator 具体中介者
- Colleague 抽象同事类
- ConcreteColleague 具体同事类
体现的设计原则
迪米特法则
- 单一职责原则
- 开闭原则
- 依赖倒转原则
模式优点
- 简化了对象之间的交互;
- 将各同事解耦;
- 减少子类生成;
- 可以简化各同事类的设计和实现。
模式缺点
- 具体中介者类包含了同事之间的交互细节,可能会导致具体中介者类非常复杂,使得系统难以维护。
适用环境
- 对象之间存在复杂的引用关系,产生的相互依赖关系结构混乱且难以理解;
- 一个对象的引用了许多其他对象且与这些对象直接通信,导致难以复用该对象;
- 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。可以通过引入中介者类来实现,在中介者中定义对象交互的公共行为,如果需要改变行为则可以增加新的中介者类。
职责链模式
模式动机
- 职责链可以是一条直线、一个环或者一个树形结构,最常见的职责链是直线型,即沿着一条单向的链来传递请求。
- 链上的每一个对象都是请求处理者,职责链模式可以将请求的处理者组织成一条链,并使请求沿着链传递,由链上的处理者对请求进行相应的处理,客户端无须关心请求的处理细节以及请求的传递,只需将请求发送到链上即可,
将请求的发送者和请求的处理者解耦
。
模式结构
角色
- Handler 抽象处理者
- ConcreteHandler 具体处理者
- Client 客户类
模式优点
- 降低耦合度;
- 可简化对象的相互连接;
- 增强给对象指派职责的灵活性;
- 增加新的请求处理类很方便。
模式缺点
- 不能保证请求一定被接收。
- 系统性能将受到一定影响,而且在进行代码调试时不太方便;可能会造成循环调用。
适用环境
- 有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。
- 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
- 可动态指定一组对象处理请求。
状态模式
模式动机
- 在很多情况下,一个对象的行为取决于一个或多个动态变化的属性,这样的属性叫做状态,这样的对象叫做有状态的 (stateful)对象,这样的对象状态是从事先定义好的一系列值中取出的。当一个这样的对象与外部事件产生互动时,其内部状态就会改变,从而使得系统的行为也随之发生变化。
- 在UML中可以使用状态图来描述对象状态的变化。
模式结构
角色
- Context 环境类
- State 抽象状态类
- ConcreteState 具体状态类
模式优点
- 封装了转换规则。
- 枚举可能的状态,在枚举状态之前需要确定状态种类。
- 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
- 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
- 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
模式缺点
- 状态模式的使用必然会增加系统类和对象的个数。
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
- 状态模式对“开闭原则”的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码。
适用环境
- 对象的行为依赖于它的状态(属性)并且可以根据它的状态改变而改变它的相关行为。
- 代码中包含大量与对象状态有关的条件语句,这些条件语句的出现,会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,使客户类与类库之间的耦合增强。在这些条件语句中包含了对象的行为,而且这些条件对应于对象的各种状态。
模式扩展
- 简单状态模式。类似策略模式,具体状态类与环境类之间没有关联,每个状态类之间独立,符合开闭原则;
- 可切换状态的状态模式。在具体状态类内部需要调用环境类
Context
的setState()
方法进行状态的转换操作,不符合开闭原则。
命令模式
模式动机
- 在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用命令模式来进行设计,使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活。
- 命令模式可以对发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。这就是命令模式的模式动机。
模式结构
角色
- Command 抽象命令类
- ConcreteCommand 具体命令类
- Invoker 调用者
- Receiver 接收者
- Client 客户类
模式优点
- 降低系统的耦合度。
- 新的命令可以很容易地加入到系统中。
- 可以比较容易地设计一个命令队列和宏命令(组合命令)。
- 可以方便地实现对请求的Undo和Redo。
模式缺点
- 使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个命令都需要设计一个具体命令类,因此某些系统可能需要大量具体命令类,这将影响命令模式的使用。
适用环境
- 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
- 系统需要在不同的时间指定请求、将请求排队和执行请求。
- 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
- 系统需要将一组操作组合在一起,即支持宏命令。
解释器模式
模式动机
- 如果在系统中某一特定类型的问题发生的频率很高,此时可以考虑将这些问题的实例表述为一个语言中的句子,因此可以构建一个解释器,该解释器通过解释这些句子来解决这些问题。
- 解释器模式描述了如何构成一个简单的语言解释器,主要应用在使用面向对象语言开发的编译器中。
模式结构
角色
- AbstractExpression 抽象表达式
- TerminalExpression 终结符表达式
- NonterminalExpression 非终结符表达式
- Context 环境类
- Client 客户类
模式优点
- 易于改变和扩展文法。
- 易于实现文法。
- 增加了新的解释表达式的方式。
模式缺点
- 对于复杂文法难以维护。
- 执行效率较低。
- 应用场景很有限。
适用环境
- 可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。
- 一些重复出现的问题可以用一种简单的语言来进行表达。
- 文法较为简单。
- 效率不是关键问题。
备忘录模式
模式动机
- 为了使软件的使用更加人性化,对于误操作,我们需要提供一种类似“后悔药”的机制,让软件系统可以回到误操作前的状态,因此需要保存用户每一次操作时系统的状态,一旦出现误操作,可以把存储的历史状态取出即可回到之前的状态。
- 现在大多数软件都有撤销(Undo)的功能,快捷键一般都是Ctrl+Z,目的就是为了解决这个后悔的问题。
- 在应用软件的开发过程中,很多时候我们都需要记录一个对象的内部状态。
- 在具体实现过程中,为了允许用户取消不确定的操作或从错误中恢复过来,需要实现备份点和撤销机制,而要实现这些机制,必须事先将状态信息保存在某处,这样才能将对象恢复到它们原先的状态。
- 备忘录模式是一种给我们的软件提供后悔药的机制,通过它可以使系统恢复到某一特定的历史状态。
模式结构
角色
- Originator 发起人
- Memento 备忘录
- Caretaker 管理者
Java代码
public class Originator {
private String state;
public Originator(){}
// 创建一个备忘录对象
public Memento createMemento(){
return new Memento(this);
}
// 根据备忘录对象恢复原发器状态
public void restoreMemento(Memento m){
state = m.state;
}
public void setState(String state)
{
this.state=state;
}
public String getState()
{
return this.state;
}
}
class Memento {
private String state;
public Memento(Originator o){
state = o.state;
}
public void setState(String state)
{
this.state=state;
}
public String getState()
{
return this.state;
}
}
public class Caretaker
{
private Memento memento;
public Memento getMemento()
{
return memento;
}
public void setMemento(Memento memento)
{
this.memento=memento;
}
}
模式优点
- 提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用先前存储起来的备忘录将状态复原。
- 实现了信息的封装,一个备忘录对象是一种原发器对象的表示,不会被其他代码改动,这种模式简化了原发器对象,备忘录只保存原发器的状态,采用堆栈来存储备忘录对象可以实现多次撤销操作,可以通过在负责人中定义集合对象来存储多个备忘录。
模式缺点
- 资源消耗过大,如果类的成员变量太多,就不可避免占用大量的内存,而且每保存一次对象的状态都需要消耗内存资源,如果知道这一点大家就容易理解为什么一些提供了撤销功能的软件在运行时所需的内存和硬盘空间比较大了。
适用环境
- 保存一个对象在某一个时刻的状态或部分状态,这样以后需要时它能够恢复到先前的状态。
- 如果用一个接口来让其他对象得到这些状态,将会暴露对象的实现细节并破坏对象的封装性,一个对象不希望外界直接访问其内部状态,通过负责人可以间接访问其内部状态。
迭代器模式
模式动机
- 一个聚合对象,如一个列表(List)或者一个集合(Set),应该提供一种方法来让别人可以访问它的元素,而又不需要暴露它的内部结构。
- 针对不同的需要,可能还要以不同的方式遍历整个聚合对象,但是我们并不希望在聚合对象的抽象层接口中充斥着各种不同遍历的操作。
- 怎样遍历一个聚合对象,又不需要了解聚合对象的内部结构,还能够提供多种不同的遍历方式,这就是迭代器模式所要解决的问题。
模式结构
角色
- Iterator: 抽象迭代器
- ConcreteIterator: 具体迭代器
- Aggregate: 抽象聚合类
- ConcreteAggregate: 具体聚合类
模式优点
- 它支持以不同的方式遍历一个聚合对象。
- 迭代器简化了聚合类。
- 在同一个聚合上可以有多个遍历。
- 在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码,满足“开闭原则”的要求。
模式缺点
- 由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性
适用环境
- 访问一个聚合对象的内容而无须暴露它的内部表示。
- 需要为聚合对象提供多种遍历方式。
- 为遍历不同的聚合结构提供一个统一的接口。
模板方法模式
模式动机
- 模板方法模式是基于继承的代码复用基本技术,模板方法模式的结构和用法也是面向对象设计的核心之一。在模板方法模式中,可以将相同的代码放在父类中,而将不同的方法实现放在不同的子类中。
- 在模板方法模式中,我们需要准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来让子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现,这就是模板方法模式的用意。模板方法模式体现了面向对象的诸多重要思想,是一种使用频率较高的模式。
模式结构
角色
- AbstractClass: 抽象类
- ConcreteClass: 具体子类
模式优点
- 模板方法模式在一个类中形式化地定义算法,而由它的子类实现细节的处理。
- 模板方法模式是一种代码复用的基本技术。
- 模板方法模式导致一种反向的控制结构,通过一个父类调用其子类的操作,通过对子类的扩展增加新的行为,符合“开闭原则”
模式缺点
- 每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象,但是更加符合“单一职责原则”,使得类的内聚性得以提高。
适用环境
- 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。
- 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。
- 对一些复杂的算法进行分割,将其算法中固定不变的部分设计为模板方法和父类具体方法,而一些可以改变的细节由其子类来实现。
- 控制子类的扩展。
访问者模式
模式动机
- 在实际使用时,对同一集合对象的操作并不是唯一的,对相同的元素对象可能存在多种不同的操作方式。
- 而且这些操作方式并不稳定,可能还需要增加新的操作,以满足新的业务需求。
- 此时,访问者模式就是一个值得考虑的解决方案。
- 访问者模式的目的是封装一些施加于某种数据结构元素之上的操作,一旦这些操作需要修改的话,接受这个操作的数据结构可以保持不变。为不同类型的元素提供多种访问操作方式,且可以在不修改原有系统的情况下增加新的操作方式,这就是访问者模式的模式动机。
模式结构
角色
- Vistor: 抽象访问者
- ConcreteVisitor: 具体访问者
- Element: 抽象元素
- ConcreteElement: 具体元素
- ObjectStructure: 对象结构
模式优点
- 使得增加新的访问操作变得很容易,能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
- 将有关元素对象的访问行为集中到一个访问者对象中,而不是分散到一个个的元素类中。
- 可以跨过类的等级结构访问属于不同的等级结构的元素类。
- 让用户能够在不修改现有类层次结构的情况下,定义该类层次结构的操作。
模式缺点
- 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作,违背了“开闭原则”的要求。
- 破坏封装。访问者模式要求访问者对象访问并调用每一个元素对象的操作,这意味着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法供访问者访问。
适用环境
- 一个对象结构包含很多类型的对象,希望对这些对象实施一些依赖其具体类型的操作。在访问者中针对每一种具体的类型都提供了一个访问操作,不同类型的对象可以有不同的访问操作。
- 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。访问者模式使得我们可以将相关的访问操作集中起来定义在访问者类中,对象结构可以被多个不同的访问者类所使用,将对象本身与对象的访问操作分离。
- 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。