创建型设计模式(工厂/抽象工厂/单例/建造者/原型模式)

前言

因为设计模式种类多,且重理解重回忆,所以本文尽量言简意赅,便于时时温习。

设计模式(Design Pattern)是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。

1995年,GoF(Gang of Four,四人组/四人帮)合作出版了《设计模式:可复用面向对象软件的基础》一书,共收录了23种设计模式,从此树立了软件设计模式领域的里程碑,人称「GoF设计模式」。

这 23 种设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性,以及类的关联关系和组合关系的充分理解。

当然,软件设计模式只是一个引导,在实际的软件开发中,必须根据具体的需求来选择:
对于简单的程序,可能写一个简单的算法要比引入某种设计模式更加容易;
但是对于大型项目开发或者框架设计,用设计模式来组织代码显然更好。

我们要清楚,设计模式并不是Java的专利,它同样适用于C++、C#、JavaScript等其它面向对象的编程语言。

设计原则

开闭原则

开闭原则的含义是:当应用的需求改变时,在不修改软件实体的源代码或者二进制代码的前提下,可以扩展模块的功能,使其满足新的需求。

里氏替换原则

里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。

依赖倒置原则

依赖倒置原则的原始定义为:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。其核心思想是:要面向接口编程,不要面向实现编程。

依赖倒置原则是实现开闭原则的重要途径之一,它降低了客户与实现模块之间的耦合。

单一职责原则

单一职责原则又称单一功能原则,由罗伯特·C.马丁(Robert C. Martin)于《敏捷软件开发:原则、模式和实践》一书中提出的。这里的职责是指类变化的原因,单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分。

该原则提出对象不应该承担太多职责,如果一个对象承担了太多的职责,至少存在以下两个缺点:

  1. 一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力;
  2. 当客户端需要该对象的某一个职责时,不得不将其他不需要的职责全都包含进来,从而造成冗余代码或代码的浪费。

迪米特法则

迪米特法则的定义是:只与你的直接朋友交谈,不跟“陌生人”说话。其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。

迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。

模式分类

根据目的来分类

根据模式是用来完成什么工作来划分,这种方式可分为创建型模式、结构型模式和行为型模式3种。

创建型模式:用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。
GoF中提供了单例、原型、工厂方法、抽象工厂、建造者等5种创建型模式。
结构型模式:用于描述如何将类或对象按某种布局组成更大的结构。
GoF中提供了代理、适配器、桥接、装饰、外观、享元、组合等7种结构型模式。
行为型模式:用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,以及怎样分配职责。
GoF中提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等11种行为型模式。

根据作用范围来分类

根据模式是主要用于类上还是主要用于对象上来分,这种方式可分为类模式和对象模式两种。
类模式:用于处理类与子类之间的关系,这些关系通过继承来建立,是静态的,在编译时刻便确定下来了。
GoF中的工厂方法、(类)适配器、模板方法、解释器属于该模式。
对象模式:用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。
GoF中除了以上4种,其他的都是对象模式。

创建型模式 结构型模式 行为型模式
类模式 工厂方法 (类)适配器 模板方法、解释器
对象模式 单例、原型、抽象工厂、建造者 代理、(对象)适配器、桥接、装饰、外观、享元、组合 策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录

1 工厂模式

【介绍】

工厂模式(Factory Pattern)是Java中最常用的设计模式之一,它提供了一种创建对象的最佳方式。

在工厂模式中,我们创建对象的逻辑不会对客户端暴露,而是收口在特定类型产品对应的工厂类中。

【比喻】
您需要一辆汽车,可以直接从工厂里面提货,而不用去管这辆汽车是怎么做出来的,以及这个汽车里面的具体实现。制造汽车的逻辑都收敛在工厂中。

奔驰工厂实现了汽车工厂类,可以制造奔驰汽车,宝马工厂也实现了汽车工厂类,制造的是宝马汽车。你需要什么车,就调用对应工厂获得。

【优点】

  1. 一个调用者想创建一个对象,只要知道其名称就可以了。

  2. 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。

  3. 屏蔽产品的具体实现,调用者只关心产品的接口。

