常用设计模式的总结

一. 创建型模式

1. Factory method 工厂方法

作用

一个类需要一个产品来完成某项工作,但它不能确定,也不关心具体拿到什么产品,因此它定义一个工厂方法,将具体产品的生产延迟到子类决定。

实现

Alt text

  1. 父类可以选择为工厂方法提供一个默认的实现;
  2. 工厂方法通常在模板方法(Template method)中被调用,上图中AnOperation()就是一个模板方法。

2. Abstract factory 抽象工厂

作用

系统有一组相互关联的产品接口,及几套不同的实现。客户只依赖产品接口,并需要能灵活地在几套实现中切换。

因此提供一个抽象工厂生产抽象的产品,每个产品在其中都对应一个工厂方法,产品族的每一套实现都提供一个具体工厂。客户通过抽象工厂获取产品,当需要切换到产品的其他实现时只需要更换工厂的实现类。

实现

Alt text

应用

根据底层数据源的不同,DAO的实现通常有几套,当切换数据源时,系统使用的DAO的实现也应当能快速切换。这是使用抽象工厂的一个典型场景。

3. Singleton 单例

作用

保证一个类只有一个对象

实现

  1. private构造器
  2. private static 类变量 singleton
  3. public static 类方法 getInstance() 返回singleton。

实例化时机:

  1. eager
  2. lazy

lazy init 多线程问题的解决办法:Double Check

private volatile static A singleton = null;

public static A getInstance(){
    if(singleton == null){
        sychronized(A.class){
            if(singleton == null) singleton = new A();
        }
    }
    return singleton;
}

private A(){}
  1. 为什么要第二次的null判断?
    在第一次判null / 获取锁之间可能有其他线程实例化了。
  2. 为什么要volatile
    在上面提到的情况下,如果没有volatile保证的可见性,在第二次null判断时当前线程可能看不到别的线程创建的对象,从而通过并再创建一次。

static 内部类利用 “类的加载/static块是线程安全的” 实现线程安全的lazy init:

public class A{
    private static class Holder{
        private static A singleton = new A();
    }

    public static A getInstance(){
        return Holder.singleton;
    }

    private A(){}
}

4. Builder

作用

你有一个产品,该产品由若干part装配而成,装配的逻辑是固定的,但各个part的构造是可切换选择的,Builder模式将 固定的装配逻辑易变的part构造逻辑 分离开,可以方便地在不同的part实现逻辑之间切换。

实现

Alt text

  1. Director#construct() 负责固定的装配逻辑;
  2. 一个Builder实例负责一个产品内部所有part的构造(buildPart()方法族),并向外部暴露方法,在part都装配完毕后获取该产品。

交互

Alt text


二. 结构型模式

1. Adapter 适配器

作用

在两个不兼容的接口之间加一个中间层,用组合的方式将一个现有对象匹配到需要的接口。

Convert the interface of a class into another interface the client expects

实现

Alt text

2. Proxy 代理

作用

Provide a surrogate or place holder for another object to control access to it

你有一个真正干活的对象RealSubject,但需要向client控制对他的访问,比如权限的控制 / Lazy load / 结果的缓存等等,因此在client和RealSubject之间增加一个中间层Proxy代替RealSubject,Proxy包裹RealSubject,将具体功能实现委托给它,并在RealSubject执行真正的功能前后插入自己的逻辑;此外,Proxy向client隐藏了RealSubject的存在。

实现

Alt text

Decorator模式的区别

Proxy与Decorator有着相似的结构,* 他们都在client和真实对象之间增加一个与真实对象实现了相同接口的中间层,这个中间层保留了对真实对象的引用并向他们发送请求*。然而他们的设计目的是不同的:

Decorator侧重动态为实体增加功能,因此在该模式中:

  1. 实体只实现了部分功能,Decorator实现了其他的增强功能;
  2. 支持递归组合(增加多重功能);
  3. Decorator不知道自己装饰的是哪个具体对象,client必须自己手动将实体和Decorator关联起来。

