设计模式精简图册
首发于我的公众号
设计模式图册
设计模式分类
创建型模式:
主要用于创建对象,包括
- 工厂方法(Factory Method)
- 抽象工厂(Abstract Factory)
- 单例(Singleton)
- 生成器(Builder)
- 原型(Prototype)
结构型模式:
用于处理类或者对象的组合,包括
- 适配器(Adapter)
- 装饰者(Decorator)
- 代理(Proxy)
- 外观(Facade)
- 桥接(Bridge Pattern)
- 组合(Composite)
- 轻量(Flyweigh)
行为型模式:
用于描述类与对象怎样的交互和分配职责,包括
- 策略(Strategy)
- 观察者(Observer)
- 命令(Command)
- 模板方法(Template Method)
- 迭代器(Iterator)
- 状态(State)
- 责任链(Chain)
- 解释器(Interpreter)
- 中介者(Mediator)
- 备忘录(Memo)
- 访问者(Visitor)
设计原则
单一职责原则(Single responsibility principle)
- 核心 不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责。
- 问题产生 类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。
- 解决方案 遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。
但是由于职责扩散会导致在实际中往往会有悖于单一职责
里氏代换原则(Liskov Substitution Principle LSP)
- 核心 所有引用基类的地方必须能透明地使用其子类的对象。
- 问题产生 有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。
- 解决方案 当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。
继承包含这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。
继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加了对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能会产生故障。
接口隔离原则(Interface Segregation Principle)
- 核心 不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
- 问题产生 类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。
- 解决方案 将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。
接口隔离原则跟之前的单一职责原则很相似,其实不然。其一,单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。其二,单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口接口,主要针对抽象,针对程序整体框架的构建。
采用接口隔离原则对接口进行约束时,要注意以下几点:
- 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
- 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。
迪米特原则(Law of Demeter/Least Knowledge Principle )
- 核心 迪米特法则又叫最少知道原则,一个对象应该对其他对象保持最少的了解。
- 问题产生 类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
- 解决方案 尽量降低类与类之间的耦合。
通俗的来讲,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息, 迪米特法则还有一个更简单的定义:只与直接的朋友通信。首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。
依赖倒置原则(Dependence Inversion Principle)
- 核心 高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。
- 问题产生 类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。
- 解决方案 将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。
依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。在java中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。
依赖倒置原则的核心思想是面向接口编程,
开闭原则(Open Close Principle)
- 核心 一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
- 问题产生 在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。
- 解决方案 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行。
几个原则的关联性
用抽象构建框架,用实现扩展细节的注意事项而已:
- 单一职责原则告诉我们实现类要职责单一;
- 里氏替换原则告诉我们不要破坏继承体系;
- 依赖倒置原则告诉我们要面向接口编程;
- 接口隔离原则告诉我们在设计接口的时候要精简单一;
- 迪米特法则告诉我们要降低耦合。 而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。
创建型设计模式(创建对象)
工厂方法(Factory Method Pattern)
名称 | Factory Method |
结构 | |
动机 | 定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method 使一个类的实例化延迟到其子类。 |
适用性 | - 当一个类不知道它所必须创建的对象的类的时候。
- 当一个类希望由它的子类来指定它所创建的对象的时候。
- 当类将创建对象的职责委托给多个帮助子类中的某一个,并且你希望将哪一个帮助子类是代理者这一信息局部化的时候。
|
优点 | - 在工厂方法中,用户只需要知道所要产品的具体工厂,无须关系具体的创建过程,甚至不需要具体产品类的类名。
- 在系统增加新的产品时,我们只需要添加一个具体产品类和对应的实现工厂,无需对原工厂进行任何修改,很好地符合了“开闭原则”。
|
缺点 | - 每次增加一个产品时,都需要增加一个具体类和对象实现工厂,是的系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。
|
小结 | - 简单工厂模式的要点就在于当你需要什么,只需要传入一个正确的参数,就可以获取你所需要的对象,而无须知道其创建细节。
- 简单工厂模式最大的优点在于实现对象的创建和对象的使用分离,但是如果产品过多时,会导致工厂代码非常复杂。
|
抽象工厂(Abstract Factory Pattern)
名称 | Abstract Factory |
结构 | |
动机 | 提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。 |
适用性 | - 一个系统要独立于它的产品的创建、组合和表示时。
- 一个系统要由多个产品系列中的一个来配置时。
- 当你要强调一系列相关的产品对象的设计以便进行联合使用时。
- 当你提供一个产品类库,而只想显示它们的接口而不是实现时。
|
优点 | - 抽象工厂隔离了具体类的生成,是的客户端不需要知道什么被创建。所有的具体工厂都实现了抽象工厂中定义的公共接口,因此只需要改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。
- 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。
|
缺点 | - 添加新的行为时比较麻烦。如果需要添加一个新产品族对象时,需要更改接口及其下所有子类,这必然会带来很大的麻烦。
|
小结 | - 抽象工厂模式中主要的优点在于具体类的隔离,是的客户端不需要知道什么被创建了。其缺点在于增加新的等级产品结构比较复杂,需要修改接口及其所有子类。
|
生成器/建造者模式(Builder Pattern)
名称 | Builder |
结构 | |
动机 | 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。 |
适用性 | - 当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时。
- 当构造过程必须允许被构造的对象有不同的表示时。
|
优点 | - 将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,使得我们能够更加精确的控制复杂对象的产生过程。
- 将产品的创建过程与产品本身分离开来,可以使用相同的创建过程来得到不同的产品。也就说细节依赖抽象。
- 每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象。
|
缺点 | - 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
- 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。
|
小结 | - 建造者模式是将一个复杂对象的创建过程给封装起来,客户只需要知道可以利用对象名或者类型就能够得到一个完整的对象实例,而不需要关心对象的具体创建过程。
- 建造者模式将对象的创建过程与对象本身隔离开了,使得细节依赖于抽象,符合依赖倒置原则。可以使用相同的创建过程来创建不同的产品对象。
|
原型模式(Prototype Pattern)
名称 | Prototype |
结构 | |
动机 | 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。 |
适用性 | - 当要实例化的类是在运行时刻指定时,例如,通过动态装载;或者
- 为了避免创建一个与产品类层次平行的工厂类层次时;或者
- 当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。
|
优点 | - 如果创建新的对象比较复杂时,可以利用原型模式简化对象的创建过程,同时也能够提高效率。
- 可以使用深克隆保持对象的状态。
- 原型模式提供了简化的创建结构。
|
缺点 | - 在实现深克隆的时候可能需要比较复杂的代码。
- 需要为每一个类配备一个克隆方法,而且这个克隆方法需要对类的功能进行通盘考虑,这对全新的类来说不是很难,但对已有的类进行改造时,不一定是件容易的事,必须修改其源代码,违背了“开闭原则”。
|
小结 | - 原型模式向客户隐藏了创建对象的复杂性。客户只需要知道要创建对象的类型,然后通过请求就可以获得和该对象一模一样的新对象,无须知道具体的创建过程。
- 克隆分为浅克隆和深克隆两种。
- 我们虽然可以利用原型模式来获得一个新对象,但有时对象的复制可能会相当的复杂,比如深克隆。
|
单例模式(Singleton Pattern)
名称 | Singleton |
结构 | |
动机 | 保证一个类仅有一个实例,并提供一个访问它的全局访问点。 |
适用性 | - 当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时。
- 当这个唯一实例应该是通过子类化可扩展的,并且客户应该无需更改代码就能使用一个扩展的实例时。
|
优点 | - 节约了系统资源。由于系统中只存在一个实例对象,对与一些需要频繁创建和销毁对象的系统而言,单 例模式无疑节约了系统资源和提高了系统的性能。
- 因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
|
缺点 | - 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。
|
小结 | - 单例模式中确保程序中一个类最多只有一个实例。
- 单例模式的构造器是私有了,而且它必须要提供实例的全局访问点。
- 单例模式可能会因为多线程的问题而带来安全隐患。
|
public class BetterSingleton {
private BetterSingleton2() {
}
public static BetterSingleton getInstance() {
return Singleton.BETTER_SINGLETON;
}
public static class Singleton{
private static final BetterSingleton BETTER_SINGLETON = new BetterSingleton();
}
}
复制代码
创建型设计模式小结
模式 | 场景发散 | 一句话概括 |
---|
工厂方法(Factory Method) | new太多如何管理 | 生产系列产品。 |
抽象工厂(Abstract Factory) | new太多如何管理 | 一次生产多个不同产品。 |
生成器(Builder) | 车手选车 | 生产有很多组件的产品。 |
原型(Prototype) | 复制不能很难 | 克隆对象。 |
单件(Singleton) | 如何管理全局信息 | 全局只有一个。 |
结构型设计模式(处理类或者对象的组合)
桥接模式(Bridge Pattern)
名称 | Bridge |
结构 | |
动机 | 将抽象部分与它的实现部分分离,使它们都可以独立地变化。 |
适用性 | - 如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
- 对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用
- 一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
|
优点 | - 分离抽象接口及其实现部分。提高了比继承更好的解决方案。
- 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。
- 实现细节对客户透明,可以对用户隐藏实现细节。
|
缺点 | - 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
- 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性。
|
小结 | - 桥接模式实现了抽象化与实现化的脱耦。他们两个互相独立,不会影响到对方。
- 对于两个独立变化的维度,使用桥接模式再适合不过了。
- 对于“具体的抽象类”所做的改变,是不会影响到客户。
|
轻量模式/享元模式(FlyWeightPattern)
名称 | Flyweight |
结构 | |
动机 | 运用共享技术有效地支持大量细粒度的对象。 |
适用性 | - 一个应用程序使用了大量的对象。
- 完全由于使用大量的对象,造成很大的存储开销。
- 对象的大多数状态都可变为外部状态。
- 如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象。
- 应用程序不依赖于对象标识。由于F l y w e i g h t 对象可以被共享,对于概念上明显有别的对象,标识测试将返回真值。
|
优点 | - 享元模式的优点在于它能够极大的减少系统中对象的个数。
- 享元模式由于使用了外部状态,外部状态相对独立,不会影响到内部状态,所以享元模式使得享元对象能够在不同的环境被共享。
|
缺点 | - 由于享元模式需要区分外部状态和内部状态,使得应用程序在某种程度上来说更加复杂化了。
- 为了使对象可以共享,享元模式需要将享元对象的状态外部化,而读取外部状态使得运行时间变长。
|
小结 | - 享元模式可以极大地减少系统中对象的数量。但是它可能会引起系统的逻辑更加复杂化。
- 享元模式的核心在于享元工厂,它主要用来确保合理地共享享元对象。
- 内部状态为不变共享部分,存储于享元享元对象内部,而外部状态是可变部分,它应当由客户端来负责。
|
外观模式(Facade Pattern)
名称 | Facade |
结构 | |
动机 | 为子系统中的一组接口提供一个一致的界面,F a c a d e 模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。 |
适用性 | - 当你要为一个复杂子系统提供一个简单接口时。子系统往往因为不断演化而变得越来越复杂。大多数模式使用时都会产生更多更小的类。这使得子系统更具可重用性,也更容易对子系统进行定制,但这也给那些不需要定制子系统的用户带来一些使用上的困难。F a c a d e 可以提供一个简单的缺省视图,这一视图对大多数用户来说已经足够,而那些需要更多的可定制性的用户可以越过f a c a d e 层。
- 客户程序与抽象类的实现部分之间存在着很大的依赖性。引入f a c a d e 将这个子系统与客户以及其他的子系统分离,可以提高子系统的独立性和可移植性。
- 当你需要构建一个层次结构的子系统时,使用f a c a d e 模式定义子系统中每层的入口点。如果子系统之间是相互依赖的,你可以让它们仅通过f a c a d e 进行通讯,从而简化了它们之间的依赖关系。
|
优点 | - 引入外观模式,是客户对子系统的使用变得简单了,减少了与子系统的关联对象,实现了子系统与客户之间的松耦合关系。
- 只是提供了一个访问子系统的统一入口,并不影响用户直接使用子系统类
- 降低了大型软件系统中的编译依赖性,并简化了系统在不同平台之间的移植过程
|
缺点 | - 不能很好地限制客户使用子系统类,如果对客户访问子系统类做太多的限制则减少了可变性和灵活性
- 在不引入抽象外观类的情况下,增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”
|
小结 | - 外观模式的主要优点就在于减少了客户与子系统之间的关联对象,使用客户对子系统的使用变得简单了,也实现了客户端与子系统之间的松耦合关系。它的缺点就在于违背了“开闭原则”。
- 如果需要实现一个外观模式,需要将子系统组合进外观中,然后将工作委托给子系统执行。
|
装饰者模式(Decorator Pattern)
名称 | Decorator |
结构 | |
动机 | 动态地给一个对象添加一些额外的职责。就增加功能来说,D e c o r a t o r 模式相比生成子类更为灵活。 |
适用性 | - 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
- 处理那些可以撤消的职责。
- 当不能采用生成子类的方法进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。
|
优点 | - 装饰者模式可以提供比继承更多的灵活性
- 可以通过一种动态的方式来扩展一个对象的功能,在运行时选择不同的装饰器,从而实现不同的行为。
- 通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更为强大的对象。
- 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开闭原则”。
|
缺点 | - 会产生很多的小对象,增加了系统的复杂性
- 这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。
|
小结 | - 装饰者模式意味着一群装饰者类,这些类用来包装具体组件
- 装饰者可以在被装饰者的行为前面或者后面加上自己的行为,甚至可以将被装饰者的行为整个取代掉,从而达到特定的目的。
- 可以用多个装饰者包装一个组件。
- 装饰者一般对于组件的客户是透明的,除非客户程序依赖于组件的具体类型。
- 装饰者会导致设计中出现许多的小对象,如果过度的使用,会让系统变得更加复杂。
- 装饰者和被装饰者对象有相同的超类型。
|
组合模式(Composite Pattern)
名称 | Composite |
结构 | |
动机 | 将对象组合成树形结构以表示?部分-整体?的层次结构。C o m p o s i t e 使得用户对单个对象和组合对象的使用具有一致性。 |
适用性 | - 你想表示对象的部分-整体层次结构。
- 你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。
|
优点 | - 可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,使得增加新构件也更容易。
- 客户端调用简单,客户端可以一致的使用组合结构或其中单个对象。
- 定义了包含叶子对象和容器对象的类层次结构,叶子对象可以被组合成更复杂的容器对象,而这个容器对象又可以被组合,这样不断递归下去,可以形成复杂的树形结构。
- 更容易在组合体内加入对象构件,客户端不必因为加入了新的对象构件而更改原有代码。
|
缺点 | - 使设计变得更加抽象,对象的业务规则如果很复杂,则实现组合模式具有很大挑战性,而且不是所有的方法都与叶子对象子类都有关联,会有冗余代码
|
小结 | - 组合模式用于将多个对象组合成树形结构以表示“整体-部分”的结构层次。组合模式对单个对象(叶子对象)和组合对象(容器对象)的使用具有一致性。
- 组合对象的关键在于它定义了一个抽象构建类,它既可表示叶子对象,也可表示容器对象,客户仅仅需要针对这个抽象构建进行编程,无须知道他是叶子对象还是容器对象,都是一致对待。
- 组合模式虽然能够非常好地处理层次结构,也使得客户端程序变得简单,但是它也使得设计变得更加抽象,而且也很难对容器中的构件类型进行限制,这会导致在增加新的构件时会产生一些问题。
|
代理模式(Proxy Pattern)
名称 | Proxy |
结构 | |
动机 | 为其他对象提供一种代理以控制对这个对象的访问。 |
适用性 | - 不希望某些类被直接访问。
- 访问之前希望先进行一些预处理。
- 希望对被访问的对象进行内存、权限等方面的控制。 模式。
|
优点 | - 代理模式能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。
- 代理对象可以在客户端和目标对象之间起到中介的作用,这样起到了的作用和保护了目标对象的
|
缺点 | - 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
- 实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
|
小结 | - 代理模式是通过使用引用代理对象来访问真实对象,在这里代理对象充当用于连接客户端和真实对象的中介者。
- 代理模式主要用于远程代理、虚拟代理和保护代理。其中保护代理可以进行访问权限控制。
|
适配器模式(Adapter Pattern)
名称 | Adapter |
结构 | <div data-type="image" data-display="block" data-align="center" data-src="http://www.uml.org.cn/chanpin/intro/WebHelp/Adapter_Class.gif" data-width="543">
<img src="http://www.uml.org.cn/chanpin/intro/WebHelp/Adapter_Class.gif" width="543" />
</div>
</div>
</td>
</tr>
<tr>
<td rowspan="1" colSpan="1">
<div data-type="p">动机</div>
</td>
<td rowspan="1" colSpan="1">
<div data-type="p">将一个类的接口转换成客户希望的另外一个接口。Adapter 模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。</div>
</td>
</tr>
<tr>
<td rowspan="1" colSpan="1">
<div data-type="p">适用性</div>
</td>
<td rowspan="1" colSpan="1">
<ul data-type="unordered-list">
<li data-type="list-item" data-list-type="unordered-list">
<div data-type="p">你想使用一个已经存在的类,而它的接口不符合你的需求。</div>
</li>
<li data-type="list-item" data-list-type="unordered-list">
<div data-type="p">你想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类(即那些接口可能不一定兼容的类)协同工作。</div>
</li>
<li data-type="list-item" data-list-type="unordered-list">
<div data-type="p">(仅适用于对象Adapter )你想使用一些已经存在的子类,但是不可能对每一个都进行子类化以匹配它们的接口。对象适配器可以适配它的父类接口。</div>
</li>
</ul>
</td>
</tr>
<tr height="34px">
<td rowspan="1" colSpan="1">
<div data-type="p">优点</div>
</td>
<td rowspan="1" colSpan="1">
<ul data-type="unordered-list">
<li data-type="list-item" data-list-type="unordered-list">
<div data-type="p">将目标类和适配者类解耦,通过使用适配器让不兼容的接口变成了兼容,让客户从实现的接口解耦。</div>
</li>
<li data-type="list-item" data-list-type="unordered-list">
<div data-type="p">增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性。</div>
</li>
<li data-type="list-item" data-list-type="unordered-list">
<div data-type="p">灵活性和扩展性都非常好在不修改原有代码的基础上增加新的适配器类,符合“开闭原则”。</div>
</li>
</ul>
</td>
</tr>
<tr height="34px">
<td rowspan="1" colSpan="1">
<div data-type="p">缺点</div>
</td>
<td rowspan="1" colSpan="1">
<ul data-type="unordered-list">
<li data-type="list-item" data-list-type="unordered-list">
<div data-type="p">过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。</div>
</li>
<li data-type="list-item" data-list-type="unordered-list">
<div data-type="p">因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。</div>
</li>
</ul>
</td>
</tr>
<tr height="34px">
<td rowspan="1" colSpan="1">
<div data-type="p">小结</div>
</td>
<td rowspan="1" colSpan="1">
<ul data-type="unordered-list">
<li data-type="list-item" data-list-type="unordered-list">
<div data-type="p">当我们需要使用的一个现有的类,但是他的接口并不符合我们的需求时,我们可以使用适配器模式。</div>
</li>
<li data-type="list-item" data-list-type="unordered-list">
<div data-type="p">适配器模式分为类适配器和对象适配器,其中类适配器需要用到多重继承。</div>
</li>
</ul>
</td>
</tr>
</tbody>
复制代码
|
结构型设计模式小结
模式 | 场景发散 | 一句话说明 |
---|
桥(Bridge) | 麻烦的日志记录 | 将“抽象”和“实现”自由搭配。 |
轻量(Flyweight) | 森林里的树太多了 | 轻松地处理“大量”对象。 |
外观(Façade) | 超级手机 | 同时提供简单接口和复杂接口。 |
装饰者(Decorator) | 星巴克的饮料计较系统 | 不改变接口但要增强功能。 |
组合(Composite) | 超酷的绘图软件 | 不管你是老子还是儿子,都一样处理。 |
代理(Proxy) | 找中介租房 | 代理要控制你的访问,同时让你的访问更舒服 。 |
适配器(Adapter) | 老掉牙系统的重生 | 不改变功能但要改变接口 |
行为型设计模式(类与对象怎样的交互和分配职责)
观察者模式(Observer Pattern)
名称 | Observer |
结构 | |
动机 | 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时, 所有依赖于它的对象都得到通知并被自动更新。 |
适用性 | - 当一个抽象模型有两个方面, 其中一个方面依赖于另一方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。
- 当对一个对象的改变需要同时改变其它对象, 而不知道具体有多少对象有待改变。
- 当一个对象必须通知其它对象,而它又不能假定其它对象是谁。换言之, 你不希望这些对象是紧密耦合的。
|
优点 | - 当两个对象之间送耦合,他们依然可以交互,但是不太清楚彼此的细节。观察者模式提供了一种对象设计,让主题和观察者之间送耦合。主题所知道只是一个具体的观察者列表,每一个具体观察者都符合一个抽象观察者的接口。主题并不认识任何一个具体的观察者,它只知道他们都有一个共同的接口。
- 观察者模式支持“广播通信”。主题会向所有的观察者发出通知。
- 观察者模式符合“开闭原则”的要求。
|
缺点 | - 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
- 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进 行循环调用,可能导致系统崩溃。
- 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
|
小结 | - 观察者模式定义了对象之间的一对多关系。多个观察者监听同一个被观察者,当该被观察者的状态发生改变时,会通知所有的观察者。
- 观察者模式中包含四个角色。主题,它指被观察的对象。具体主题是主题子类,通常它包含有经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知;观察者,将对观察主题的改变做出反应;具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致。
|
策略模式(Strategy Pattern)
名称 | Strategy |
结构 | |
动机 | 定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。 |
适用性 | - 许多相关的类仅仅是行为有异。策略模式提供了一种用多个行为中的一个行为来配置一个类的方法。
- 需要使用一个算法的不同变体。
- 算法使用客户不应该知道的数据。可使用策略模式以避免暴露复杂的、与算法相关的数据结构。
- 一个类定义了多种行为, 并且这些行为在这个类的操作中以多个条件语句的形式出现。将相关的条件分支移入它们各自的S t r a t e g y 类中以代替这些条件语句。
|
优点 | - 策略模式提供了对“开闭原则”的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。
- 策略模式提供了可以替换继承关系的办法。
- 使用策略模式可以避免使用多重条件转移语句。
|
缺点 | - 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
- 策略模式将造成产生很多策略类,
|
小结 | - 如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
- 一个系统需要动态地在几种算法中选择一种。
- 如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
|
迭代器模式(Iterator Pattern)
名称 | Iterator |
结构 | |
动机 | 提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。 |
适用性 | - 访问一个聚合对象的内容而无需暴露它的内部表示。
- 支持对聚合对象的多种遍历。
- 为遍历不同的聚合结构提供一个统一的接口(即, 支持多态迭代)。
|
优点 | - 它支持以不同的方式遍历一个聚合对象。
- 迭代器简化了聚合类。
- 在同一个聚合上可以有多个遍历。
- 在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码。
|
缺点 | - 由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
|
小结 | - 将遍历聚合对象中数据的行为提取出来,封装到一个迭代器中,通过专门的迭代器来遍历聚合对象的内部数据,这就是迭代器模式的本质。迭代器模式是“单一职责原则”的完美体现。
- 当使用迭代器的时候,我们依赖聚合提供遍历。
- 迭代器提供了一个通用的接口,让我们遍历聚合的项,放我们编码使用聚合项时,就可以使用多态机制。
|
命令模式(Command Pattern)
名称 | Command |
结构 | |
动机 | 将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。 |
适用性 | - 抽象出待执行的动作以参数化某对象,你可用过程语言中的回调(c a l l b a c k )函数表达这种参数化机制。所谓回调函数是指函数先在某处注册,而它将在稍后某个需要的时候被调用。C o m m a n d 模式是回调机制的一个面向对象的替代品。
- 在不同的时刻指定、排列和执行请求。一个C o m m a n d 对象可以有一个与初始请求无关的生存期。如果一个请求的接收者可用一种与地址空间无关的方式表达,那么就可将负责该请求的命令对象传送给另一个不同的进程并在那儿实现该请求。
- 支持取消操作。C o m m a n d 的E x c u t e 操作可在实施操作前将状态存储起来,在取消操作时这个状态用来消除该操作的影响。C o m m a n d 接口必须添加一个U n e x e c u t e 操作,该操作取消上一次E x e c u t e 调用的效果。执行的命令被存储在一个历史列表中。可通过向后和向前遍历这一列表并分别调用U n e x e c u t e 和E x e c u t e 来实现重数不限的取消和重做。
- 支持修改日志,这样当系统崩溃时,这些修改可以被重做一遍。在C o m m a n d 接口中添加装载操作和存储操作,可以用来保持变动的一个一致的修改日志。从崩溃中恢复的过程包括从磁盘中重新读入记录下来的命令并用E x e c u t e 操作重新执行它们。
- 用构建在原语操作上的高层操作构造一个系统。这样一种结构在支持事务( t r a n s a c t i o n )的信息系统中很常见。一个事务封装了对数据的一组变动。C o m m a n d 模式提供了对事务进行建模的方法。C o m m a n d 有一个公共的接口,使得你可以用同一种方式调用所有的事务。同时使用该模式也易于添加新事务以扩展系统。
|
优点 | - 降低了系统耦合度
- 新的命令可以很容易添加到系统中去。
|
缺点 | - 使用命令模式可能会导致某些系统有过多的具体命令类。
|
小结 | - 命令模式的本质就是将命令对象进行封装打包,将发出命令的责任和执行命令的责任进行割开。
- 命令模式中发送者只需要知道如何发送请求命令,无须关心命令执行具体过程。
- 在发送者和接收者两者间是通过命令对象进行沟通的。请求命令本身就当做一个对象在两者间进行传递,它封装了接收者和一组动作。
- 命令模式支持撤销。
- 命令模式队列请求和日志请求。
|
访问者模式(Visitor Pattern)
名称 | Visitor |
结构 | |
动机 | 表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。 |
适用性 | - 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作。
- 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而你想避免让这些操作?污染?这些对象的类。Vi s i t o r 使得你可以将相关的操作集中起来定义在一个类中。当该对象结构被很多应用共享时,用Vi s i t o r 模式让每个应用仅包含需要用到的操作。
- 定义对象结构的类很少改变,但经常需要在此结构上定义新的操作。改变对象结构类需要重定义对所有访问者的接口,这可能需要很大的代价。如果对象结构类经常改变,那么可能还是在这些类中定义这些操作较好。
|
优点 | - 使得新增新的访问操作变得更加简单。
- 能够使得用户在不修改现有类的层次结构下,定义该类层次结构的操作。
- 将有关元素对象的访问行为集中到一个访问者对象中,而不是分散搞一个个的元素类中。
|
缺点 | - 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作,违背了“开闭原则”的要求。
- 破坏封装。当采用访问者模式的时候,就会打破组合类的封装。
|
小结 | - 1.SomeClass 的 Accept()方法就是访问原来类的小口, Accept()方法只有一句代码,就是: visitor.NewMethod(this) ,这是访问者模式的精妙之处。 2.SomeClass 的新功能通过实现 IVisitor 接口的类来实现。
- 访问者模式封装了对象结构元素之上的操作,使得新增元素的操作变得非常简单。所以它比较适用于那么对象结构很少变化的类。
- 访问者模式中对象结构存储了不同类型的元素对象,以供不同访问者访问。
|
解释器模式(Interpreter Pattern)
名称 | Interpreter |
结构 | |
动机 | 给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。 |
适用性 | - 当有一个语言需要解释执行, 并且你可将该语言中的句子表示为一个抽象语法树时,可使用解释器模式。而当存在以下情况时该模式效果最好:
- 该文法简单对于复杂的文法, 文法的类层次变得庞大而无法管理。此时语法分析程序生成器这样的工具是更好的选择。它们无需构建抽象语法树即可解释表达式, 这样可以节省空间而且还可能节省时间。
- 效率不是一个关键问题最高效的解释器通常不是通过直接解释语法分析树实现的, 而是首先将它们转换成另一种形式。例如,正则表达式通常被转换成状态机。但即使在这种情况下, 转换器仍可用解释器模式实现, 该模式仍是有用的。
|
优点 | - 可扩展性比较好,灵活。
- 增加了新的解释表达式的方式。
- 易于实现文法。
|
缺点 | - 执行效率比较低,可利用场景比较少。
- 对于复杂的文法比较难维护。
|
小结 | - 在解释器模式中由于语法是由很多类表示的,所以可扩展性强。
- 虽然解释器的可扩展性强,但是如果语法规则的数目太大的时候,该模式可能就会变得异常复杂。所以解释器模式适用于文法较为简单的。
- 解释器模式可以处理脚本语言和编程语言。常用于解决某一特定类型的问题频繁发生情况。
|
中介者模式(Mediator Pattern)
名称 | Mediator |
结构 | |
动机 | 用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。 |
适用性 | - 一组对象以定义良好但是复杂的方式进行通信。产生的相互依赖关系结构混乱且难以理解。
- 一个对象引用其他很多对象并且直接与这些对象通信,导致难以复用该对象。
- 想定制一个分布在多个类中的行为,而又不想生成太多的子类。
|
优点 | - 简化了对象之间的关系,将系统的各个对象之间的相互关系进行封装,将各个同事类解耦,使系统成为松耦合系统。
- 减少了子类的生成。
- 可以减少各同事类的设计与实现。
|
缺点 | - 由于中介者对象封装了系统中对象之间的相互关系,导致其变得非常复杂,使得系统维护比较困难。
|
小结 | - 每个控件不需要直接和别的控件打交道,只需要知道中介者就可以了。
- 每个控件都需要保持保持中介者的引用,而中介者不一定需要保持每个控件的引用
- Mediator 的代码可能很复杂。
|
备忘录模式(Memento Pattern)
名称 | Mediator |
结构 | |
动机 | 用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。 |
适用性 | - 一组对象以定义良好但是复杂的方式进行通信。产生的相互依赖关系结构混乱且难以理解。
- 一个对象引用其他很多对象并且直接与这些对象通信,导致难以复用该对象。
- 想定制一个分布在多个类中的行为,而又不想生成太多的子类。
|
优点 | - 给用户提供了一种可以恢复状态的机制。可以是用户能够比较方便地回到某个历史的状态。
- 实现了信息的封装。使得用户不需要关心状态的保存细节。
|
缺点 | - 消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。
|
小结 | - 到需要保存对象多个状态,并且可以恢复到任意状态,可考虑备忘录模式。
- 将对象、对象状态和状态的保存办法分离,设计上会更加灵活。
- 但对象属性的备份、恢复、保存等动作,都是可能比较耗时的。
- 备忘录模式只是提供了保存对象状态的一种解决方案框架 ,真正应用时还需要解决这些 问题:1.如何设计 Memento? 2.怎样保存多个对象? 3.怎样保存组织成树结构的多个对象?
|
模板方法模式(TemplateMethod Pattern)
名称 | Template Method |
结构 | |
动机 | 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Te m p l a t e M e t h o d 使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。 |
适用性 | - 算法的骨架是不变的,但其中的步骤实现可能有变化,这时可应用模板方法。
|
优点 | - 模板方法模式在定义了一组算法,将具体的实现交由子类负责。
- 模板方法模式是一种代码复用的基本技术。
- 模板方法模式导致一种反向的控制结构,通过一个父类调用其子类的操作,通过对子类的扩展增加新的行为,符合“开闭原则”。
|
缺点 | - 每一个不同的实现都需要一个子类来实现,导致类的个数增加,是的系统更加庞大。
|
小结 | - 模板方法模式定义了算法的步骤,将这些步骤的实现延迟到了子类。
- 模板方法模式为我们提供了一种代码复用的重要技巧。
- 模板方法模式的抽象类可以定义抽象方法、具体方法和钩子。
- 为了防止子类改变算法的实现步骤,我们可以将模板方法声明为final。
|
状态模式(State Pattern)
名称 | State |
结构 | |
动机 | 允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。 |
适用性 | - 一个对象的行为取决于它的状态, 并且它必须在运行时刻根据状态改变它的行为。
- 一个操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示。通常, 有多个操作包含这一相同的条件结构。S t a t e模式将每一个条件分支放入一个独立的类中。这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化。
|
优点 | - 封装了转换规则。
- 枚举可能的状态,在枚举状态之前需要确定状态种类。
- 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
- 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
- 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
|
缺点 | - 状态模式的使用必然会增加系统类和对象的个数。
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
- 状态模式对“开闭原则”的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码。
|
小结 | - 状态模式允许一个对象基于内部状态而拥有不同的行为。
- Context会将行为委托给当前状态对象。
- 状态模式对“开闭原则”支持不是很好。
|
责任链模式(Chain of Responsibility Pattern)
名称 | Chain of Responsibility |
结构 | |
动机 | 使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。 |
适用性 | - 有多个的对象可以处理一个请求,哪个对象处理该请求运行时刻自动确定。
- 你想在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
- 可处理一个请求的对象集合应被动态指定。
|
优点 | - 降低耦合度。它将请求的发送者和接受者解耦。
- 简化了对象。使得对象不需要知道链的结构。
- 增强给对象指派职责的灵活性。通过改变链内的成员或者调动它们的次序,允许动态地新增或者删除责任。
- 增加新的请求处理类很方便。
|
缺点 | - 不能保证请求一定被接收。
- 系统性能将受到一定影响,而且在进行代码调试时不太方便;可能会造成循环调用
- 可能不容易观察运行时的特征,有碍于除错。
|
小结 | - 职责链模式将请求的发送者和接受者解耦了。客户端不需要知道请求处理者的明确信息,甚至不需要知道链的结构,它只需要将请求进行发送即可。
- 职责链模式能够非常方便的动态增加新职责或者删除职责。
- 客户端发送的请求可能会得不到处理。
- 处理者不需要知道链的结构,只需要明白他的后续者是谁就可以了。这样就简化了系统中的对象。
|
行为型设计模式小结
模式 | 场景发散 | 一句话说明 |
---|
观察者(Observer) | 同步更新的问题 | 一呼百应。 |
策略(Strategy) | 设计你的战士 | 分离算法。 |
迭代器(Iterator) | 集合访问的烦恼 | 以一致的方式访问集合, “松绑 ”遍历算法代码。 |
命令(Command) | 神奇的Do与Undo | 分离功能调用者与功能实现者。 |
访问者(Visitor) | 增加新方法的烦恼 | 不改变对象结构增加新方法。 |
解释器(Interpreter) | 超级表达解释引擎 | 语法解释。 |
中介者(Mediator) | 麻烦的多角关系 | 处理多对多关系。 |
备忘录(Memento) | 假如一切可以重来 | 保存对象的多个状态并可任意恢复。 |
模板方法(Template Method) | 万能的排序器 | 定义好框架算法 ,某些步骤可自己定义 。 |
状态(State) | 是攻击还是逃走 | 方便地处理不同状态不同行为,以及状态之间的转换。 |
责任链(Chain) | 邮件自动处理系统 | 用不同的规则去处理请求。 |
模式比较
参考