【缺点】

每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。

复杂对象适合使用工厂模式,而简单对象,特别是只需要通过new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。

【应用】

  1. Spring框架中BeanFactory,bean生成的创建逻辑,都收敛在其中。

【案例】

如下图,FactoryPatternDemo 类使用ShapeFactory来获取Shape对象。它将向 ShapeFactory 传递信息(CIRCLE/RECTANGLE/SQUARE),以便获取它所需对象的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ShapeFactory {

//使用 getShape 方法获取形状类型的对象
public Shape getShape(String shapeType){
if(shapeType == null){
return null;
}
if(shapeType.equalsIgnoreCase("CIRCLE")){
return new Circle();
} else if(shapeType.equalsIgnoreCase("RECTANGLE")){
return new Rectangle();
} else if(shapeType.equalsIgnoreCase("SQUARE")){
return new Square();
}
return null;
}
}

2 抽象工厂模式

【介绍】

抽象工厂模式(Abstract Factory Pattern)是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同组的不同产品的模式结构。

抽象工厂模式是工厂方法模式的升级版本,工厂模式只生产一个类型的产品,而抽象工厂模式集合了同组的工厂,可生产同组多个类型的产品。

抽象工厂模式主要设计产品组的概念,就是某一个工厂生产出配套的一系列产品。例如,在生产足球的同时,SoccerFactory还可以生产与之配套的足球杂志。

【比喻】

前面介绍的工厂模式中考虑的是一类产品的生产,如汽车厂只生产轿车。而这里抽象工厂模式,则是综合汽车厂的模式,它拥有汽车厂,卡车厂,特种车厂。

或者换个维度,抽象工厂模式也可以是全产业链汽车厂,它拥有汽车厂,轮胎厂,汽车车标厂等等。

奔驰车厂实现抽象工厂类,可分别生产奔驰车,奔驰轮胎和奔驰车标。我们如果需要一套奔驰牌的东西,直接调用奔驰车厂就行。宝马车厂同理。

【优点】

当一个产品组中的多个对象被设计成一起工作时,它能保证客户端从单个抽象工厂类中始终只能获取同一个产品组中的对象。如从奔驰车厂中获取的,不管是轮胎还是汽车,肯定都是奔驰牌。

我如果想替换一整套的组合,那么只要替换奔驰车厂为宝马车厂类就行了,这样汽车,轮胎和车标,都自动换成了宝马牌。

【缺点】

产品组的扩展非常困难,要增加一个系列的某一产品,既要在抽象的工厂里加代码,又要在具体的新产品类里面加代码。

【应用】

  1. java JDK中的java.sql.Connection类。MySQLConnection和OracleConnection等继承了它,并在各自的内部,实现了自己专用的Statement/PreparedStatement/CallableStatement对象。

【案例】

抽象工厂模式也就是不仅生产鼠标,同时生产键盘。PC厂商是个父类,有生产鼠标,生产键盘两个接口。戴尔工厂,惠普工厂继承它,可以分别生产戴尔鼠标+戴尔键盘,和惠普鼠标+惠普键盘。

1
2
3
4
public abstract class PcFactory {
public abstract Mouse createMouse();
public abstract Keybo createKeybo() ;
}
1
2
3
4
5
6
7
8
public class DellFactory extends PcFactory{
public Mouse createMouse(){
return new DellMouse();
};
public Keybo createKeybo(){
return new DellKeybo();
};
}
1
2
3
4
5
6
7
8
public class HpFactory extends PcFactory{
public Mouse createMouse(){
return new HpMouse();
};
public Keybo createKeybo(){
return new HpKeybo();
};
}

3 单例模式

【介绍】

单例模式(Singleton Pattern),它的定义就是确保某一个类只有一个实例,并且提供一个全局访问点。

单例模式具备典型的3个特点:1、只有一个实例。 2、自我实例化。 3、提供全局访问点。

因此当系统中只需要一个实例对象或者系统中只允许一个公共访问点,除了这个公共访问点外,不能通过其他访问点访问该实例时,可以使用单例模式。