Proxy的目的则是当访问一个特定实体不方便或不符合要求时,为这个实体提供一个替代者,因此:

  1. 实体实现了关键功能,Proxy提供(或拒绝)对它的访问;
  2. 不支持递归组合;
  3. Proxy向client屏蔽RealSubject的存在,client只能拿到Proxy;
  4. Proxy确定地知道自己的代理目标是RealSubject,因此它和RealSubject相关联而不是Subject接口;此外,它们的关系是静态的,无法在运行时改变Proxy代理的目标对象。

3. Bridge 桥接模式

作用

你有一个产品,它在两个维度上都是可变化的,如果用继承,则需要n*m个子类。Bridge模式将两个维度的继承体系独立出来,并在二者之间用组合进行装配,避免类的泛滥。

进一步地考虑,一个产品的继承体系应该只有一个维度,如果出现了其他维度上的继承,要考虑该维度是否是行为/实现相关的。对于行为/实现方面的变化,应当先把行为独立地抽象出来,并与原产品组合(这就是策略模式的含义),而不应该直接在原产品上通过继承表达该行为的变化。

举个例子,假如系统内要发送消息,消息按迫切程度分为普通/加急/特急,消息的发送形式也可以多样,比如站内信/短信/email,每种消息都要求可以用任意方式发送:

Alt text

如果简单地用继承,则需要3*3 = 9个类。但实际上,消息的发送 这个维度属于行为,不要用继承来表达行为的变化,这样会污染原本的抽象层次,应当用策略模式消息发送 这个行为分离。

采用Bridge模式:
Alt text

实现

Alt text

产品的抽象 + 行为的分离(策略模式

总结

Bridge模式在我看来是对策略模式的扩展,它的核心有两点:

  1. 只在一个维度上用继承,出现了多个维度则考虑分离并用组合,避免类的泛滥和抽象维度的混杂;
  2. 策略模式隔离行为的变化,不要让行为/实现的变化污染原本的继承体系。

4. Decorator 装饰者

作用

有一系列产品,你希望动态地为他们添加额外 / 可自由组合 的功能,并且不影响产品本身。

Attach additional responsibilities to an object dynamically.

实现

Alt text

  1. Decorator 装饰器继承产品抽象接口,并在内部持有一个产品(可能是具体产品,也有可能被装饰过了);
  2. Decorator的具体实现,为其装饰的产品提供额外的功能,类似递归的调用;
  3. 可以同时反复应用多个 Decorator ,实现额外功能的动态组合。

应用

1.
java的IO流的设计是一个典型的装饰者模式:
Alt text

ByteArrayInputStream | FileInputStream | ObjectInputStream | StringBufferInputStream是具体的输入流产品,根据数据来源区分;
FilterInputStream是装饰器;
BufferedInputStream | DataInputStream | LineNumberInputStream | PushbackInputStream是具体的装饰器,分别为其他输入流提供缓冲/类型读写/跟踪行号/退回已读数据的功能,这些装饰器是可以组合使用的:

InputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("test.txt"))); 

2.
Decorator模式也可以用来实现AOP的类似功能(虽然实际大部分都是用JDK动态代理 / 运行时修改字节码),Decorator的具体实现就是我们想要独立出来的切面,产品的具体实现则是我们想要保持独立的业务逻辑。

5. Composite 组合模式

作用

实现树形结构,并让用户可以用统一的接口对待叶子节点和非叶子节点。

实现

Alt text

  1. 操作孩子的方法应该放在Component中吗?毕竟Leaf是不支持这些操作的。
    出于透明性考虑,应该放在Component中,Leaf对这些方法就提供一个空的实现。
  2. Component除了保存孩子,也可以记录父亲;

应用

UI / 人员组织管理这种典型的树形结构中用的比较广泛。

6. Facade

作用

一个系统对外提供服务,系统暴露的接口应该是简单而统一的,客户不应该直接和系统内复杂的子部件进行交互,而应只依赖于一个单一的高层接口,该组件为客户屏蔽了内部的复杂性,降低了客户和系统的耦合。

