设计模式
设计模式是对常见设计的总结和抽象,既是一种解决问题的思路,同时也是一种提高沟通效率的工具。设计模式展示了如何构建具有良好面向对象设计特性的系统,是经过验证的面向对象经验。一般来说,设计模式不会直接提供代码,而是针对设计问题给出通用解决方案,需要结合实际问题进行应用。大多数设计模式和原则旨在解决软件中的变化问题,即它们常常试图将系统中变化的部分封装起来,从而使得系统在面对变化时能够保持灵活性和可维护性。
按照作用不同,可以将设计模式分为三类:创建型模式、结构型模式和行为型模式。而按照结构来说,可以分为类模式和对象模式。
范围\目的 | 创建型模式 | 结构型模式 | 行为型模式 |
---|---|---|---|
类模式 | 工厂方法模式 | 类适配器模式 | 模板方法模式 |
对象模式 | 简单工厂模式 抽象工厂模式 建造者模式 原型模式 单例模式 | 对象适配器模式 桥接模式 组合模式 装饰模式 外观模式 享元模式 代理模式 | 命令模式 迭代器模式 中介者模式 观察者模式 状态模式 策略模式 |
本文将介绍一些常见的设计模式。
创建型模式
工厂模式
名称:工厂模式
英文名称:
Factory Pattern
类型:创建型模式
简单工厂模式( )
考虑一个可以提供不同类型按钮的组件库,不同的按钮继承自同一个按钮基类,这些按钮都对基类的样式进行了相应的修改。如果我们不希望了解按钮派生类的具体信息,而只希望通过相应的参数来获取(创建)新的派生类。这时就可以采用简单工厂模式。
简单工厂模式又称为静态工厂模式,属于创建型模式。在简单工厂模式中,可以通过参数不同返回不同的实例。
简单工厂模式的结构如下:
- 工厂角色
Factory
:负责创建所有实例的内部逻辑。工厂角色中应当包含一个静态方法,用于接收一个参数,并返回一个相应的创建好的对象。 - 抽象产品角色
Product
:定义了所有具体产品的公共接口。 - 具体产品角色
ConcreteProduct
:实现了抽象产品角色定义的接口的具体产品。
简单工厂模式将对象的创建和对象的业务本身分离,降低了系统的耦合度。当你需要一个类,只需要传入一个正确的参数,就可以获取想要的对象,而不需要知道任何创建的细节,甚至连具体产品的类名都可以不知道。
简单工厂模式的简化
有些情况下,工厂角色与抽象产品角色可以是同一个。也就是说,把静态工厂方法写到抽象产品类中。

