本篇文章是对Design Patterns: Elements of Reusable Object-Oriented Software的总结。
看完Design Patterns: Elements of Reusable Object-Oriented Software已经有些时日了,现在提笔将其概括总结一下,以备将来的参考。此书总结了23中不同的设计模式,并且将它们的设计目的、结构、用法等方面进行的很好的介绍,是一本值得放在手边随时查阅的工具书。
Creational Patterns
1. Abstract Factory
目的
为创建一组相关对象,Abstract Factory提供统一的接口,并且不需要指定它们具体是什么类型的。Abstract Factory是和Concrete Factory(简称Factory)相对的一个模式,Factory的功能是创建相关的一组对象,它是具体的。而Abstract Factory则是仅仅提供接口,具体如何创建、创建什么类型的对象由子类Factory来具体实现。也叫Kit。
结构
AbstractFactory: 声明了创建具体产品对象的接口。
ConcreteFactory: 实现了创建具体产品对象的函数。
AbstractProduct: 声明了某一种产品对象的接口。
ConcreteProduct: 定义了具体被创建的一个产品对象。实现了AbstractProduct的接口。
Client: 仅仅调用AbstractFactory创建产品,并且使用AbstractProduct的接口。用户无需关心具体使用了哪一个Factory和Product。
何时使用
- 一个系统需要和它的产品的创建、组成、表示相互独立时。
- 一个系统需要配置多种产品家族中的一个时。如结构图中所示,有1和2两个产品家族,client只需要配置其中的一个即可,如果配置了1,则client使用的所有product都是1类型的,有ProductA1、ProductB1等。
- 一族相关联的产品需要一起被使用,不可混用,你可以通过Abstract Factory来加强这种限制。client如果选择使用了1,则应该保证所有的产品都是1家族的产品。比如,一个应用的UI支持windows和mac两种风格,用户选择了windows风格的UI,则应该保证所有的按钮、菜单和边栏等都是windows风格。
- 你想提供一个product的库,同时你只想暴露它们的接口,而不是如何实现的。
2. Builder
目的
将一个复杂对象的构建和表示相分离,从而可以使得相同的构建过程创造出不同的表示。
结构
Builder: 声明了一个创建一个产品对象一部分的抽象接口。
ConcreteBuilder: 实现Builder定义的接口,可以创建和组装产品的一部分。定义并且保存了它所创建产品的当前表示。同时,它还提供了一个可以获得所创建产品的接口。
Director: 使用Builder的提供的接口创建产品对象。
Product:表示正在被创建的一个复杂对象。ConcreteBuilder建造了它的内部表示,决定了它组装的过程。它包含了定义它组成部分的类,同时提供了接口让具体Builder来组装这些组成部分。
何时使用
- 创建一个复杂对象的算法需要独立于对象的组成部分的定义和组装方式。如Director中实现了一个创建产品对象的算法,而对象内部如何组装的则由Builder决定。
- 一个创建过程可以产生不同的对象表示。比如,Director可以采用不同的ConcreteBuilder来创建产品对象,在Director中创建的过程是相同的,但是由于Builder的不同,导致最后产品的内部结构可能差异很大。
3. Factory Method
目的
定义一个创建对象的接口,由子类去决定创建哪一个具体的对象。Factory Method可以将实例化对象的过程委派给子类。注意,这里Factory Method强调的是方法,并且该方法极有可能会被子类覆盖。在Abstract Factory中就使用了Factory Method来创建产品。
结构
Product: 定义了Factory method创建的对象的接口。
ConcreteProduct: 实现了Product的接口。
Creator:定义了Factory Method,factory method会创建某一个类型的产品。它有可能定义一个默认的factory method方法来创建一个具体的产品。
ConcreteCreator: 重写factory method来实例化一个ConcreteProduct。
何时使用
- 一个类不能预料它必须创建的对象属于哪一类。
- 一个类希望它的子类来决定需要创建的对象。
- 一个类将责任委派给它的一个子类,你希望将哪一个类被委派的知识本地化。
4. Prototype
目的
通过一个原型实例来创建一类的对象,创建一个新对象的过程就是拷贝原型实例。这样做可以省去传入参数来创建对象的麻烦,保证每次创建的对象都具有相同的属性。即使需要相同类的不同属性的对象,也可以先进行拷贝,然后通过稍加修改,就可以得到我们想要的对象。
在Nodejs中有prototype的使用。
结构
Prototype: 定义了一个来拷贝自己的接口。
ConretePrototype: 实现了克隆自己的操作。
Client: 让Prototype克隆自己来创建一个新对象。
何时使用
- 系统需要独立于它的产品如何创建、组成和表示的时候。
- 当需要实例化的类在运行才决定时。
- 想要避免创建一个平行于产品类层次结构的factory层次结构。因为创建一个对象只需要通过原型拷贝,无需使用factory来创建。
- 当一个类的实例只有为数不多的几个状态组合中的一个时,可以先实例好几个不同状态的对象,每次只需要从这些原型对象中按需拷贝出来新对象即可。
5. Singleton
目的
保证一个类只有一个实例,并且提供一个可以访问它的全局切入点。
在Android系统中的ActivityManagerService就是一个单例模式。
结构
Singleton: 定义一个实例化函数,可以让用户通过该函数获取该独一无二的实例。Instrance是一个类函数。有些时候,它还要负责创建它独一无二的实例。
何时使用
- 一个类只能有一个实例对象时,并且需要提供一个公共接口给用户访问该独一无二的实例。
- 这个孤立的实例对象可以通过继承来扩展,并且用户可以无需修改原来的代码来使用该扩展的实例。
Structural Patterns
6. Adapter
目的
将一个类的接口转化为用户想要的接口形式。Adapter可以让原来接口不相匹配的类进行合作。也叫Wrapper。
结构
Adapter使用多继承来实现接口的适配。
Adapter通过组合来实现结构的匹配。
Target: 定义了用户想要使用的接口。
Client: 调用目标接口来使用某些对象。
Adaptee: 实现了client想要的功能,但是接口并不是client想要的,所以要进行适配。
Adapter: 将Adaptee的接口适配成目标接口。
何时使用
- 你想使用一个现有的类,但是它的接口和你需要的不太匹配。
- 你想定义一个类,它可以和不相关、不可预见的其它类进行配合使用,这个时候你定义的类没必要适配所有的情况,根据具体情况配置适当的adapter就可以了。
- 你需要使用很多现成的子类,但是通过继承类修改每一个子类的接口是比较繁琐的,这时候你可以仅仅利用adapter适配它们的父类即可。
7. Bridge
目的
将一个类的抽象行为和具体实现进行解耦,从而可以使得这两个部分独立变化。也叫做Handle/Body。Bridge和Adapter都有给用户提供适配的接口的作用,但是Adapter更强调让原来不相干的类进行合作,往往类已经设计好了。而Bridge在设计之初,希望让抽象行为和具体实现进行分离。
结构
Abstraction: 定义抽象接口。有一个指向Implementor对象的引用。
RefmedAbstraction: 扩展Abstraction的接口。
Implementor: 定义了实现类的接口。这些接口没必要和Abstraction的接口保持一致,甚至可以完全不同。通常情况下,Implementor的接口提供的是基本的功能,Abstraction利用这些基本函数来构成更高级的函数功能。
ConcreteImplementor: 具体实现了Implementor的接口,定义了具体的实现。
何时使用
- 你希望避免将抽象行为和具体实现进行永久的绑定在一起。例如,一个类提供的抽象行为没有变化,但是在运行时,这些行为的具体的实现却需要进行变动。
- 抽象行为和具体实现应该可以通过继承来进行扩展。Bridge可以让及将不同的抽象和实现进行组合,并且可以互不影响的进行扩展。
- 对具体实现的改变不会影响用户,因为抽象行为没有变化。
- 你希望对用户隐藏抽象行为的具体实现。
- 你想共享具体实现部分,并且这种共享对用户而言是透明的,因为用户只能看到抽象行为。
8. Composite
目的
将对象组合成树的结构来表现部分-整体的层次结构。Composite使得用户可以将个体对象和组合对象统一进行处理。
Android系统中Activity中存放View组件的RootView就是一个典型的Composite。
结构
Component: 为需要进行组合的对象定义了接口。适当的实现所有类共有的默认行为。定义访问、管理子组件的接口。也可以定义访问父节点的接口。
Leaf: 表示叶子节点,叶子节点没有子节点。定义了组合中基本对象的函数。
Composite: 定义有子节点的组件的行为。存储子节点。实现Component中定义的有关子节点操作的函数。
Client: 通过Component提供的接口操作组合结构。
何时使用
- 你想表现一个部分-整体的层次结构时。
- 你希望让用户无法感知组合对象和孤立对象的区别,用户可以统一地处理composite结构中所有的对象。
9. Decorator
目的
动态的给一个对象添加功能。Decorators提供了一个灵活的选择来扩展功能。也叫做Wrapper。
结构
Component: 定义了基本组件的接口。
ConcreteComponent: 定义了一个具体组件的类。
Decorator: 有一个指向一个Compoent对象的应用,并且有着和Component一样的接口。这样可以保持给用户提供一致的接口,用户可以在不知不觉中使用已经扩展功能的对象。
ConcreteDecorator: 给组件添加新的功能。并通过将Component的原始接口和新功能封装在重写的函数中实现行为的一致性。
何时使用
- 动态地、透明地、不影响其它对象地给某个对象添加新的功能。
- 可以撤销对象的新功能。
- 通过继承来实现功能的扩展不切实际时。有时大量的可以扩展的独立功能,但是直接通过继承来实现会导致子类的组合爆炸。继承是需要在编程时写死的,而采用Decorator则可以在运行时动态的组合新的功能。
10. Facade
目的
为一个子系统的一组接口提供一个统一地接口。Facade定义了一个更高层次的接口,是的子系统更加易于使用。
结构
Facade: 知道在子系统中哪一个类应该对一个请求进行处理,并且将适当的请求交给相应的类。
subsystem classes: 实现子系统的功能,处理Facade交给的任务,但是它们并不知道Facade存在。
何时使用
- 你如果想给一个复杂的系统提供一个简单的接口,请用Facade。子系统会随着升级越来越复杂,可能会导致许多细粒度的类出现,虽然这增加了子系统的可定制性,但是对用户而说却越来越难使用。Facade可以给这些用户提供一个相对简单的视图,只有在用户需要定制子系统时,才需要越过Facade来观察子系统。
- 在传统的系统中,客户端和子系统之间会产生复杂的依赖关系,而Facade的引入可以很好的将用户和子系统进行解耦,因此可以提升子系统的独立性和可携带性。
- 将子系统进行层次化。用facade来定义每个子系统的接口,如果子系统之间有依赖关系,你就可以让这些子系统之间只通过他们的facade接口来进行交互,从而简化他们之间的依赖。
11. Flyweight
目的
通过分享的方式来高效的支持大量的细粒度的对象。
结构
Flyweight: 声明了用来接收和处理外部状态的接口。
ConcreteFlyweight: 实现了Flyweight接口,并且有自己的内部状态。ConcreteFlyweight必须是可以共享的。它内部存储的状态必须是与上下文无关的。
UnsharedConcreteFlyweight: 不是所有的Flyweight子类都需要被分享,Flyweight并不强制需要被共享。
FlyweightFactory: 创建和管理flyweight对象。保证flyweight可以被适当的共享。当用户需要一个flyweight时,FlyweightFactory会根据是否已经有该flyweight而创建或者直接返回一个flyweight对象。
Client: 有指向flyweight的引用,同时有flyweight的外部状态。
何时使用
当满足一下所有条件时可以使用:
- 一个应用使用了大量的对象。
- 因为对象的数量庞大而导致耗费大量的存储资源。
- 大部分的对象状态可以提出来作为外部状态传入。
- 当外部状态被提取出来以后,大量的的对象可以被几个可以共享的对象替代。
- 应用不依赖于对象的独一无二性,因为对用户而言,它们表面上是不同的对象,但是实质上它们是被共享的。
12. Proxy
目标
为一个对象提供一个代理或者占位符来控制对它的访问。也被称作Surrogate。
在Android系统中,Activity与ActivityManagerService进行进程间通信时,Activity是与ActivityManagerService的Proxy进行直接打交道的。
结构
Proxy: 有着一个指向真正的主体引用。它会提供和主体一样的接口,所以它可以替代主体。控制对真正主体的访问,也可能负责主体的创建和删除。不同种类的proxy:
1. **remote porxy**: 负责对请求和参数进行编码,然后将请求发送给在其它地址空间的真正主体。
2. **virtual proxy**: 缓存部分真正主体的信息,因为它可以延缓访问主体。
3. **protection proxy**: 验证访问者是否有权限发送请求。
Subject: 定义了RealSubject和Proxy的统一接口,所以Proxy可以在任何地方替代RealSubject。
RealSubject: 定义了Proxy需要代理的真正对象。
何时使用
- remote proxy: 给不在本地空间的对象提供了一个本地表示。
- virtual proxy: 在必要时才需求创建昂贵的对象,能拖就拖。
- protection proxy: 控制访问原始对象。在一个对象有着不同等级的访问权限时,Protection proxy十分有用。
- smart reference: 可以代替指针,在访问主体对象之前可以进行一些附加操作。比如统计有多少个引用指向该主体,在适当的时候可以释放主体;在第一访问时将主体加载到内存;对主体进行加锁等。
Behavioral Patterns
13. Chain of Responsibility
目的
解耦请求的接收对象,使得一个请求可以被多个接收者处理。将这些接收对象串联成链,并且将请求依次传递过去,直到有接收者可以处理这个请求。
在Nodejs中有Connect模块负责在原始的http模块上进一步处理http请求,加入了session、Cookie、body解析等中间件来依次处理http请求。Connect的设计模式的就是典型的Chain of Responsibility。
结构
Handler: 定义了一个处理请求的接口。提供了后继的连接。
ConcreteHandler: 处理它需要处理的请求,也可以将请求继续传递下去。
Client: 创建一个请求给ConcreteHandler。
何时使用
- 当不止一个对象可以处理一个请求时,并且接收者的优先级不被告知,那就需要这些接收者自己确定优先级。
- 你想将一个请求发送给多个对象中的一个,但是不想指明接收者时。
- 那些可以处理对象的接收者可以动态的被决定。
14. Command
目的
将请求封装为一个对象,因此可以让你使用不同的请求参数化客户端、列出请求序列,还能够支持撤销等行为。也被称作Action、Transaction。
结构
Command: 声明一个接口来执行一个操作。
ConcreteCommand: 定义一个Receiver和action之间的绑定关系。实现Execute函数来在Receiver上执行某些操作。实质上是将receiver和作用其上的Action封装成了一个command的对象,以便记录和撤销。
Client: 创建一个ConcreteCommand并且制定它的receiver。
Invoker: 让command执行请求。
Receiver: 知道如何对请求进行具体的操作。
何时使用
- 通过一个action来参数化一个对象时。你可以在面向过程的语言中使用回调函数来实现这种定制,回调函数就是先在某些地方注册,在以后某个时间点执行的函数。Command是在面向对象的语言中一种回调函数的替代方式。
- 在不同的时刻指定、放入队列,和执行请求。如果一个请求的接收者可以在不同的地址空间中,那么你可以将请求封装成command给不同的进程,并且在这些进程中执行这个command。
- 支持撤销动作。Command可以存储装状态用来撤销自己执行的动作所产生的效果。所有执行过的动作应该放在一个列表中,使用该列表可以实现撤销和重做一系列动作的功能。
- 可以记录行为日志,可以在crash发生时恢复系统状态。
- Command可以让系统建立在由基本操作组成的高级操作之上。在信息系统中,将基本操作封装为高级操作–“事务“是很常见的一种模式。
15. Interpreter
目的
给定一种语言,在interpreter中定义该语言的语法表示来翻译该语言。
结构
AbstractExpression: 给抽象语法树种所有节点声明抽象的公共翻译接口。
TerminalExpression: 实现一个和语法中终结符号相关的的翻译函数。每个语法中的终结符都需要有一个对应的实例。
NonterminalExpression: 非终结符表达式。有着一组类型为AbstractExpression的变量,这些变量是该表达式的子表达式。它会提供接口来递归的翻译所有子表达式。
Context: 给Interpreter提供上下文环境。
Client: 建造一个抽象语法树来表现某一种语法定义的语言。抽象语法树由NonterminalExpression和TerminalExpression构成。Client负责调用翻译函数。
何时使用
在需要翻译一种语言时使用Interpreter模式,并且该语言中的语句可以使用抽象语法树来表示。下列情况下使用最佳:
- 语法比较简单。对于较为复杂的语法,用来表现语法的类的层次结构变得很庞大和不好管理。在语法复杂的时候解析生成器更为适合。他们可以不使用抽象语法树来翻译语句,因此可以节省空间和时间。
- 效率不是特别的重要的时候。最有效的翻译方式通常不是直接翻译语法树,而是首先将它们转化为其它的形式。例如,正则表达式经常首先被转化成一个状态机。
16. Iterator
目的
提供了一个可以按序访问集合对象元素的方式,并且无需暴露集合的内部实现。
结构
Iterator: 定义一个访问和遍历元素的接口。
ConcreteIterator: 实现Iterator接口,保存当前遍历到的位置。
Aggregate: 定义了一个创建Iterator对象的接口。
ConcreteAggregate: 实现了穿件Iterator的接口来返回一个合适的ConcreteIterator。
何时使用
- 访问一个集合的内容,并且无需暴露集合的内部表示。
- 可以支持多种遍历方式。
- 可以提供统一的接口来遍历不同的接口(因此可以支持多态的迭代)。
17. Mediator
目的
定义一个对象来封装一组对象之间的交互。Mediator让对象之间不通过指明的互相引用来减轻它们之间的耦合度,从而可以让你互不影响的变化它们之间的交互。
结构
Mediator: 定义了一个和同伴交流的接口。
ConcreteMediator: 实现合作的接口来让这些伙伴对象可以相互协作。它知道并且维护着它的同伴对象。
Colleague classes: 每个Colleague类都知道它的Mediator对象。当它想和同伴对象交流的时候,它会通过它的mediator来进行间接的交流。
何时使用
- 一组对象通过定义良好但方式复杂的方式来交流合作。结果就导致它们之间的依赖结构混乱并且难以理解。
- 重新使用一个对象是比较困难的,因为它引用着许多其它对象。
- 一个分布在多个类之间的行为应该很容易被定制,而不需要搞出来很多的子类。
18. Memento
目的
不用破坏对象的封装性质,获取一个对象的内部状态以便该对象可以恢复原来的状态。也可以被作token。在Android系统中,Activity可以通过savedInstanceState来保存和恢复一个Activity的状态。
结构
Memento:存储Originator对象的内部状态。有originator自己决定需要保存多少的内部状态。它应该保护存储的状态只由originator自己访问。Memento最好有两种接口,Caretaker只能看到一个“狭窄”的接口,它只能将Memento传给其他对象。相反的,Originator应该可以看到一个“宽广”的接口,从而可以让它访问存储的状态,从而恢复到原来的状态。理想情况下,只有产生memento的originator才可以访问memento的内部状态。
Originator:创建一个memento来保存它的内部状态快照。使用Memento来恢复它的状态。
Caretaker: 安全的保存着memento。不会操作和访问memento的内部状态。
何时使用
- 一个对象的状态快照需要被保存下来,以后可以用来恢复状态。
- 直接访问一个对象的内部状态会导致破坏一个对象的封装性,暴露对象的实现细节。
19. Observer
目的
定义一个一对多的对象之间的依赖关系,因此当一个对象发生改变,所有依赖于它的对象会收到提醒,并且自动的改变状态。可以称作Dependents,Publish-Subscribe模式。在Android系统中,有broadcast/broadcast receiver机制,在nodejs中,同样有event emmiter/listen机制。
结构
Subject: 知道所有的observer,observer的数量时不固定的。提供接口来添加或者移除所有的observer对象。
Observer: 给那些需要观察subject变化的对象提供了一个update的接口。
ConcreteSubject: 存储了ConcreteObserver感兴趣的状态,当它的状态发生变化时,提醒它的观察者。
ConcreteObserver: 有一个指向ConcreteSubject对象的引用,存储了应该和subject一致的状态。实现了Observer的更新接口,来保持与subject状态的一致。
目前,很多使用该模式的系统会进一步解耦合,使得subject和observer相互透明。
何时使用
- 一个抽象结构中有两个部分,一个依赖于另一个。将这两个部分封装成独立的对象,可以让你独立的修改和重复使用它们。
- 当一个对象的变化需要需要其他对象跟着变化,但是你不知道有多少对象需要被修改的时候。
- 当你想让一个对象通知其它对象,而无需关心这些被通知的对象是谁时。一句话,你不想让这些对象紧密耦合的时候。
20. State
目的
让一个对象可以在它的状态改变时,改变它的行为。使得对象看上去改变了自己的类。也被称作Object for States。
结构
Context: 定义了用户需要的接口。保存着一个定义了当前状态的ConcreteState子类实例。
State: 定义了一个用来封装与之对应的行为的接口。
ConcreteState subclasses: 每个subclass实现了与状态对应的行为。
何时使用
- 一个对象的行为依赖于它的状态,它必须在运行时刻根据它的状态改变它的行为。
- 一个对象的操作有着大量的条件语句依赖于对象的状态。这些状态通常只有可数的几种组合。通常,很多函数含有重复的条件结构。State可以将这些不同的条件分支放到不同的类中。从而,你可以将对象的状态看成独立的对象,这些状态可以相互独立的进行切换。
21. Strategy
目的
定义了一组算法,并且对每个算法进行了封装,并且他们之间是可以相互切换的。Strategy 让算法可以独立于用户变化。也可以称作Policy。
结构
Strategy: 定义了所有支持的算法的通用接口。Context使用该接口来调用ConcreteStrategy提供的算法。
ConcreteStrategy: 使用Strategy定义的接口实现了具体的算法。
Context: 它会配置一个ConcreteStrategy对象。保存一个对Strategy对象的引用。提供了可以让Strategy访问数据的接口。
何时使用
- 一些类只有他们的行为不同,使用Strategy可以让一个类配置多种行为。
- 你需要不同算法。比如,你可能定义了一些有着不同的时间/空间复杂度的算法。
- 用户无需知道算法如何使用数据。使用Strategy可以避免暴露复杂的,算法特定的数据结构。
- 一个类定义了多种行为,导致在函数中有多种条件语句来选择这些行为。为了避免这种繁杂的条件语句,将相关的分支封装成不同的Strategy对象。
22. Template Method
目的
在函数中定义了算法的骨架,将具体某些步骤交给子类来实现。Template Method让子类重定义部分步骤,而不用改变整个算法的结构。
结构
AbstractClass: 定义了抽象的primitive函数,让子类来具体实现这几步。实现了一个模版方法,决定了算法的骨架。template method 调用了primitive方法,还有一些其他方法。
ConcreteClass: 实现了primitive方法,从而达到定制算法某些步骤的目的。
何时使用
- 实现算法中不可变的部分,将可变的部分交给子类去实现。
- 当几个子类的函数中有着共同部分行为,可以将这共同的部分在父类中实现,避免代码的重复。有一种通过重构达到一般化的目的。
- 控制子类的扩展。你可以定义一个template方法在指定位置调用“hook”操作,从而限制子类只能在这些hook的位置进行扩展。
23. Visitor
目的
表示一个可以对结构体中的元素进行操作的模式。可以对其中的元素添加新的操作,而无需修改该元素对应的类。
结构
Visitor: 给每个结构中的类定义了一个访问函数。函数名指明了它能能够访问的对象。Visitor决定了自己可以访问的元素,通过特定的参数,visitor能够直接的访问这些元素。
Concrete Visitor: 实现Visitor声明的每个操作,每个操作位结构中对应的元素实现了算法的一个片段,Concrete Visitor提供了算法的上下文,并且存储了本地状态,可以加速遍历。
Element: 定义了可以接受Visitor的接口。
ConcreteElement: 实现了接受接口。
ObjectStructure: 可以列举自己的element。提供了一个更高层次的接口来让visitor来访问它的元素。可以是一个composite结构,也可以是一个列表、集合等。
何时使用
- 一个结构体中有着很多接口不同的元素对象,同时你希望对其中的这些元素进行操作。
- 你需要对一个结构体中的对象进行很多不同的,并且不相关的操作,同时你不希望“污染”这些原有的类。Visitor可以让你将这些无关的操作定义在一个类中。
- 结构体中对象的类很少变化,但是你常常想对结构添加新的操作。修改结构体中的类的接口,会导致需要重新定义visitor的接口,这些代价很大。所以如果结构体中的类如果常常变化,那么最好还是将操作定义在这些类中,而不要用visitor。
整理完了23个设计模式,经常回顾,必将会受益匪浅。