Alt text

更像是一种设计思路,而非一个具体模式。


三. 行为模式

1. Chain of Responsibility 责任链

作用

客户端发出一个请求,有一系列的处理器都有机会处理这个请求,但具体哪个是运行时决定的,客户端也不知道究竟谁会来处理。

因此将所有处理器组成一个链条,将请求从链条中流过,每个处理器查看是否应该处理它,如果不是,则交给后面的处理器,否则处理并退出。处理器在链中的位置决定其优先级。

将请求者和处理者解耦,可以动态切换/组合处理者。

实现

Alt text

扩展

客户端发出一个请求,请求的处理分为很多步骤,这些步骤是不确定的/可以动态组合的,甚至需要支持在运行时改变步骤,或者在步骤间任意跳转。

解决方案和责任链类似,将处理流程抽象为一个处理器链条,链条的组装交给外部决定。每个处理器对请求完成自己负责的业务逻辑,并看情况结束/传递给下一个处理器/跳转到任意处理器。

这和标准的责任链的结构基本一样,但他们的目的不一样。标准责任链目的是动态 找到请求的处理者 ;扩展(某些地方称为“功能链”?)则是为了获取 动态拼装和改变处理流程 的能力。

应用

标准责任链

UI中的事件冒泡机制是责任链的一个典型应用。HTML中,点击一个DOM元素,产生的click事件将依次冒泡给它的父元素,每个父元素上都可以注册对click事件的监听器,监听器中除了对事件处理外,也可以结束事件的继续冒泡。

扩展(功能链)
  1. Web应用中的各种filter/拦截器;
  2. Netty中的pipeline

2. Command 命令

Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.

你需要向一个对象提交请求,但对请求的处理是动态的,无法写死。比如一个菜单项,在不同的上下文中,点击它要做的事情显然是不一样的。

Command模式的思路是 抽象请求(及处理)

Alt text

  1. Client 装配 InvokerCommand* 如果需要不同的处理,装配不同的Command即可 *;
  2. ClientInvoker 发出请求;
  3. Invoker 将请求的处理委托给 Command#execute()

很多时候Command不够智能,自己无法处理请求,需要将请求委托给另一个Receiver进行真正的处理,ConcreteCommand可以认为是Receiver的适配器:

Alt text

可以看到,Command模式的最大价值在于:隔离 请求的接收者请求的处理逻辑
此外,将请求及其处理逻辑抽象为Command后可以做很多有意思的事情:

1. 可撤销的操作
在Command接口中增加一个接口 undo() 实现单个命令的撤销动作,并用一个stack保存所有Command;当用户触发撤销时依次从stack pop出最近的Command,执行其undo()方法。

2. 宏命令
宏命令实质是个树形结构,对Command应用Composite 组合模式即可实现:

Alt text

3. 排队
4. 日志记录和恢复

3. Memento 备忘录

有些情况下你需要记录一个对象(称为Originator)在某个时刻的状态(snapshot),以便后续恢复,我们可以用一个类Memento表示snapshot,它包含了Originator的部分或全部状态:

Alt text

  1. Originator 负责创建Memento,以及恢复到某个Memento;
  2. Memento 即Originator的snapshot;
  3. Caretaker 充当协调者,它负责向Originator请求当前Memento / 保存Memento / 在后续某个时刻让Originator恢复到某个Memento。

但这里有一个问题,为了隐藏Originator的实现细节,Memento必须向外部隐藏内部数据,即不开放state 的 getter/setter 给外部,但这样一来,Originator也无法创建Memento了。

为了解决这个问题,在Memento模式的一般实现中,Memento 类被分为两个部分:

Alt text

  1. 标记接口 Memento,空的,Caretaker只能得到这个接口;
  2. Memento的真正实现MementoImpl,作为Originator私有内部类 ,这样既允许Originator访问Memento的内部状态,又满足了Memento向外部(主要是Caretaker)隐藏内部细节的要求。