简单工厂模式的简化
一般来说,当工厂类负责创建的对象比较少,且客户端不需要了解创建细节,也不需要工厂角色形成继承层次结构时,通常使用易于实现的简单工厂模式。其他情况下使用简单工厂模式反而会降低系统设计的质量,可见下文。
工厂方法模式( )
简单工厂模式最大的问题是工厂的职责相对过重,增加新的产品需要修改工厂类的判断逻辑。在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。这违反了开闭原则。下面,工厂方法模式将解决这一问题。
工厂方法模式,又叫虚拟构造器(
工厂方法模式主要改进了工厂类的组织方式。在工厂方法模式中,我们不再设计一个具体的工厂类来统一负责所有产品的创建,而是将工厂类抽象为一个接口,将具体的产品创建过程交给专门的具体工厂类来实现。这样,每一个具体的产品都有一个对应的具体工厂类,而所有的具体工厂类都实现了工厂接口。在新增产品时,只需要新增一个具体产品类和一个具体工厂类即可,不需要修改原有的代码。这样,工厂方法模式就遵循了开闭原则。
客户端在使用工厂创建产品时,只需关心所需产品对应的具体工厂类是什么就可以了,无须关心创建细节,同样也无须知道具体产品类的类名。
// 抽象工厂类
public abstract class PayMethodFactory {
public abstract AbstractPay getPayMethod();
}
// 具体工厂类
public class CashPayFactory extends PayMethodFactory {
public AbstractPay getPayMethod() {
return new CashPay(); // CashPay 为产品类
}
}
// 客户端代码
PayMethodFactory factory;
AbstractPay payMethod;
factory = new CashPayFactory();
payMethod = factory.getPayMethod();
payMethod.pay();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
为了提高系统的可扩展性和灵活性,在定义工厂和产品时都必须使用抽象层,以应对具体工厂和产品可能发生的变更。在实际开发中,经常通过配置文件和反射结合的方式来自动创建对象。
使用配置文件和反射来自动创建对象
在实际的应用开发中,一般将具体工厂类的实例化过程进行改进,不直接使用 new
关键字来创建对象,而是将具体类的类名写入配置文件中,再通过 Java 的反射机制,读取 XML
格式的配置文件,根据存储在 XML
文件中的类名字符串生成对象。
<?xml version="1.0"?>
<config>
<className>CashPayFactory</className>
</config>
2
3
4
Java 反射是指在程序运行时获取已知名称的类或已有对象的相关信息的一种机制。可通过 Class
类的 forName()
方法返回与带有给定字符串名的类或接口相关联的 Class
对象,再通过 newInstance()
方法创建此对象所表示的类的一个新实例,即通过一个类名字符串得到类的实例。
Class c = Class.forName("CashPayFactory");
PayMethodFactory factory = c.newInstance();
return factory;
2
3
工厂方法模式的缺点在于添加新产品时需要添加新的工厂类,这样会导致类的数量增加;此外,还有可能使用 DOM、反射等技术。这些会增加系统的复杂度,并提高了编译运行的代价。
抽象工厂模式( )
无论是简单工厂模式还是工厂方法模式,它们都只能创建同一类产品(即继承自同一接口或抽象类的产品)。如果我们需要创建多个不同类别或位于不同等级结构的产品,那么我们就需要使用抽象工厂模式。
抽象工厂模式提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。抽象工厂模式又称为 Kit
模式,同样属于对象创建型模式。
产品族和产品等级结构
- 产品族:指由同一个工厂生产的,位于不同产品等级结构中的一组产品。
- 产品等级结构:指产品的继承结构。代表着不同类别的产品。
抽象工厂模式解决的问题是多种类产品和产品类之间等级结构的问题,是所有形式的工厂模式中最为抽象和最具一般性的一种形态。它与工厂方法模式的区别就在于,工厂方法模式针对的是一个产品等级结构,而抽象工厂模式则需要面对多个产品等级结构。
抽象工厂模式的结构如下:
- 抽象工厂
AbstractFactory
:最高层次的抽象,定义了工厂的公共接口。 - 具体工厂
ConcreteFactory
:实现了抽象工厂定义的接口,负责创建一个产品族中的产品。如下图中的WindowsFactory
、UnixFactory
等。 - 抽象产品
AbstractProduct
:定义了一种类别的产品。抽象产品可以有很多个。如下图中的Button
、Text
。 - 具体产品
ConcreteProduct
:实现了抽象产品定义的接口,是一个具体的产品。如下图中的WindowsButton
、UnixButton
等。

抽象工厂模式的一个例子
抽象工厂模式同样隔离了具体类的生成,使得客户并不需要知道什么被创建。此外,当一个产品族的多个对象被设计为一起工作时,能够保证客户端始终只使用同一个产品族中的对象,这对于一些需要根据不同的环境创建不同的对象的系统来说是非常有用的。另外,和工厂方法模式一样,抽象工厂模式也是符合开闭原则的,当需要增加一个新的产品族时,只需要增加一个新的具体工厂类即可。
抽象工厂模式的缺点在于,难以扩展抽象工厂来支持新种类的产品,这是因为支持新种类的产品就要对抽象工厂角色及其所有子类的修改,显然是不方便的。抽象工厂增加新的工厂和产品族较为方便,但是增加新的产品等级结构就比较麻烦。因此如果系统的变化主要体现在新增产品族上,抽象工厂模式是一个很好的选择,否则需要慎重考虑。这被称为开闭原则的倾斜性。
抽象工厂模式中,如果只有一个产品族,可以省略抽象工厂角色,退化为简单工厂模式;如果只有一个产品等级结构,退化为工厂方法模式。
建造者模式( )
名称:建造者模式
英文名称:
Builder Pattern
类型:创建型模式
无论是在现实世界还是在软件系统中,都存在一些具有多种组成成分的复杂对象。对于大多数用户来说,它们无需知道这些对象的内部构造细节和组合这些组件的过程,只需要知道如何创建并使用这些对象。
建造者模式将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。建造者模式是一种创建型模式。它可以一步一步构造一个复杂的对象,用户不需要知道具体的建造过程和细节,只需要知道应当使用哪一个建造者即可创建相应的产品。新增产品时,只需要新增一个具体建造者即可,不需要修改原有的代码,符合开闭原则。
建造者模式主要有四个角色,其结构如下:
- 产品
Product
:表示被构造的复杂对象。Product
类通常包含多个组成部分,这些部分可以是对象,也可以是原子数据类型。 - 抽象建造者
Builder
:抽象建造者类,规范产品的组建,一般是由一个接口或抽象类实现。接口中定义了产品的各个组成部分的建造方法,以及返回构建完成的产品的方法。 - 具体建造者
ConcreteBuilder
:具体建造者类,实现抽象类定义的所有方法。 - 指挥者
Director
:指挥者类,负责设定实际的建造者,安排已有模块的顺序以调用建造者完成对象的构建。前者一般可以利用setBuilder()
方法或初始化时设置,后者一般使用construct()
方法完成。
使用指挥者类的原因
指挥者类一方面隔离了客户与生产过程,另一方面负责控制产品的生成过程。这样,客户端只需要知道具体的建造者即可,无需自行创建产品或控制产品的生成过程。
例如,下面是一个在 KFC 套餐中使用建造者模式的例子:

建造者模式的一个例子
客户端可以这样去使用上图中的建造者模式:
// 创建指挥者
KFCWaiter waiter = new KFCWaiter();
MealBuilder builder = new SubMealBuilderA();
// 指挥者设定建造者
waiter.setMealBuilder(builder);
// 指挥者开始建造,并返回产品
Meal meal = waiter.construct();
2
3
4
5
6
7
8
9
注意,如果产品的组成或构造过程之间存在较大的差异,则不适合使用建造者模式。建造者模式的出发点是通过相同的建造过程构建不同的产品。在这一基础上,如果需要生成的产品对象有复杂的内部结构、或者需要生成的产品对象的属性相互依赖(有构造顺序要求),则建造者模式是一个很好的选择。
建造者模式的简化
如果系统中只需要一个具体建造者的话,可以省略掉抽象建造者;在这一基础上,还可以继续省略指挥者角色,让 Builder
扮演指挥者与建造者双重角色。
注意,简化是建立在对需求的洞察之上的。简化一定会带来可扩展性上的损失,但如果确定不会进行扩展,那么简化是一个不错的选择。
原型模式( )
名称:原型模式
英文名称:
Prototype Pattern
类型:创建型模式
有时,如果我们需要频繁创建复杂对象,我们可以尝试复制一个已有的对象,然后再进行修改。这种方式有时比直接创建一个新对象更加高效。而原型模式就利用了这一思想。
原型模式是一种创建型模式,它允许一个对象(原型实例)创建另一个与其相同且可定制的对象,同时无需知晓创建的细节。它一般包含以下几个角色:
- 原型
Prototype
:声明一个克隆自身的接口。 - 具体原型
ConcretePrototype
:实现Clone
方法,这一方法复制自身。 - 客户
Client
:让一个原型克隆自身从而创建一个新的对象。

原型模式的一个例子
使用原型模式克隆对象时,根据其成员对象的克隆情况,分为浅克隆和深克隆两种。浅克隆时,只复制对象本身,对于对象内部的引用类型,只是复制了引用,而不克隆其指向的对象;深克隆则是对对象本身以及对象内部的引用类型指向的对象都进行了克隆。
在 Java
中,可以通过实现 Cloneable
接口(能够实现克隆的 Java 类必须实现这一接口)和重写 clone()
方法来实现原型模式。Object
类含有一个 clone()
方法,它属于浅拷贝。下面的模版代码展示了如何实现原型模式:
// 原型接口
public interface Prototype extends Cloneable {
public Object clone();
}
// 具体原型
public class ConcretePrototype implements Prototype {
public Object clone() {
try {
return super.clone(); // 调用 Object 类的 clone() 方法,实现浅拷贝
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}
// 客户端
public class Client {
public static void main(String[] args) {
Prototype prototype = new ConcretePrototype();
Prototype clone = (Prototype) prototype.clone();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
如果需要深克隆,需要在 clone()
中自行实现对引用类型的拷贝。
总而言之,原型模式简化了对象的创建过程,提供了简化的创建结构(创建时不需要过度关心对象的类型与层次),提高了新实例的创建效率。使用深拷贝,还可以保存对象的状态(相当于快照)。然而,原型模式的缺点表现为需要为每一个类配备一个克隆方法(在修改已有类时违反开闭原则),并且实现深克隆较为复杂。
原型模式较为适用的情况包括:
- 创建新对象成本较大,通过复制原型对象得到新实例再修改可能比使用构造函数创建一个新实例更加方便;
- 系统要保存对象的状态,而对象的状态变化很小;
- 需要避免使用分层次的工厂类来创建分层次的对象;
行为型模式
策略模式( )
名称:策略模式
英文名称:
Strategy Pattern
类型:行为型模式
让我们考虑一个名为 SimDuck
的模拟鸭子游戏,这一游戏用于展示各种不同鸭子来愉悦心情。这个应用程序的主要需求即模拟各种类型的鸭子,例如野鸭、红头鸭、诱饵鸭等。这些鸭子都有一些共同的行为,例如游泳、飞行等,但是它们也有一些不同的行为,例如呱呱叫、吱吱叫等。有时,我们需要为某些鸭子添加新的行为(即变化),例如潜水。我们如何设计这个应用程序,使得它既能够保持灵活性,又能够避免代码重复呢?
继承并重写(override
)是一种解决方案。所有鸭子都继承 BaseDuck
父类,那么在添加新的行为时就可以在父类中添加新的方法,然后在子类中重写这个方法。但是这种方法有一个缺点,即我们需要为未添加这一行为的鸭子重写这个方法,这样会导致代码重复。另一种解决方案是使用接口,即为每一种行为定义一个接口,然后在每一个鸭子类中实现这些接口。但是这种方法也有一个缺点,即如果我们需要为某些鸭子添加新的行为,那么我们需要修改所有添加这一行为的鸭子子类,这也不是一个完美的方法。
我们重新从设计的角度考虑,可以发现这些方式都没有从变化的角度出发。先前的设计都是直接在实现部分进行修改,而并没有为变化的部分进行封装。可以看出,易变的部分是鸭子的行为,而不是鸭子的类型。因此,我们可以将鸭子的各类行为抽象出来,然后将其封装到几个类中。这样,当我们需要为某些鸭子添加新的行为时,我们只需要修改这个行为抽象类即可。在不同的鸭子类中,我们只要按需引入这些行为抽象类,然后调用它们的方法即可。这一设计是依赖倒置原则、开闭原则和合成复用原则的体现。

修改后的 SimDuck 类图
如果将鸭子看作是一个上下文,而将鸭子的行为看作是一个策略,那么,这一设计模式就是经典的策略模式(Strategy
或 Policy
的命名。
策略模式的结构如下:
- 策略接口
Strategy
:定义了所有支持的算法的公共接口。通常是一个接口或抽象类。 - 具体策略
ConcreteStrategy
:实现了策略接口的具体算法。
策略模式的特点有:
- 可维护性和可复用性强,易于扩展。
- 策略可以在运行时进行替换。
- 可以提供一系列具有相同行为、不同实现的策略。然而,客户需要对策略的具体内容有了解,知晓每一个策略的优缺点,从而选择合适的策略。
Shared Pattern Vocabulary
前文提到,设计模式是一种提高沟通效率的工具。使用设计模式时,通常会使用一些特定的术语来描述模式的结构和行为,这些术语有助于更好地理解和交流设计模式的概念。当你与其他开发者或团队使用模式进行交流时,你不仅是在传递一个模式名称,更是在传达该模式所代表的一整套特质、特征和约束条件——模式让你能够以简驭繁。其他开发者能迅速准确地理解你脑海中的设计意图。
状态模式( )
名称:状态模式
英文名称:
State Pattern
类型:行为型模式
有时一些对象的行为取决于一个或多个动态变化的属性,这样的属性可以称为状态。当状态改变时,对象的行为也会随之改变。状态模式就是为了解决这一问题而设计的。
状态模式是一种行为型模式,它主要包含如下的三个角色:
- 环境类
Context
:持有一个状态对象的引用,并定义了一个接口来与状态对象进行交互。 - 抽象状态类
State
:定义了若干接口,用于一些状态相关的操作供环境类调用。 - 具体状态类
ConcreteState
:实现了抽象状态类的接口,包括不同状态下的具体行为,以及各种状态之间的转换逻辑。

状态模式的一个例子
环境类针对抽象状态类进行编程,其通常持有一个具体状态类的实例,这使其拥有状态。此外,环境类通常还有一个改变状态对象的方法 setState(State newState)
。这样,当状态发生变化时,只需要修改环境类所持有的实例而不需要修改环境类的代码,就可以使环境类表现出不同的行为。这一修改可以发生在环境类本身,也可以在具体状态类中进行。因此,具体状态类通常持有一个对环境类的引用,以便在状态转换时能够修改环境类的状态。这样,就实现了对转换规则的封装。
当拥有多个环境对象的话,可以让多个环境对象共享一个状态对象(将其设置为静态),这样可以减少内存开销。
状态模式的优点包括:封装了转换规则,避免了使用大量的条件语句(如 if
、switch
等)来处理状态转换;可以在运行时改变对象的行为,就好像改变了类的代码一样;将所有与某个状态相关的行为都封装在一个类中,符合单一职责原则;便于增加新的状态等。缺点在于:状态模式会导致类的数量增加;结构复杂,如果使用不当(如每个状态的行为划分不正确)的话将会导致混乱;对“开闭原则”不利,增加新的状态需要修改负责状态转换的代码。
通常,当对象的行为依赖某些属性并且根据这些属性的不同而有不同的行为时,可以考虑使用状态模式。此外,如果代码中包含大量的条件语句来处理状态转换,那么也可以考虑使用状态模式来简化代码并减少客户端和类库的耦合。一些应用的场景:工作流
、游戏中的角色状态
、酒店预订
等。
两种状态模式
- 简单状态模式:指状态均相互独立,状态之间没有转换关系。如果是这种简单的状态模式,它遵循“开闭原则”,在客户端可以针对抽象状态类进行编程,而将具体状态类写到配置文件中,同时增加新的状态类对原有系统也不造成任何影响。
- 可切换状态的状态模式:通过调用环境类的
setState(State newState)
方法来切换状态。这一情况下可能会违背“开闭原则”,因为需要修改状态转换代码来增加新的状态。
命令模式( )
名称:命令模式
英文名称:
Command Pattern
/Action Pattern
/Transaction Pattern
类型:行为型模式
在软件设计中,我们经常需要向某些对象发送请求,但并不需要知道请求的接收者是谁,也不需要知道请求的具体实现细节。命令模式就是为了解决这一问题而设计的,使请求的发送和接收解耦。
命令模式是一种行为型模式,它将一个请求封装为一个对象。一般包含以下五个角色:
- 抽象命令
Command
:声明了一个执行操作的接口。 - 具体命令
ConcreteCommand
:实现了抽象命令接口,定义了与接收者之间的绑定关系,并调用接收者的相应操作。 - 接收者
Receiver
:知道如何实施与执行一个请求相关的操作。 - 调用者
Invoker
:负责调用命令对象执行请求。它不需要知道命令的具体实现细节,只需要知道如何调用命令对象的执行方法。 - 客户端
Client
:保存相应的接收者和调用者,并创建具体命令对象。之后,客户端将具体命令对象与接收者关联起来,并将命令对象传递给调用者。

命令模式的一个例子
命令模式引入了抽象命令的接口,使得命令的调用者可以针对抽象命令接口进行编程,只有实现了具体命令才能与接收者产生关联。此外,命令的调用者和接受者之间相对独立,调用者不需要知道命令的具体实现细节、命令的执行时间、命令的执行过程,只需要知道如何调用命令对象的执行方法即可。
抽象命令接口可能还包含一些其他方法,例如 undo()
方法,用于撤销命令的执行。这样,命令模式还可以支持命令的撤销和重做操作。
使用命令模式的优点包括:解耦了请求的发送者和接收者,使得请求的发送者不需要知道请求的接收者是谁,降低了系统的耦合度;可以将请求封装为对象,从而支持请求的排队、记录日志、撤销等操作;符合开闭原则,新的命令可以很容易地加入到系统中。缺点在于:使用命令模式可能会导致某些系统有过多的具体命令类。
当系统需要将请求调用者和请求接收者解耦,或需要支持请求的撤销、重做、排队、组合与定时执行等功能时,可以考虑使用命令模式。常见的应用场景包括:图形用户界面
(同一功能按钮的不同操作)等。
观察者模式( )
名称:观察者模式
英文名称:
Observer Pattern
/Publish-Subscribe Pattern
/Model-View Pattern
/Source-Listener Pattern
/Dependents Pattern
类型:对象行为型模式
考虑你正在开发一个天气预报应用程序。这个应用程序需要能够实时更新天气信息,并将这些信息推送给所有订阅了该应用程序的用户。每当天气信息发生变化时,所有订阅者都需要收到通知,以便他们可以及时获取最新的天气信息。这时,我们就可以使用观察者模式来建立它们的依赖关系。
观察者模式是一种对象行为型模式,它定义了对象间的一对多响应式依赖关系,使得当一个对象(主题/观察目标)的状态发生变化时,其相关的所有依赖对象(观察者)都会收到通知并自动更新。一般来说,观察者模式包含以下几个角色:
- 主题/观察目标
Subject
:定义了一个抽象类,用于注册、注销和通知观察者。它有一个观察者列表,保存所有的观察者对象。它还应有一方法notify()
来通知所有注册的观察者。 - 具体主题
ConcreteSubject
:实现了主题接口,维护了观察者列表,并在状态发生变化时通知所有观察者。 - 观察者
Observer
:定义了一个接口update()
,用于接收主题的通知。 - 具体观察者
ConcreteObserver
:实现了观察者接口,定义了在接收到主题通知时的具体行为。

观察者模式的一个例子
何时调用 notify() 方法?
notify()
方法通常在主题的状态发生变化时调用。具体来说,当主题的状态发生变化时,主题会遍历观察者列表,调用每个观察者的 update()
方法,以通知它们状态的变化。但是这种方法可能会在主题频繁更新时产生性能问题。实际应用中,还可以使用一些防抖或节流的技术来控制通知的频率,或者由客户端决定何时调用 notify()
方法。
使用观察者模式,可以实现主题和观察者之间的松耦合,即二者可交互但不必知道对方的具体实现细节。这样的设计可以实现表示层和数据逻辑层的分离,并定义了稳定的消息更新传递机制,抽象了更新接口,使得可以有各种各样不同的表示层作为具体观察者角色。此外,观察者模式还可以支持动态添加和删除观察者,符合开闭原则。观察者模式还可以实现广播机制,可以很好地实现一对多绑定关系。
然而,观察者模式也有一些缺点。前文所说的广播机制可能会导致性能问题,尤其是在观察者数量较多时,通知所有观察者可能会消耗大量资源。此外,观察者模式还可能导致循环依赖,即观察者和主题之间相互依赖,导致系统无限更新。最后,主题和观察者之间的松耦合会导致观察者不知道主题具体的变化原因等具体细节,而只能知道发生了变化。
观察者适用的场景包括:两个相互依赖的对象需要松耦合地交互;一个对象的状态变化需要通知多个其他对象;一个对象的变化需要通知其他对象,但并不需要知道这些对象具体是谁。MVC
模式中的模型-视图关系就是一个典型的观察者模式应用场景。此外,Java 中的 java.util.Observer
接口和 java.util.Observable
类也使用了观察者模式。
观察者模式扩展
当目标和观察者的依赖关系十分复杂时,不仅需要优化通知的频率,还需要优化通知的方式。此时,可能需要一个维护这些依赖关系的对象,称为更改管理器 Change Manager
。它可以进一步减少通知的工作量。例如,如果一个操作涉及到对几个相互依赖的目标进行改动,那么仅会在所有目标都完成改动后,才会通知观察者。Change Manager
可以通过一些图上算法来优化通知的方式。

使用 Change Manager 的观察者模式 (省略主题和观察者的具体类)
中介者模式( )
名称:中介者模式
英文名称:
Mediator Pattern
类型:对象行为型模式
在聊天系统中,用户对象之间的关系十分复杂,且存在较强的关联性。若没有其他对象的支持,用户对象的变化将很难处理,新增用户对象也很困难,用户对象的职责混乱。为了减少对象两两之间复杂的引用关系,使之成为一个松耦合的系统,我们需要使用中介者模式。
中介者模式是一种对象行为型模式,它通过引入一个中介者对象来封装一组对象之间的交互。中介者对象负责协调这些对象之间的通信,从而使得这些对象之间不需要直接引用彼此,实现了松耦合。此外,对象之间的交互还可以通过中介者对象进行独立地扩展和修改。终结者模式通常包含以下几个角色:
- 抽象中介者
Mediator
:定义了与各个同事对象交互的接口。 - 具体中介者
ConcreteMediator
:实现了抽象中介者接口,维护了各个同事对象的引用,并协调它们之间的交互。 - 抽象同事类
Colleague
:定义了一个接口,用于与中介者进行交互。 - 具体同事类
ConcreteColleague
:实现了抽象同事类的接口,并通过中介者与其他同事对象进行交互。
中介者模式的一个例子
这当中,中介者担任了两方面的职责。一方面是结构性的中转作用,减少了同事对象之间的直接引用关系;另一方面是行为上的协调作用,中介者可以进一步对同事之间的关系和行为进行封装,同事可以一致的和中介者进行交互,而不需要关心中介者和相关同事对象的具体实现细节。
中介者模式简化了对象之间的交互,降低了系统的耦合度,减少了子类的数量,符合单一职责原则。它还可以简化同事类的设计和实现。然而,中介者模式也有一些缺点。首先,它可能会导致中介者类的职责过于复杂,使得系统难以维护。
当系统中对象之间存在复杂且混乱的引用关系,如果不加以控制,会使不同的对象增加、难以被复用时,可以考虑使用中介者模式。此外,想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类时,也可以使用中介者模式。
中介模式是迪米特法则的一个典型应用。常见的应用场景包括:聊天系统
、MVC
模式中的 Controller
等。
使用了中介者模式的聊天系统
模版方法模式( )
名称:模版方法模式
英文名称:
Template Method Pattern
类型:类行为型模式
模版方法模式是一种类行为型模式,它定义了一个操作中的算法的骨架,而将一些步骤延迟到子类中。模版方法允许子类在不改变算法结构的情况下重新定义算法中的某些特定步骤。模版方法模式基于面向对象的继承机制,我们需要准备一个抽象类,定义一个模版方法控制整个算法的执行流程,将一些较为固定的逻辑放在具体方法中,而将一些变化的逻辑放在抽象方法中,由子类来实现。模版方法模式通常包含以下几个角色:
- 抽象类
AbstractClass
:定义了一个模版方法,包含了算法的骨架。此外,还有一些基本方法Primitive Method
,包括:具体方法、抽象方法和钩子方法。模版方法调用了一些抽象方法和具体方法。- 具体方法
concreteMethod
:在抽象类中实现的具体逻辑,通常是一些不需要子类重写的包含固定逻辑的方法。 - 抽象方法
abstractMethod
:在抽象类中声明但不实现的方法,子类需要实现这些方法来提供具体的算法步骤。通常是一些变化的逻辑。 - 钩子方法
hookMethod
:在抽象类中实现的可选方法,子类可以选择重写或不重写。钩子方法通常可以提供子类的额外功能或对父类模版方法行为的调整(子类对父类的反向控制)。钩子方法可以对其他方法进行约束(如返回boolean
值来影响控制流),或者提供一些默认的实现。
- 具体方法
- 具体类
ConcreteClass
:继承自抽象类,实现了抽象方法,提供了算法的具体实现。
模版方法模式
模版方法模式是一种代码复用的基本技术,通过定义模版方法和一些具体方法来实现代码复用。此外,模版方法模式还可以封装变化,在一个类中抽象地定义算法,而由它的子类实现可变化的细节的处理。模版方法满足开闭原则,只需要增加一个新的子类,就可以扩展算法的行为。模版方法的缺点在于每个不同的实现都需要定义一个子类,这会导致类的个数增加,从而增加系统的复杂性。然而,这种增加的复杂性通常是可以接受的,因为模版方法模式很好地符合单一职责原则,提供了清晰的结构和可维护性。
好莱坞原则
模版方法模式用到的一个重要原则是好莱坞原则(Hollywood Principle),即 Don't call us, we'll call you
。这意味着,子类不需要主动调用父类的方法,而是由父类在适当的时候调用子类的方法。这种设计使得子类可以专注于实现自己的逻辑,而不需要关心父类的具体实现细节。
模版方法模式适用于对复杂的算法进行分割和提取重复代码,确定算法的骨架并封装变化的逻辑。常用的场景有:框架初始化(如 Spring
、Vue.js
等),以及 JUnit
测试框架中的测试用例执行流程等。
结构型模式
TODO soon
适配器模式( )
名称:适配器模式
英文名称:
Adapter Pattern
/Wrapper Pattern
类型:类/对象结构型模式
在软件开发的过程中,客户端经常需要访问许多目标类的接口来获取其提供的服务。现存的类或许可以满足客户端的功能需要,但是很有可能其接口不是客户端想要的。在这种情况下,现有的接口需要转化为客户类期望的接口,这样保证对现有类的重用。此时,我们就可以使用适配器模式。
适配器模式是一种类或对象结构型模式,它通过引入一个适配器类来将一个类(又称适配者)的接口转换成客户端所期望的另一个接口,把客户端的直接请求转化为对适配者的相应接口的调用。适配器类可以选择直接继承适配者类(类适配器),也可以选择持有适配者类的实例(对象适配器),从而实现对适配者类的包装。适配器模式通常包含以下几个角色:
- 目标抽象类
Target
:定义了客户端所期望的接口。适配器类需要实现这个接口。 - 适配者类
Adaptee
:定义了一个现有的接口。 - 适配器类
Adapter
:实现了目标抽象类的接口,并持有一个适配者类的实例或继承自适配者类。适配器类将客户端的请求转换为对适配者类的调用。 - 客户端
Client
:使用目标抽象类的接口来与适配器类进行交互。
类适配器
对象适配器
适配器模式的优点为:解耦了目标类和适配者类,无需修改现有的代码就可以使其适配新的接口;提高了代码的复用性,可以让客户端复用适配者类的实现;符合开闭原则,可以在不修改现有代码的情况下增加新的适配器类。
对于类适配器模式,适配器类甚至还可以 override
适配者类的方法,进一步提高了灵活性。但是如果语言本身不支持多重继承,那么就不能将适配者类及其子类均进行适配(因为适配器类只能继承自一个类)。
对于对象适配器模式,适配器类可以持有多个继承自同一父类的适配者类的实例,也就可以将适配者类及其子类均进行适配。缺点是适配器类难以 override
适配者类的方法,除非新增一个适配者类的子类对其方法进行修改,然后再针对这个间接的子类进行适配,较为繁琐。
当系统需要使用现有的一些不符合接口需要的类,或者将一些没有太大关联但功能相近的类进行聚合时,可以考虑使用适配器模式。常见的应用场景有:JDBC
接口等。
适配器模式的变种
- 默认适配器模式(Default Adapter Pattern):也叫单接口适配器模式。在适配器类和目标接口之间新增一个默认适配器抽象类,其所有方法留空。当目标接口有多个方法时,由于适配器类实际继承默认适配器类,因此可以只实现其中的一部分方法,其他方法则由默认适配器类提供默认实现。默认适配器模式支持类适配器和对象适配器两种形式。
默认(对象)适配器
- 双向适配器模式(Bidirectional Adapter Pattern):适配器类可以同时适配两个不同的接口,使得两个接口之间可以互相调用对方,类似于一个双向的桥梁。比如,前后端需要双向传输数据,但前后端的数据格式不一致,可以使用双向适配器模式来实现数据的转换。如果语言不支持多重继承,那么双向适配器模式通常需要使用对象适配器的形式来实现。
前后端双向数据转换 —— 一个双向(对象)适配器的例子
如上图所示,当使用 handleBackendRequest()
方法时,我们希望将 request
送到 FrontendService
进行处理,然后再将结果返回。但是 FrontendService
中对应方法的返回值都是 FrontendData
类型。此时,我们可以使用双向适配器模式来实现 BackendData
和 FrontendData
之间的双向数据转换并对数据进行处理。
组合模式( )
名称:组合模式
英文名称:
Composite Pattern
/Part-Whole Pattern
类型:对象结构型模式
我们经常需要处理一些树形结构的数据,比如文件系统、组织结构等。在这些数据中,通常有一些叶子节点(即叶子对象,如文件、员工)和一些非叶子节点(即容器对象,如文件夹、部门)。显然,叶子节点和非叶子节点的处理方式是不同的,但在许多情况下,客户端希望能够统一地处理这些节点,而不需要关心它们的具体类型。这时,我们就可以使用组合模式。
组合模式是一种结构型模式,它允许将对象组合成树形结构来表示“部分-整体”的层次结构。组合模式使得客户端可以统一地对待叶子对象和容器对象。组合模式通常包含以下几个角色:
- 抽象构件
Component
:定义了叶子对象和容器对象的共同接口。通常包含一些通用的属性和方法,如add()
、remove()
、getChild()
、operation()
等。 - 叶子构件
Leaf
:实现了抽象构件的某些接口,表示树形结构中的叶子节点。叶子对象通常不包含子节点,因此不需要实现add()
和remove()
等子节点相关方法。 - 容器构件
Composite
:实现了抽象构件的绝大部分接口,表示树形结构中的非叶子节点。容器对象可以包含多个子节点(叶子对象或其他容器对象),因此需要实现子节点相关方法。 - 客户端
Client
:使用抽象构件的接口来操作叶子对象和容器对象。客户端可以针对抽象构件类进行编程,而不需要关心具体的叶子对象和容器对象的类型。
组合模式的一个例子
一般来说,组合模式中的容器构件包含一个子节点列表用来与抽象构件类建立聚合的关系,从而形成递归的树形结构。
组合模式可以清楚地定义分层次的复杂对象,也使得增加新构件变得容易。它可以简化客户端代码,使得客户端可以统一地对待叶子对象和容器对象;其递归结构使得组合模式可以轻松地处理树形结构的数据。组合模式还符合开闭原则,可以在不修改现有代码的情况下增加新的叶子构件类、容器构件类以及新的组合构件类(即诸多抽象构件的组合体)。
然而,组合模式也有一些缺点。首先,组合模式可能会使得系统的设计变得过于抽象,不一定适合复杂的业务场景;此外,较难对容器构件中聚合的构件类型(包括叶子构件、容器构件与组合构件)进行检查和限制。
当需要表示对象整体或部分层次结构,并希望忽略整体和部分之间的差异来简化设计时,可以考虑使用组合模式。另外,当对象的结构可能动态变化并且复杂程度不一样,但是客户最终需要统一处理时,也可以使用组合模式。常见的应用场景包括:文件系统
、图形界面库组件设计
、XML/JSON 解析
等。
组合模式的扩展
更复杂的组合模式:可以再进一步将叶子构件和容器构件分别抽象出基类,然后再进一步细化容器构件和叶子构件的类型。
透明组合模式
Transparent Composite Pattern
:即在叶子构件中也实现抽象构件的所有接口,但是与子节点相关的方法(如add()
、remove()
等)抛出异常。与之相对的即安全组合模式,也就是前文所述的组合模式。
桥接模式( )
名称:桥接模式
英文名称:
Bridge Pattern
/Handle and Body Pattern
/Interface and Implementation Pattern
类型:对象结构型模式
对于有两个变化维度的系统,如果使用继承来处理层次关系,产生的类过多。通常需要将这两个维度分离开来,将继承关系转变为关联关系,以便降低耦合,独立地扩展和修改。这就是桥接模式的基本思路。
桥接模式是一种结构型模式,它通过引入一个实现类接口并用关联代替继承,来将抽象部分与实现部分分离,使得二者可以独立地变化。桥接模式通常包含以下几个角色:
- 抽象类
Abstraction
:定义了一个抽象接口,并包含一个对实现类接口的引用。它通常包含一些通用的方法,并将这些方法委托给实现类接口来实现。 - 扩展抽象类
RefinedAbstraction
:继承自抽象类,可以添加一些额外的功能或修改抽象类的方法。 - 实现类接口
Implementor
:定义了一个接口,包含一些具体的方法。它通常是一个较为简单的接口,提供了实现部分的基本功能。 - 具体实现类
ConcreteImplementor
:实现了实现类接口,提供了实现部分的具体功能。
跨平台视频播放器——桥接模式的一个例子
桥接模式中,抽象类和实现类接口之间通过关联而非继承进行连接,从而分离了抽象和实现,形成一个桥接关系,很好地发挥了合成复用原则。同时桥接模式提高了系统的可扩充性,抽象部分和实现部分均可以独立地扩展和修改,符合开闭原则。此外,桥接模式还可以减少类的个数,避免了多层继承带来的复杂性。而且实现的细节对客户端是透明的,客户端只需要关心抽象类的接口即可。
然而,桥接模式可能会增加系统的设计和理解的复杂程度,并且如果对两个维度拆分失误,可能会导致系统的可维护性降低。因此,在使用桥接模式时,需要仔细考虑抽象部分和实现部分的划分。
当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时,或避免因为多层次继承导致的类爆炸时,可以考虑使用桥接模式。常用于:跨平台应用程序
、图形界面库
等。
桥接模式与适配器模式的联动
桥接模式和适配器模式都是结构型模式,但它们的目的不同。适配器模式是将一个类的接口转换成客户端所期望的另一个接口,而桥接模式是将抽象部分和实现部分分离,使得二者可以独立地变化。适配器模式通常用于解决接口不兼容的问题,而桥接模式则用于处理多维度变化的问题。
一般来说,在项目设计初期,会尽可能使用桥接模式来处理多维度变化的问题,以便在后续的开发中可以更容易地扩展和修改系统。而在项目设计后期,如果的确需要将某个类的接口转换成客户端所期望的另一个接口时,可以使用适配器模式来实现。
例如,下面的报表展示例子中,已经事先设计好了 ReadTextFile
和 ReadDatabase
两个类作为具体实现类来分别读取文本文件和数据库中的数据。现在由于需求变更,又需要新增一个具体实现类 ReadExcelFile
来读取 Excel 文件中的数据。但是,由于 ExcelAPI
的接口与实现类接口并不兼容,并且显然我们不能修改微软提供的 ExcelAPI
接口,因此我们需要使用适配器模式来将 ExcelAPI
的接口转换成与实现类接口相符合的借口。

报表展示
装饰器模式( )
名称:装饰器模式
英文名称:
Decorator Pattern
类型:对象结构型模式
有些时候,我们希望用一种对客户透明的方式来动态地给一个对象添加一些额外的职责或功能。此时,如果我们选择使用继承来实现,可能会导致子类的个数过多,难以维护,且职责难以动态切换。这时,我们就可以使用装饰器模式。
给一个类或者对象增加行为的方式
一般来说,有两种方式可以实现给一个类或者对象增加行为:继承和关联。
- 继承:通过继承一个类来增加行为。继承的方式通常是静态的,且在编译时就确定了类的行为,用户不能动态地控制增加行为的方式和时机。
- 关联:通过将一个类的对象嵌入到另一个对象中来增加行为,由被嵌入的对象来决定是否调用嵌入对象的行为来扩展自己的行为。
例如,前面提到的类适配器模式就是通过继承来实现的,而对象适配器模式则是通过关联来实现的。桥接模式和装饰器模式也是通过关联代替继承来实现的。
装饰器模式是一种对象结构型模式,它通过引入一个装饰器类来动态地给一个对象添加一些额外的职责或功能,而不改变对象的接口(或所属的父类),使得用户可以在运行时选择是否添加这些额外的职责或功能但不需要修改原有的代码或新增子类。装饰器模式通常包含以下几个角色:
- 抽象构件
Component
:定义了一个接口或抽象类,表示被装饰的对象或装饰器。它通常包含一些通用的方法。 - 具体构件
ConcreteComponent
:实现了抽象构件的接口,表示被装饰的对象。它通常包含一些具体的属性和方法。 - 抽象装饰器类
Decorator
:继承自抽象构件,持有一个对抽象构件的引用。装饰器类可以在调用抽象构件的方法之前或之后添加一些额外的功能或行为,也可以添加一些新的属性和方法。 - 具体装饰器类
ConcreteDecorator
:继承自装饰器类,提供了具体的装饰功能或行为。
装饰器模式的一个例子
装饰器模式可以提供比继承更多的灵活性来扩展对象的功能:可以动态地扩展一个对象的功能,而不需要修改原有的代码或新增子类;不同的具体装饰类由于均继承自同一个抽象构件类,因此可以自由组合,从而实现更复杂的功能;装饰器模式符合单一职责原则,每个装饰器类只负责添加一种特定的功能或行为;具体构件类和具体装饰类之间的关系是松耦合的,可以独立地扩展和修改,符合开闭原则。
然而,装饰器模式也有一些缺点。首先,装饰器模式可能产生很多细小对象,它们之间可能只是连接方式不同,导致对使用者的要求更高,增加了错误的可能性和排查难度。
当希望以动态、透明方式为单个对象添加额外的职责或功能时,或者使用继承的方式会导致子类个数过多、难以维护时,可以考虑使用装饰器模式。常见的应用场景包括:Java I/O
流、图形界面库
中的组件装饰等。
装饰模式的简化
当只有一个具体构件类时(这一具体构件类一般都是最基础最简单的类),可以让抽象装饰器类直接作为具体构件类的子类,而不需要再定义一个抽象构件类。此时使用的是简化的装饰器模式。
简化的装饰器模式
客户端调用方式
考虑下面的多重加密系统:
多重加密系统类图
客户端可以有两种方式进行调用:
- 透明装饰模式:要求客户端完全面向抽象编程,指客户端程序不应该声明具体构件类型和具体装饰类型,而应该全部声明为抽象构件类型。
Cipher sc, cc, ac; // 完全面向抽象编程
sc = new SimpleCipher();
cc = new ComplexCipher(sc);
ac = new AdvancedCipher(cc);
2
3
4
- 半透明装饰模式:允许客户端声明具体构件类型和具体装饰类型,也可调用在具体装饰类型中新增的属性和方法。
Cipher simpleCipher = new SimpleCipher();
ComplexCipher complexCipher = new ComplexCipher(simpleCipher);
complexCipher.reverse("123"); // 调用具体装饰类型中的方法
2
3
外观模式( )
名称:外观模式
英文名称:
Facade Pattern
类型:结构型模式
考虑一个网站的前端页面,它需要与多个后端服务进行交互。为了简化前端页面的开发,我们可以为这些后端服务提供一个统一的接口,这样前端页面就不需要关心后端服务的具体实现细节了。这就是外观模式的基本思路。引入了外观模式,客户只需要直接和外观角色(即上文中统一的接口)进行交互,而不需要关心子系统的具体实现细节。客户和子系统之间的复杂关系由外观角色来处理。
外观模式是一种对象结构型模式,客户和子系统之间的通信由一个统一的外观对象来实现,这一外观对象为子系统提供了一个一致的界面。外观模式通常包含以下几个角色:
- 外观角色
Facade
:提供一个统一的接口,供客户调用。它通常包含一些方法,这些方法会调用子系统中的多个方法来完成某个功能。 - 子系统角色
Subsystem
:子系统角色通常包含一些具体的实现逻辑,供外观角色调用。客户有时也可以直接调用子系统角色的方法,但通常不建议这样做,因为这会增加客户和子系统之间的耦合关系,使得外观模式失去意义。
外观模式的一个例子
一个系统中也可以有多个外观角色,每个外观角色可以为不同的子系统提供统一的接口。如果需要的话,还可以为外观角色定义一个抽象外观类,以便于扩展和修改。
外观模式时迪米特法则的一个很好的应用,它通过引入一个新的外观角色来减少客户和子系统之间的耦合关系。此外,外观模式还支撑了单一职责原则,让子系统的职责分配清晰,子系统之间的通信和依赖关系减小,各个子系统之间相对独立,而由外观角色来处理向客户端暴露的最终接口。
外观模式可以对客户屏蔽子系统组件,减少客户的开发负担,让客户代码变得简洁清晰,实现了子系统与客户之间的松耦合关系。此外,外观模式还很适用于大型软件系统,方便修改和编译各个子系统,降低了大型软件系统的编译依赖性,简化移植过程。最后,外观模式只是提供了客户访问的统一接口,但用户仍可使用子系统类的接口来细粒度使用子系统的功能。
然而,外观模式也有一些缺点。首先,在不引入抽象外观类的前提下,外观模式并不符合开闭原则,因为如果子系统发生变化,外观角色也需要修改;其次,外观模式不能很好地限制客户使用子系统类。
当希望将客户和多个子系统进行解耦时,或者要为复杂子系统提供一个简单的接口供大多数用户使用时,可以考虑使用外观模式。此外,在分层架构中,外观模式也可以用来简化层与层之间的交互。
不要通过外观类为子系统增加行为
外观模式的目的是为了简化客户和子系统之间的交互,而不是为子系统增加新的行为。如果需要为子系统增加新的行为,应当修改原有的子系统类或者新增子系统类,而不是通过修改或继承现有外观类进行。
享元模式( )
名称:享元模式
英文名称:
Flyweight Pattern
类型:对象结构型模式
有时我们需要使用大量的对象来表示一个系统中的某些实体,但这些对象之间有很多相同的部分。为了节省内存和提高性能,我们可以将这些相同的部分抽取出来,放在一个共享的对象中,这就是享元模式的基本思路。
享元模式是一种对象结构型模式,它运用共享技术来有效地支持大量细粒度的相似对象的复用。它通常包含以下几个角色:
- 抽象享元类
Flyweight
:定义了享元对象的接口,通常包含一些通用的方法。 - 具体享元类
ConcreteFlyweight
:实现了抽象享元类的接口,表示具体的享元对象。它通常包含一些具体的属性和方法,并且只包含内部状态,是可以共享的。 - 非共享具体享元类
UnsharedConcreteFlyweight
:有时也需要一些不需要共享的享元对象,这类对象通常包含客户提供的外部状态,并且通常是具体享元类的组合。 - 享元工厂类
FlyweightFactory
:负责为客户端提供创建和管理具体享元对象的方法。它通常包含一个享元对象池,用来存储已经创建的具体享元对象,并提供一个方法来获取具体享元对象。
享元模式的一个例子
这是一个利用享元模式设计的文本编辑器的例子。我们将带字体和大小的文本字符作为享元对象 ConcreteCharacter
,因为它们可以共享;而文本字符的颜色和位置由于很难复用,因此作为外部状态,放在非共享具体享元类 CharacterContext
中。这里的非共享具体享元类还对享元对象 ConcreteCharacter
进行组合,兼顾了灵活性与复用性。在这一例子中,享元工厂类提供了一个用于存储享元对象的池以及一个用户获取享元对象的方法(类似缓存机制),其伪代码如下:
class FlyweightFactory {
// 享元对象池
private Map<String, Flyweight> flyweights = new HashMap<>();
public Flyweight getFlyweight(String key) {
if (!flyweights.containsKey(key)) {
flyweights.put(key, new ConcreteFlyweight(key));
}
return flyweights.get(key);
}
}
2
3
4
5
6
7
8
9
10
11
由此可见,享元模式的优点在于它可以大幅度减少内存的使用,相同或相似的对象可以”合并同类项“进行存储。内部状态和外部状态的划分让享元对象可以在不同环境、不同层次上进行复用。缺点在于:享元模式中内部和外部状态的划分使系统的逻辑变得复杂,运行的时间也会变长。
当系统需要重复使用大量的相似对象,并且对象的大部分状态都可以外部化时,可以考虑使用享元模式。常见的应用场景包括:文本编辑器
、图片多次渲染
、字符串常量优化
等。
享元模式的变种
- 单纯享元模式(Pure Flyweight Pattern):所有的状态都可以外部化,享元对象只包含内部状态,因此所有享元对象都是可以共享的,这种享元对象称为单纯享元对象。单纯享元模式不存在非共享具体享元类。
- 复合享元模式(Composite Flyweight Pattern):运用组合模式,享元对象可以包含其他享元对象,形成一个具有树形结构的复合享元对象。并且复合享元对象最终可以分解为单纯享元对象。
复合享元模式
代理模式( )
名称:代理模式
英文名称:
Proxy Pattern
/Surrogate Pattern
类型:结构型模式
在某些情况下,出于安全等层面的考量,客户不想或者不可能直接引用一个对象,此时我们可以通过一个第三方代理对象来间接引用这个对象。代理对象既起到了中介的作用,还可以在调用被代理对象的方法之前或之后添加一些额外的功能或行为。这就是代理模式的基本思路。
代理模式是一种结构型模式,它给一个对象提供一个代理对象,以控制对这个对象的访问。代理模式通常包含以下几个角色:
- 抽象主题类
Subject
:定义了代理和被代理对象的共同接口。它通常包含一些通用的方法。 - 真实主题类
RealSubject
:实现了抽象主题类的接口,表示被代理的对象。它通常包含一些具体的属性和方法。 - 代理类
Proxy
:实现了抽象主题类的接口,持有一个对具体主题类的引用。代理类可以在调用具体主题类的方法之前或之后添加一些额外的功能或行为,也可以控制对具体主题类的访问。
代理模式的一个例子
从结构上来看,代理模式能够协调调用者和被调用者,将其解耦。此外,从业务层面来看,代理模式还可以使客户端通过访问一个小对象来操控大对象,从而节省资源;对代理对象的访问实际上还可以保护真实对象的隐私和使用权限,增强系统的安全性。
代理模式同样有一些缺点:有些代理模式可能会造成请求的延迟;实现代理模式需要额外的代码,有时十分难实现。
代理模式的不同应用
代理模式有多种不同的应用场景,常见的有以下几种:
- 虚拟代理
Virtual Proxy
:用于延迟加载或节省资源。虚拟代理通常在需要时才创建真实对象,从而节省资源。例如,图片加载时可以先显示一个占位符或低分辨率图片,等图片加载完成或用户点击查看再显示真实的图片。 - 远程代理
Remote Proxy
:用于访问远程对象。远程代理通常在客户端和服务器之间传递请求和响应,从而实现对远程对象的访问。客户端可以操纵本地的代理对象,再由代理对象进行远程操控,从而让客户对远程对象透明,就好像远程对象就在本地一样。例如,Java RMI
(远程方法调用)就是一种远程代理。 - 保护代理
Protection Proxy
:用于控制对真实对象的访问权限。保护代理通常在调用真实对象的方法之前或之后添加一些额外的权限检查,从而实现对真实对象的访问控制。例如,某些敏感操作可能需要用户验证或权限检查。 - 缓存代理
Cache Proxy
:用于缓存真实对象的结果,以提高性能。缓存代理通常在调用真实对象的方法之前检查缓存中是否有结果,如果有则直接返回缓存中的结果,否则调用真实对象的方法并将结果存入缓存。例如,某些计算密集型操作可以使用缓存代理来避免重复计算。 - 智能引用代理
Smart Reference Proxy
:当一个对象被引用时,智能引用代理可以提供一些额外的功能,如引用计数、自动释放等。 - 写复制代理
Copy-on-Write Proxy
:当一个对象需要深拷贝时,写复制代理可以延迟拷贝操作,只有在需要修改对象或客户端使用时才进行深拷贝,从而节省内存和提高性能。 - 防火墙代理
Firewall Proxy
:用于保护真实对象不受恶意访问。