设计模式
设计模式是对常见设计的总结和抽象,既是一种解决问题的思路,同时也是一种提高沟通效率的工具。按照作用不同,可以将设计模式分为三类:创建型模式、结构型模式和行为型模式。而按照结构来说,可以分为类模式和对象模式。
本文将介绍一些常见的设计模式。
策略模式( )
让我们考虑一个名为 SimDuck
的模拟鸭子游戏,这一游戏用于展示各种不同鸭子来愉悦心情。这个应用程序的主要需求即模拟各种类型的鸭子,例如野鸭、红头鸭、诱饵鸭等。这些鸭子都有一些共同的行为,例如游泳、飞行等,但是它们也有一些不同的行为,例如呱呱叫、吱吱叫等。有时,我们需要为某些鸭子添加新的行为(即变化),例如潜水。我们如何设计这个应用程序,使得它既能够保持灵活性,又能够避免代码重复呢?
继承并重写(override
)是一种解决方案。所有鸭子都继承 BaseDuck
父类,那么在添加新的行为时就可以在父类中添加新的方法,然后在子类中重写这个方法。但是这种方法有一个缺点,即我们需要为未添加这一行为的鸭子重写这个方法,这样会导致代码重复。另一种解决方案是使用接口,即为每一种行为定义一个接口,然后在每一个鸭子类中实现这些接口。但是这种方法也有一个缺点,即如果我们需要为某些鸭子添加新的行为,那么我们需要修改所有添加这一行为的鸭子子类,这也不是一个完美的方法。
我们重新从设计的角度考虑,可以发现这些方式都没有从变化的角度出发。先前的设计都是直接在实现部分进行修改,而并没有为变化的部分进行封装。可以看出,易变的部分是鸭子的行为,而不是鸭子的类型。因此,我们可以将鸭子的各类行为抽象出来,然后将其封装到几个类中。这样,当我们需要为某些鸭子添加新的行为时,我们只需要修改这个行为抽象类即可。在不同的鸭子类中,我们只要按需引入这些行为抽象类,然后调用它们的方法即可。这一设计是依赖倒置原则、开闭原则和合成复用原则的体现。
如果将鸭子看作是一个上下文,而将鸭子的行为看作是一个策略,那么,这一设计模式就是经典的策略模式(Strategy
或 Policy
的命名。
策略模式的结构如下:
- 策略接口
Strategy
:定义了所有支持的算法的公共接口。通常是一个接口或抽象类。 - 具体策略
ConcreteStrategy
:实现了策略接口的具体算法。
策略模式的特点有:
- 可维护性和可复用性强,易于扩展。
- 策略可以在运行时进行替换。
- 然而,客户需要对策略有了解,知晓每一个策略的优缺点,从而选择合适的策略。
工厂模式
简单工厂模式( )
考虑一个可以提供不同类型按钮的组件库,不同的按钮继承自同一个按钮基类,这些按钮都对基类的样式进行了相应的修改。如果我们不希望了解按钮派生类的具体信息,而只希望通过相应的参数来获取(创建)新的派生类。这时就可以采用简单工厂模式。
简单工厂模式又称为静态工厂模式,属于创建型模式。在简单工厂模式中,可以通过参数不同返回不同的实例。
简单工厂模式的结构如下:
- 工厂角色
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
工厂方法模式的缺点在于添加新产品时需要添加新的工厂类,这样会导致类的数量增加,增加了系统的复杂度,并提高了编译运行的代价。
抽象工厂模式( )
无论是简单工厂模式还是工厂方法模式,它们都只能创建同一类产品(即继承自同一接口或抽象类的产品)。如果我们需要创建多个不同类别或位于不同等级结构的产品,那么我们就需要使用抽象工厂模式。
抽象工厂模式提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。抽象工厂模式又称为 Kit
模式,同样属于对象创建型模式。
产品族和产品等级结构
- 产品族:指由同一个工厂生产的,位于不同产品等级结构中的一组产品。
- 产品等级结构:指产品的继承结构。代表着不同类别的产品。
抽象工厂模式解决的问题是多种类产品和产品类之间等级结构的问题,是所有形式的工厂模式中最为抽象和最具一般性的一种形态。它与工厂方法模式的区别就在于,工厂方法模式针对的是一个产品等级结构,而抽象工厂模式则需要面对多个产品等级结构。
抽象工厂模式的结构如下:
- 抽象工厂
AbstractFactory
:最高层次的抽象,定义了工厂的公共接口。 - 具体工厂
ConcreteFactory
:实现了抽象工厂定义的接口,负责创建一个产品族中的产品。如下图中的WindowsFactory
、UnixFactory
等。 - 抽象产品
AbstractProduct
:定义了一种类别的产品。抽象产品可以有很多个。如下图中的Button
、Text
。 - 具体产品
ConcreteProduct
:实现了抽象产品定义的接口,是一个具体的产品。如下图中的WindowsButton
、UnixButton
等。

抽象工厂模式的一个例子
抽象工厂模式同样隔离了具体类的生成,使得客户并不需要知道什么被创建。此外,当一个产品族的多个对象被设计为一起工作时,能够保证客户端始终只使用同一个产品族中的对象,这对于一些需要根据不同的环境创建不同的对象的系统来说是非常有用的。另外,和工厂方法模式一样,抽象工厂模式也是符合开闭原则的,当需要增加一个新的产品族时,只需要增加一个新的具体工厂类即可。
抽象工厂模式的缺点在于,难以扩展抽象工厂来支持新种类的产品,这是因为支持新种类的产品就要对抽象工厂角色及其所有子类的修改,显然是不方便的。抽象工厂增加新的工厂和产品族较为方便,但是增加新的产品等级结构就比较麻烦。因此如果系统的变化主要体现在新增产品族上,抽象工厂模式是一个很好的选择,否则需要慎重考虑。这被称为开闭原则的倾斜性。
建造者模式( )
无论是在现实世界还是在软件系统中,都存在一些具有多种组成成分的复杂对象。对于大多数用户来说,它们无需知道这些对象的内部构造细节和组合这些组件的过程,只需要知道如何创建并使用这些对象。
建造者模式将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。建造者模式是一种创建型模式。它可以一步一步构造一个复杂的对象,用户不需要知道具体的建造过程和细节,只需要知道应当使用哪一个建造者即可创建相应的产品。新增产品时,只需要新增一个具体建造者即可,不需要修改原有的代码,符合开闭原则。
建造者模式的结构如下:
- 产品
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
:声明一个克隆自身的接口。 - 具体原型
ConcretePrototype
:实现Clone
方法,这一方法复制自身。 - 客户
Client
:让一个原型克隆自身从而创建一个新的对象。

原型模式的一个例子
使用原型模式克隆对象时,根据其成员对象的克隆情况,分为浅克隆和深克隆两种。浅克隆时,只复制对象本身,对于对象内部的引用类型,只是复制了引用,而不克隆其指向的对象;深克隆则是对对象本身以及对象内部的引用类型指向的对象都进行了克隆。
在 Java
中,可以通过实现 Cloneable
接口和重写 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()
中自行实现对引用类型的拷贝。
总而言之,原型模式简化了对象的创建过程,提供了简化的创建结构(创建时不需要过度关心对象的类型与层次),提高了新实例的创建效率。使用深拷贝,还可以保存对象的状态(相当于快照)。然而,原型模式的缺点表现为需要为每一个类配备一个克隆方法(违反开闭原则),并且实现深克隆较为复杂。
原型模式较为适用的情况包括:
- 创建新对象成本较大,通过复制原型对象得到新实例再修改可能比使用构造函数创建一个新实例更加方便;
- 系统要保存对象的状态,而对象的状态变化很小;
- 需要避免使用分层次的工厂类来创建分层次的对象;