如果对 Memento 的封装性没有严格的要求,第一种实现显然更简单。

4. Observer 观察者

作用

定义一个一对多关系,在Subject状态发生改变时,所有Observer获得通知。

解耦 事件发生者 & 事件接收者,使得双方的改动互不影响,关联关系也可动态改变。

实现

Alt text

数据传递的两种方式:

  1. 推:由 Subject 主动向 Observer 推送信息,而不管信息对后者而言是否需要/是否足够。
  2. 拉:Subject 把自己传递给 Observer,由 Observer 从 Subject 拉取自己需要的信息。

扩展: Observer 注册时可以指定自己感兴趣的事件。

扩展:EventBus

传统的Observer模式中,事件发生者和接收者依然存在耦合,发生者需要管理接收者的集合,我们可以进一步地,在Subject和Observer间增加一个中间层负责转发事件,将它们彻底地解耦;进一步,这个事件转发者可以是通用的,支持任意发布者和接受者,通常称之为EventBus,是一种广泛应用的架构。

5. State 状态模式

作用

模拟状态机,描述一个对象(Context)的状态变迁,将特性状态下的行为分割开来,避免在Context中用大量的if维护所有状态的变迁,而且容易扩展新的状态。

实现

Alt text

  1. Context中记录它自己当前的状态;
  2. Context接收一个输入动作,并将该输入委托给当前所处State处理;
  3. State处理输入,根据需要让Context跃迁到另一状态。

如果State不保存状态则可以是单例的,Java中,可以用enum类型实现State

6. Strategy 策略模式

作用

你有一个对象负责完成某件事情,但在不同时刻其使用的算法是不同的,Strategy模式将可变的算法独立并封装,避免大量if条件判断,并方便替换和扩展。

Strategy 封装了 相同行为的不同实现

实现

Alt text

Strategy的实现通常依赖Context的数据,后者在调用前者的方法时需要将自己传递过去。

实际应用中,经常会发现不同的策略其算法骨架类似,只有某些具体步骤不同,此时可以对Strategy应用Template Method模式。

7. Template Method 模板方法

作用

将一个算法的通用骨架抽象到父类以避免代码重复,而将一些可变的步骤延迟到子类,子类不用关心算法结构,只需关注自己需要实现的特定步骤。

实现

Alt text

这个没什么好说的。


四. 设计原则

  1. 单一职责
  2. 开放-关闭原则
    一个类应当对扩展开放,对修改关闭。即当有新的需求时,不是修改已有的类,而是对已有的类进行扩展。
    实现开闭原则的关键在于 分离不变和变化的部分,并对变化的部分进行合理的高层抽象,并让不变的部分依赖该高层抽象,这样就能在不同的实现间切换,或者扩展新的实现。很多设计模式都体现了这一点,比如策略模式将算法抽象出来,模板方法将不变的算法骨架与易变的需要自定义的步骤隔离,装饰者模式将不变的核心功能对象和易变的增强功能隔离等等。

  3. 里氏替换原则
    子类必须能替换掉父类,这个原则通常由语言保证。

  4. 依赖倒置
    高层不直接依赖底层,而是高层定义自己需要底层提供什么样的接口,底层负责实现,这样就可以随意切换底层的具体实现而不用影响高层,但底层反而要依赖高层公布的接口,所以称为“依赖倒置”。

  5. 接口分离原则 (interface segregation principle)
    不应出现庞大的接口,迫使客户在使用时必须从一大堆它不需要的方法中寻找目的方法。这样的接口应该按照不同客户的需求被分离成若干小接口。

  6. 最少知识原则(least knowledge principle)
    类应当只与自己的朋友交互。该原则的思想是,将类对外部的了解尽量保持在一定范围内,尽量减少类之间的交互,从而降低各个组件间的耦合。
    “朋友”的定义:

    • 当前对象的属性
    • 当前对象所创建的对象
    • 方法参数传递进来的参数
    • 方法内创建的对象
Loading Disqus comments...
目录