【比喻】:无

【优点】

  1. 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
  2. 避免对资源的多重占用(比如写文件操作)。

【缺点】

  1. 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

【应用】

  1. spring框架中的ApplicationContext
  2. 数据库中的连接池

【案例】

我们将创建一个Singleton类。Singleton 类有它的私有构造函数和本身的一个静态实例。

Singleton类提供了一个静态方法,供外界获取它的静态实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 第1种:懒汉式单例,线程不安全,懒加载
// 类加载时没有生成单例,
// 只有当第一次调用getlnstance方法时才去创建这个单例。
public class Singleton {
//创建 Singleton 的一个对象
private static Singleton instance = null;

//让构造函数为 private,这样该类就不会被实例化
private Singleton(){}

//获取唯一可用的对象
public static Singleton getInstance(){
//getInstance 方法前加同步
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 第2种:饿汉式单例,线程不安全,非懒加载
// 该模式的特点是类一旦加载就创建一个单例,
// 保证在调用getInstance方法之前单例已经存在了。
public class Singleton {
//创建 Singleton 的一个对象
private static Singleton instance = new Singleton();

//让构造函数为 private,这样该类就不会被实例化
private Singleton(){}

//获取唯一可用的对象
public static Singleton getInstance(){
return instance;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 第3种:双检锁/双重校验锁单例,线程安全,懒加载
// 这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
// getInstance()的性能对应用程序很关键。
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 第4种:登记式/静态内部类,线程安全,懒加载
// 这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。
// 这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
// 这种方式同样利用了classloader机制来保证初始化instance时只有一个线程
// 它跟饿汉式不同的是:饿汉式只要Singleton类被装载了,那么instance就会被实例化,
// 而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用
// 只有通过显式调用 getInstance方法时,才会显式装载SingletonHolder类,从而实例化instance。
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}

public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 第5种:枚举,线程安全,非懒加载
// 这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
// 这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。
// 不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}

public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

一般情况下,不建议使用懒汉方式,建议使用第2种饿汉方式。只有在要明确实现lazy loading效果时,才会使用第4种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第5种枚举方式。如果有其他特殊的需求,可以考虑使用第3种双检锁方式。


4 建造者模式

【介绍】

建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。一个Builder类会一步一步构造最终的对象。该 Builder 类是独立于其他对象的。

【比喻】

将一个复杂对象分布创建。如果一个超大的类的属性特别多,我们可以把属性分门别类,不同属性组成一个稍微小一点的类,再把好几个稍微小点的类窜起来。

比方说一个电脑,可以分成不同的稍微小点的部分CPU、主板、显示器。CPU、主板、显示器分别有更多的组件,不再细分。

生活中这样的例子很多,如游戏中的不同角色,其性别、个性、能力、脸型、体型、服装、发型等特性都有所差异;还有汽车中的方向盘、发动机、车架、轮胎等部件也多种多样;每封电子邮件的发件人、收件人、主题、内容、附件等内容也各不相同。

【优点】

  1. 建造者独立,易扩展。
  2. 便于控制细节风险。

【缺点】

  1. 产品必须有共同点,范围有限制。
  2. 如内部变化复杂,会有很多的建造类。

【应用】

主要应用在如下情况中:有时候面临着”一个复杂对象”的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。

  1. java JDK的StringBuilder类。
  2. Zookeeper的Java客户端CuratorFrameworkFactory类中的Builder内部类。
  3. Spring中的BeanDefinitionBuilder类。

【案例】

以组装一台电脑为例,电脑包含了主机,操作系统,显示器三个核心部分。我们定义抽象的Builder,在生产一个具体的MacbookBuilder,用来创建MacBook。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 定义电脑的抽象类
public abstract class Computer {
protected String mBoard;
protected String mDisplay;
protected String mOs;

protected Computer(){
}
public void setBoard(String board){
mBoard=board;
}
public void setDisplay(String display) {
this.mDisplay = display;
}
public abstract void setOs() ;
}
// 定义MacBook,继承Computer
public class MacBook extends Computer{


protected MacBook() {
}
@Override
public void setOs() {
mOs="Mac OS X 12";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 抽象的Builder
public abstract class Builder {
abstract void buildBoard(String board);
abstract void buildDisplay(String display);
abstract void buildOs();
abstract Computer build();
}
// 具体的Builder
public class MacBookBuilder extends Builder {
private Computer mComputer=new MacBook();
@Override
void buildBoard(String board) {
mComputer.setBoard(board);
}

@Override
void buildDisplay(String display) {
mComputer.setDisplay(display);
}

@Override
void buildOs() {
mComputer.setOs();
}

@Override
Computer build() {
return mComputer;
}
}

Director类,负责具体的构造 Computer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Director {
Builder mBuilser=null;

public Director(Builder builer) {
this.mBuilser = builer;
}

public void construct(String board,String display){
mBuilser.buildDisplay(display);
mBuilser.buildBoard(board);
mBuilser.buildOs();
}
}

public static void main(String[] args){
Builder builder=new MacBookBuilder();
Director pcDirector=new Director(builder);
pcDirector.construct("英特尔主板","Retina显示器");

Computer computer = builder.build();
System.out.println(computer.toString());
}

5 原型模式

【介绍】

原型模式(Prototype Pattern)是用于创建重复的大对象,它实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则可以采用这种模式。

例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。

与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的。浅拷贝实现Cloneable的重写,深拷贝是通过实现Serializable读取二进制流。

【比喻】

原型模式的创建的对象是一个克隆对象,可以理解成科幻电影中的克隆机器人,直接生产一个机器人可能需要巨大的代价(比如要消耗大量的IO资源或者硬盘资源,或者调用大量的数据库数据等),那么我们就以某个已经生产出来的机器人为原型或者模板,采用克隆技术去复制它。

【优点】

  1. Java自带的原型模式基于内存二进制流的复制,在性能上比直接new一个对象更加优良。
  2. 逃避构造函数的约束。

【缺点】

  1. 需要为每一个类都配置一个clone方法
  2. clone方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违背了开闭原则。
  3. 当实现深克隆时,需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦。因此,深克隆、浅克隆需要运用得当。

【应用】

spring中,scope=”singleton”和scope=”prototype”分别表示该bean是单例模式还是非单例模式。prototype作用域部署的bean,每一次请求(将其注入到另一个bean中,或者以程序的方式调用容器的getBean()方法)都会产生一个新的bean实例,这内部就是使用了原型模式。

【案例】

我们将创建一个抽象类Shape和扩展了Shape类的实体类。下一步是定义类ShapeCache,该类把shape对象的原型存储在一个Hashtable中,并在请求的时候返回它们的克隆。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义父类shape,三个子类我们就不赘述了
public abstract class Shape implements Cloneable {

private String id;
protected String type;

...

abstract void draw();

public Object clone() {
Object clone = null;
try {
clone = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clone;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ShapeCache {
private static Hashtable<String, Shape> shapeMap
= new Hashtable<String, Shape>();

public static Shape getShape(String shapeId) {
Shape cachedShape = shapeMap.get(shapeId);
return (Shape) cachedShape.clone();
}

// 对每种形状都运行数据库查询,并创建该形状
// shapeMap.put(shapeKey, shape);
// 例如,我们要添加三种形状
public static void loadCache() {
Circle circle = new Circle();
circle.setId("1");
shapeMap.put(circle.getId(),circle);

Square square = new Square();
square.setId("2");
shapeMap.put(square.getId(),square);

Rectangle rectangle = new Rectangle();
rectangle.setId("3");
shapeMap.put(rectangle.getId(),rectangle);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
// PrototypePatternDemo 使用 ShapeCache 类来获取存储在 Hashtable 中的形状的克隆。
public class PrototypePatternDemo {
public static void main(String[] args) {
ShapeCache.loadCache();

Shape clonedShape = (Shape) ShapeCache.getShape("1");

Shape clonedShape2 = (Shape) ShapeCache.getShape("2");

Shape clonedShape3 = (Shape) ShapeCache.getShape("3");
}
}
0%