设计模式扫盲系列(二):创建型模式集

【引言】此篇主要讨论5种常见的创建型模式:工厂模式(Factory Method Pattern)、抽象工厂模式(Abstract Factory Pattern)、单例模式(Singleton Pattern)、建造者模式(Builder Pattern)、原型模式(Prototype Pattern)。点击查看完整示例代码


工厂模式(Factory Method Pattern)


定义

  工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一,它也有不同的变种(比如:简单工厂、抽象工厂);在工厂模式中工厂方法代替了传统的new操作(对象的创建就由工厂来负责而不需要我们主动去new了)。
  在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑(隐藏实现,面向接口编程),并且是通过使用一个共同的接口来指向新创建的对象。

分类

  通俗的理解,工厂模式可以粗粒度的分两种:

  • 第一种:简单工厂模式,顾名思义就是简单,这种情况下通常只有一个工厂,但是其中有多条生产线(逻辑分支),不同的生产线可以生产不同的具体产品,你只需要告诉工厂你想生产那种产品(输入参数)就可以了。
  • 第二种:工厂模式,这种类别更具通用性,一般有多个工厂,每个工厂都可以生产一种特定的产品,你需要哪种产品,就直接找到对应产品的生产工厂就可以了。

角色

  工厂方法模式的主要角色如下。

  • 抽象工厂(Creator):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法 factoryMethod() 来创建产品。
  • 具体工厂(ConcreteCreator):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
  • 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
  • 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。

  针对以上四个角色(有时候会有一些简化,比如取消了抽象产品);实际给客户端使用时,对外只需要暴露Creator这个接口就可以(实际的创建逻辑外部是不需要知道的),通过这个工厂接口即可完成Product(对应的具体产品)的创建过程。

类图

实践

产品接口

  通常使用一个接口定义产品的抽象功能,因为我们提倡的是面向接口编程,同一类型的产品可以属于同一个接口,这样便于产品的扩展,而且也可以尽量减少产品变化引起的客户端的调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 抽象产品接口
*
* @author Lin.C
* @date 2019/5/28 7:46
*/
public interface Product {

/**
* 功能接口,可能有多个
*/
void doSomething();
}

产品实体

  产品实体可以定义很多个,这里就列一个例子,不同的产品实体都是实现了产品接口的,区别就是对产品定义的功能会有不同的实现,具体到客户端实际使用时实现是根据具体产品来的:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 产品A
*
* @author Lin.C
* @date 2019/5/28 7:46
*/
public class ConcreteProductA implements Product {

@Override
public void doSomething() {
System.out.println("我是Product接口的实际产品A");
}
}

工厂接口

  重要的事情说三遍:我们是面向接口编程的!
  当然这里的工厂接口你要真的不想用,也是OK的,但是一旦你抛弃了接口后期如果再想做扩展的,就会比较头疼了;所以这里使用一个工厂接口定义了生产产品的方法,当然工厂可能还有些其他的方法(也就是anOperation了,这个就和我们的设计模式没什么关系了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 工厂接口
*
* @author Lin.C
* @date 2019/5/28 7:48
*/
public interface Creator {

/**
* 工厂方法
*/
Product factoryMethod();

/**
* 其他操作
*/
void anOperation();
}

具体工厂

  这里就是具体的生产产品的逻辑操作了,不同的工厂都可以生产对应的产品,针对客户的需要,他想要什么类型的产品,就对接什么样的工厂,工厂则会给客户直接输出相应的产品,自动化实现,客户是看不到我具体怎么生产的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 具体的工厂(工厂模式)
*
* @author Lin.C
* @date 2019/5/28 7:50
*/
public class ConcreteCreatorA implements Creator {

@Override
public Product factoryMethod() {
return new ConcreteProductA();
}

@Override
public void anOperation() {
System.out.println("我是个独立的其他方法,比如可以告诉你我的地址是:南京市江宁区XXX街道XXX社区");
}
}

客户来了

  万事俱备,只缺客户了,所以在这里我们就模拟一个客户来让工厂运转起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 工厂模式的客户端
*
* @author Lin.C
* @date 2019/5/28 7:54
*/
public class FactoryMethodClient {

/**
* Main
* @param args
*/
public static void main(String[] args) {
// 1. 创建一个工厂A
Creator creatorA = new ConcreteCreatorA();
// 2. 生产A产品
creatorA.factoryMethod().doSomething();

// 1. 创建一个工厂
Creator creatorB = new ConcreteCreatorB();
// 2. 生产B产品
creatorB.factoryMethod().doSomething();
}
}

  客户一通操作猛如虎啊,恨不得各种产品都来一套,于是乎我们的工厂马不停蹄的生产出了下面这些产品:

1
2
3
4
我是Product接口的实际产品A
我是Product接口的实际产品B

Process finished with exit code 0

乌龙

  说实话,写Demo的时候完全没有意识到第一遍把工厂模式写成了简单工厂模式,所以这里追加一节说明一下简单工厂模式和工厂模式的区别。
  实际上看呢,简单工厂模式也是工厂模式的一种最简实现,它把抽象工厂的多个实现直接使用传入参数的模式模拟了,这种方法有一个致命的缺陷就是一旦需要新增工厂,那么就涉及到对工厂实现类的内部逻辑进行修改(这就违反了对修改关闭对扩展开放的原则),而实际的工厂模式由于使用的是对工厂接口的多个不同实现,在扩展时只需要增加实现就可以解决问题。
  不过话说回来简单工厂模式在某些场景下还是有用武之地的(对于产品变化几率很小的情况下还是实用性很强的);下面就直接通过代码演示了一下:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* 具体的工厂(简单工厂模式)
*
* @author Lin.C
* @date 2019/5/24 7:50
*/
public class ConcreteCreator implements Creator {

@Override
public Product factoryMethod(String productType) {
switch (productType) {
case "A":
return new ConcreteProductA();
case "B":
return new ConcreteProductB();
default:
break;
}
return new ConcreteProductDefault();
}

@Override
public void anOperation() {
System.out.println("我是个独立的其他方法,比如可以告诉你我的地址是:南京市江宁区XXX街道XXX社区");
}
}

/**
* 简单工厂模式的客户端
*
* @author Lin.C
* @date 2019/5/24 7:54
*/
public class FactoryMethodClient {

/**
* Main
* @param args
*/
public static void main(String[] args) {
// 1. 创建一个工厂
Creator creator = new ConcreteCreator();
// 2. 生产A产品
creator.factoryMethod("A").doSomething();
// 3. 生产B产品
creator.factoryMethod("B").doSomething();
// 4. 生产X产品(不存在的类型)
creator.factoryMethod("X").doSomething();
// 5. 工厂的其他操作
creator.anOperation();
}
}

# Console output
"C:\Program Files\Java\jdk1.8.0_201\bin\java.exe" ...
我是Product接口的实际产品A
我是Product接口的实际产品B
我是Product接口的默认产品
我是个独立的其他方法,比如可以告诉你我的地址是:南京市江宁区XXX街道XXX社区

Process finished with exit code 0


抽象工厂模式(Abstract Factory Pattern)


定义

  所谓的抽象工厂(Abstract Factory Pattern),实际上是围绕一个超级工厂创建其他工厂(也就是对工厂本身的创建也进行了抽象)。该超级工厂又称为其他工厂的工厂。
  通俗的理解,抽象工厂模式里有一个产品族的概念,每个工厂生产的多个产品之间有一定的联系,不同工厂间有可能有无法兼容的情况,为了避免让客户去梳理兼容逻辑,就在工厂端做了这种兼容性的限定;实际上就是一个工厂生产一套产品。

角色

  抽象工厂模式的主要角色如下。

  • 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法 createProductX(),可以创建多个不同等级的产品。
  • 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
  • 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
  • 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它 同具体工厂之间是多对一的关系。

类图

  个人观点:下面这个类图或许有些地方不大好理解,实际上的抽象工厂的实现,是针对一个产品,有多个工厂类,每个工厂类负责一个独立的完整产品的生产,所以补充一个稍微具象一点的图,这两个结合起来看就会比较清晰了;


  两张图一对比,就可以很清晰的理解抽象工厂的实际含义了:Mouse和Keybo对应到上图就是AbstractProduct,而PCFactory对应到上图就是AbstractFactory了;实际上上图如果把多个Product画出来,就更好理解了。

实践

产品接口

  这里的产品接口可以有多个,每个产品的实现也可以有多个,这样就形成了一个完整的产品族;还是以上面的具象图结合理解:组装PC,键盘算一种产品,鼠标算一种产品(对应了多个接口);然后键盘有不同的厂家出的,鼠标也有不同厂家出的(对应了每个接口的多个实现)。
  实际Demo中,这里使用了A、B两种产品(一个鼠标、一个键盘),每种产品对应L1、L2两种实现(对应多个厂家)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.linc.dp.factory.abstractf;

/**
* 抽象产品接口
*
* @author Lin.C
* @date 2019/5/27 7:46
*/
public interface AbstractProductA {

/**
* 功能接口,可能有多个
*/
void doSomething();
}

抽象工厂

  这里的抽象工厂可以同时生产A、B两种产品,实际作为Demo来说只通过一种也可以演示,但是可能理解起来没有两种那么具体,所以就稍微多动动手多写了几行代码。这里我们也有两个实际的具体工厂,每个工厂都可以生产A和B两种产品。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.linc.dp.factory.abstractf;

/**
* 抽象工厂接口
*
* @author Lin.C
* @date 2019/5/27 7:48
*/
public interface AbstractFactory {

/**
* 产品A工厂
*/
AbstractProductA createProductA();

/**
* 产品B的工厂
*/
AbstractProductB createProductB();
}

客户来了

  面对客户的时候,我们不需要暴露的太多(好比我去买电脑,我只需要明确我想买那个品牌的,Dell的还是Asus的,具体每个零件是不是兼容什么的,大多数时候是不需要考虑的),所以这里我们就模拟了客户分别下单购买了X厂和Y厂的产品(线)。

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
31
32
package com.linc.dp.client;

import com.linc.dp.factory.abstractf.AbstractFactory;
import com.linc.dp.factory.abstractf.ConcreteFactoryX;
import com.linc.dp.factory.abstractf.ConcreteFactoryY;

/**
* 抽象工厂模式的客户端
*
* @author Lin.C
* @date 2019/5/27 7:54
*/
public class AbstractFactoryClient {

/**
* Main
* @param args
*/
public static void main(String[] args) {
// 工厂X,分别生产对应的产品
System.out.println("-------------X 工厂开始加入生产--------------");
AbstractFactory factoryX = new ConcreteFactoryX();
factoryX.createProductA().doSomething();
factoryX.createProductB().doSomething();

// 工厂Y,分别生产对应的产品
System.out.println("-------------Y 工厂开始加入生产--------------");
AbstractFactory factoryY = new ConcreteFactoryY();
factoryY.createProductA().doSomething();
factoryY.createProductB().doSomething();
}
}

  随着客户订单的落实,产品很顺利的下线了,X工厂生产的都是统一L1品类的产品A和B,Y工厂则是品类L2的,这样就不会存在任何不兼容的情况了。

1
2
3
4
5
6
7
8
-------------X 工厂开始加入生产--------------
我是Abstract Product接口的实际产品-产品A-品类L1
我是Abstract Product接口的实际产品-产品B-品类L1
-------------Y 工厂开始加入生产--------------
我是Abstract Product接口的实际产品-产品A-品类L2
我是Abstract Product接口的实际产品-产品B-品类L2

Process finished with exit code 0

结构回溯

  因为前面没有贴出完整代码,所以这里对实际的代码结构进行了简单的说明:

1
2
3
4
5
6
7
8
9
10
11
12
# 核心包
./dp/factory/abstractf:
total 9
-rw-r--r-- 1 Administrator 197121 328 5月 27 07:39 AbstractFactory.java -- 抽象工厂
-rw-r--r-- 1 Administrator 197121 246 5月 27 07:37 AbstractProductA.java -- 抽象产品A
-rw-r--r-- 1 Administrator 197121 246 5月 27 07:37 AbstractProductB.java -- 抽象产品B
-rw-r--r-- 1 Administrator 197121 335 5月 27 07:58 ConcreteAbstractProductAL1.java -- A产品L1品类
-rw-r--r-- 1 Administrator 197121 335 5月 27 07:58 ConcreteAbstractProductAL2.java -- A产品L2品类
-rw-r--r-- 1 Administrator 197121 335 5月 27 07:58 ConcreteAbstractProductBL1.java -- B产品L1品类
-rw-r--r-- 1 Administrator 197121 335 5月 27 07:58 ConcreteAbstractProductBL2.java -- B产品L2品类
-rw-r--r-- 1 Administrator 197121 426 5月 27 07:58 ConcreteFactoryX.java -- X工厂(生产L1品类)
-rw-r--r-- 1 Administrator 197121 426 5月 27 07:58 ConcreteFactoryY.java -- Y工厂(生产L2品类)


单例模式(Singleton Pattern)


定义

  单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
  前面的解释比较理论化,通俗的说呢,单例模式就是谁也不能创建我(当然也有些特殊手段除外),我自己提供自己的实例,这样就可以保证全局唯一性。

角色

  单例模式的主要角色如下:

  • 单例类:包含一个实例且能自行创建这个实例的类。
  • 访问类:使用单例的类。

类图

  通过类图也基本可以很清晰的看清楚单例的一些特性:

  • 实例变量肯定是静态私有的,因为要提供给外部静态访问,而且不能直接给外部获取
  • 构造方法私有,这个就不用过多解释了,就是为了避免new的
  • 必须提供一个static的获取实例方法,你要别人用你,又不让人直接接触你的真身,那么肯定得开放一个口子给人对吧

实践

  提到单例模式,可能稍微有一些设计模式基础的都能写出那么两三种,所以这一章节我们也按照传统的类别看看不同的单例模式有什么区别,哪一种比较适合我们使用;鉴于单例模式一般都是一个类就能搞定,这里的客户类就直接附在每个演示类内部了。

饿汉式单例

  简单的解释,就是在声明static变量时直接初始化,每次getInstance都不需要判断了,简单暴力的解决了单例的问题;demo程序运行一下就能清楚的发现每次获取的实例hashCode都是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.linc.dp.Singleton;

/**
* 饿汉式单例
*
* @author Lin.C
* @date 2019/5/28 7:44
*/
public class SingletonTypeA {

private static SingletonTypeA instance = new SingletonTypeA();

private SingletonTypeA() {
}

/**
* 获取实例
*
* @return
*/
public static SingletonTypeA getInstance() {
return instance;
}
}

懒汉式单例

  我们平时比较习惯的一种单例模式,在需要时才创建(也就是懒加载),但是线程安全不能保证(主要是因为在第19行代码会引起多线程并发进入的风险)。

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
31
32
33
package com.linc.dp.Singleton;

/**
* 懒汉式单例
*
* @author Lin.C
* @date 2019/5/28 7:44
*/
public class SingletonTypeB {

private static SingletonTypeB instance;

private SingletonTypeB() {
}

/**
* 获取实例
*
* @return
*/
public static SingletonTypeB getInstance() {
if (null == instance) {
// 为了加强多线程模拟的效果,否则线程运行太快无法演示出多线程时的效果(实际使用时不需要)
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new SingletonTypeB();
}
return instance;
}
}

同步锁式单例

  一般情况下,对并发性能要求不高的话,会使用这种模式,既实现了线程安全的单例,也不会导致太复杂的逻辑,这种实现和前一种唯一的区别就是给getInstance方法加上了一个同步锁,仅此而已。

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
31
32
33
package com.linc.dp.Singleton;

/**
* 同步锁式单例
*
* @author Lin.C
* @date 2019/5/29 7:24
*/
public class SingletonTypeBPlus {

private static SingletonTypeBPlus instance;

private SingletonTypeBPlus() {
}

/**
* 获取实例
*
* @return
*/
public static synchronized SingletonTypeBPlus getInstance() {
if (null == instance) {
// 为了加强多线程模拟的效果,否则线程运行太快无法演示出多线程时的效果(实际使用时不需要)
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new SingletonTypeBPlus();
}
return instance;
}
}

双重检查锁式单例

  这种模式,相对一前面一种同步锁,效率会更高一些,主要是因为锁的区域缩小了,这个实现方式也是面试笔试经常会遇到的一种场景(但还不是最优场景)。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.linc.dp.Singleton;

/**
* 双重检查锁式单例
*
* @author Lin.C
* @date 2019/5/29 7:44
*/
public class SingletonTypeC {

private static SingletonTypeC instance;

private SingletonTypeC() {
}

/**
* 获取实例方式1
*
* @return
*/
public static SingletonTypeC getInstance() {
if (null == instance) {
synchronized (SingletonTypeC.class) {
if (null == instance) {
instance = new SingletonTypeC();
}

}
}
return instance;
}

/**
* 获取实例方式2
*
* @return
*/
public static SingletonTypeC getInstancePlus() {
if (null == instance) {
SingletonTypeC temp;
synchronized (SingletonTypeC.class) {
temp = instance;
if (null == instance) {
instance = new SingletonTypeC();
synchronized (SingletonTypeC.class) {
if (null == temp) {
temp = new SingletonTypeC();
}
}
instance = temp;
}
}
}
return instance;
}
}

枚举式单例模式

  说实话这种单例模式听过很多次,但是少有人用,因为枚举本身自己就保证了单例性,而且它原生防止反射与反序列化击穿(因为其他单例模式虽然使用了private的构造函数,但在反射或者反序列化面前也不完全是安全的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.linc.dp.Singleton;

/**
* 枚举式单例
*
* @author Lin.C
* @date 2019/5/29 7:48
*/
public enum SingletonTypeD {

INSTANCE;

public static Object doSomething() {
// 功能代码
return null;
}
}

静态内部类式单例(推荐)

  外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存;只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

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
package com.linc.dp.Singleton;

/**
* 静态内部类式单例
*
* @author Lin.C
* @date 2019/5/29 7:44
*/
public class SingletonTypeE {

private SingletonTypeE() {
}

public static class inner {
public static final SingletonTypeE instance = new SingletonTypeE();
}

/**
* 获取实例
*
* @return
*/
public static SingletonTypeE getInstance() {
return inner.instance;
}
}

验证

  这里提供了一个针对上述单例模式的测试,但是实际运行时由于很多情况都是采用多线程的模式,所以需要分段注释运行,不然结果较差就没有任何参考价值了,这里就不一一列举结果了。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.linc.client;

import com.linc.dp.Singleton.*;

/**
* 单例模式的测试客户类
*
* @author Lin.C
* @date 2019/5/30 7:53
*/
public class SingletonClient {

public static void main(String[] args) {
System.out.println("--------------- 饿汉式单例(单线程) ------------");
for (int i = 0; i < 3; i++) {
System.out.println(SingletonTypeA.getInstance().hashCode());
}

System.out.println("--------------- 懒汉式单例(多线程) ------------");
for (int i = 0; i < 3; i++) {
new Thread(() -> System.out.println(SingletonTypeB.getInstance().hashCode())).start();
}

System.out.println("--------------- 懒汉式单例(单线程) ------------");
for (int i = 0; i < 3; i++) {
System.out.println(SingletonTypeB.getInstance().hashCode());
}

System.out.println("--------------- 同步锁式单例(多线程) ------------");
for (int i = 0; i < 3; i++) {
new Thread(() -> System.out.println(SingletonTypeBPlus.getInstance().hashCode())).start();
}

System.out.println("--------------- 双重检查锁式单例(多线程) ------------");
for (int i = 0; i < 3; i++) {
new Thread(() -> System.out.println(SingletonTypeC.getInstance().hashCode())).start();
}
for (int i = 0; i < 3; i++) {
new Thread(() -> System.out.println(SingletonTypeC.getInstancePlus().hashCode())).start();
}

System.out.println("--------------- 枚举式单例(多线程) ------------");
for (int i = 0; i < 3; i++) {
new Thread(() -> System.out.println(SingletonTypeD.INSTANCE.hashCode())).start();
}

System.out.println("--------------- 静态内部类式单例(多线程) ------------");
for (int i = 0; i < 3; i++) {
new Thread(() -> System.out.println(SingletonTypeE.getInstance().hashCode())).start();
}
}
}

尾声

  关于单例,其实还涉及到反射穿透和反序列化破解两种情况,以及不同单例的性能有多大区别,尤其是在并发环境下的区别,这些在本章不做过多讨论,会在后续提升系列做补充。


建造者模式(Builder Pattern)


定义

  建造者模式(Builder Pattern) 又名生成器模式,是一种对象构建模式。它可以将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象。

角色

  建造者(Builder)模式的主要角色如下。

  • 产品角色(Product):它是包含多个组成部件的复杂对象,由具体建造者来创建其各个滅部件(类图中并未体现,实际上对应的getResult的返回结果类型)。
  • 抽象建造者(Builder):它是一个包含创建产品各个子部件的抽象方法的接口,通常还包含一个返回复杂产品的方法 getResult()。
  • 具体建造者(Concrete Builder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。
  • 指挥者(Director):它调用建造者对象中的部件构造与装配方法完成复杂对象的创建,在指挥者中不涉及具体产品的信息。

类图

  这里的类图画的还是稍微有些抽象,实际使用中我们Builder中可能会有多个buildPart(相当于一步一步组装一个完整的产品),这种情况下也是发挥我们这种模式的长处的最佳场景。
  另外要提一句的是,标准的Builder模式是有director的(也就是导演类),实际上它是在抽象生产者的基础上,把组装的步骤进行了一次封装,实际使用时可能客户端也会兼职完成了director该做的,但是不能忘了它的存在。

实践

产品实体

  为了体现Builder模式的步骤性,所以这里抽象了一个含有3个部分的产品实体(就好比现实生活中你需要造一辆自行车,那么这个part就更多了,车轮、坐垫、龙头、把手、链条等等):

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
31
32
33
34
package com.linc.dp.Builder;

import lombok.Getter;
import lombok.Setter;

/**
* 产品实体
*
* @author Lin.C
* @date 2019/5/31 7:20
*/
public class Product {

@Getter
@Setter
private String partA;

@Getter
@Setter
private String partB;

@Getter
@Setter
private String partC;

@Override
public String toString() {
return "Product{" +
"partA='" + partA + '\'' +
", partB='" + partB + '\'' +
", partC='" + partC + '\'' +
'}';
}
}

建造者接口

  因为面向抽象编程嘛,所以就提取出一个接口(实际使用简单的情况下也可以取消接口),需要注意的是这里的product属性需要设置为protected(父子可见),因为在抽象类中并没有给product设置任何属性,而是需要子类去实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.linc.dp.Builder;

/**
* 建造者接口
*
* @author Lin.C
* @date 2019/5/31 7:20
*/
public abstract class Builder {

protected Product product = new Product();

public abstract void buildPartA();

public abstract void buildPartB();

public abstract void buildPartC();

public Product getResult() {
return product;
}
}

建造者实现类

  这里没什么好解释的,就是对抽象的Builder的一个具体实现。

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
package com.linc.dp.Builder;

/**
* 建造者实现类
*
* @author Lin.C
* @date 2019/5/31 7:24
*/
public class ConcreteBuilder extends Builder {

@Override
public void buildPartA() {
product.setPartA("partA");
}

@Override
public void buildPartB() {
product.setPartB("partB");
}

@Override
public void buildPartC() {
product.setPartC("partC");
}
}

导演类Director

  导演类实际上做了几件事情:1)它需要有一个抽象建造者的引用,也就是下面的Builder;2)它需要对外提供一个组装产品的方法(对外屏蔽掉产品生产的细节);一来它实现类客户和生产过程的隔离,二来其内部还负责整个产品的生产过程。

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
31
32
33
package com.linc.dp.Builder;

import lombok.Getter;
import lombok.Setter;

/**
* 导演类
*
* @author Lin.C
* @date 2019/5/31 7:28
*/
public class Director {

@Getter
@Setter
private Builder builder;

public Director(Builder builder) {
this.builder = builder;
}

/**
* 生产产品
*
* @return
*/
public Product construct() {
builder.buildPartA();
builder.buildPartB();
builder.buildPartC();
return builder.getResult();
}
}

客户来了

  对客户来说,就很简单了,它无须关心产品对象的具体组装过程,只需确定知道具体的建造者的类型即可;也就好比买自行车,你只需要告诉经销商你要哪个牌子的厂家造的,而不需要知道他们是先装轮子还是先装链条这些具体细节。

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
package com.linc.client;

import com.linc.dp.Builder.ConcreteBuilder;
import com.linc.dp.Builder.Director;

/**
* 建造者模式客户端模拟
*
* @author Lin.C
* @date 2019/5/31 7:26
*/
public class BuilderClient {

/**
* Main
*
* @param args
*/
public static void main(String[] args) {
Director director = new Director(new ConcreteBuilder());
System.out.println(director.construct());
}
}

// Console Output
Product{partA='partA', partB='partB', partC='partC'}

补充

  实际开发过程中,我们有时候也会用到很简单的建造者模式,比如下面这种简化用法,在Entity内部直接通过一个类似Builder的实现方式进行属性设置,也可以很好地实现建造者的步骤化流程:

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
31
32
33
34
35
36
package com.linc.dp.Builder;

import lombok.Getter;
import lombok.Setter;

/**
* 简化的建造者
*
* @author Lin.C
* @date 2019/5/31 7:43
*/
public class Product2 {

@Getter
@Setter
private String partA;

public Product2 partASet(String partA) {
this.partA = partA;
return this;
}

@Override
public String toString() {
return "Product{" +
"partA='" + partA + '\'' +
'}';
}

public static void main(String[] args) {
System.out.println(new Product2().partASet("I am part A"));
}
}

// Console Output
Product{partA='I am part A'}


原型模式(Prototype Pattern)


定义

  原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能,它用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象;说起来一长串的定义,实际上就是用到了Java的clone操作来实现的。

角色

  原型模式包含以下主要角色。

  • 抽象原型类(prototype):它是声明克隆方法的接口,是所有具体原型类的公共父类,它可以是接口,抽象类甚至是一个具体的实现类
  • 具体原型类(concretePrototype):它实现了抽象原型类中声明的克隆方法,在克隆方法中返回一个自己的克隆对象
  • 访问类(Client):在客户类中,使用原型对象只需要通过工厂方式创建或者直接NEW(实例化一个)原型对象,然后通过原型对象的克隆方法就能获得多个相同的对象。由于客户端是针对抽象原型对象编程的所以还可以可以很方便的换成不同类型的原型对象!

类图

实践

原型产品

  原型产品必须是实现了Cloneable接口的,表示它是一个可复制的对象;这里的实现实际上和前面的类图不完全一致,因为去掉了抽象原型这一层而直接到了原型实现实体。

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
31
32
33
34
35
36
37
38
39
package com.linc.dp.Prototype;

import lombok.Getter;
import lombok.Setter;

/**
* 原型产品抽象
*
* @author Lin.C
* @date 2019/5/31 7:52
*/
public class Prototype implements Cloneable {

@Getter
@Setter
protected String name;

public void doSomething() {
System.out.println("I am : " + name + "; i am working.");
}

@Override
public String toString() {
return "Prototype{" +
"name='" + name + '\'' +
'}';
}

@Override
public Prototype clone() {
Prototype clone = null;
try {
clone = (Prototype) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clone;
}
}

客户端

  客户端在产生一个原型后,可以随意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
27
28
package com.linc.client;

import com.linc.dp.Prototype.Prototype;

/**
* 原型模式客户端
*
* @author Lin.C
* @date 2019/5/31 8:00
*/
public class PrototypeClient {

public static void main(String[] args) {
Prototype prototype = new Prototype();

Prototype b = prototype.clone();
b.setName("B");
System.out.println(b);

Prototype c = prototype.clone();
c.setName("C");
System.out.println(c);
}
}

// Console Output
Prototype{name='B'}
Prototype{name='C'}

扩展知识

  在某些情况下,你会发现通过这种clone方式,有些属性会没有被复制(比如在实体类的内部有引用到了其他对象类型),这时候实际上发生了浅克隆(也就是说只会克隆本对象的基本数据类型,而对引用数据类型则只是简单的克隆了引用并没有重新生成对象),具体什么是深克隆和浅克隆,本节暂不展开说明,可参考后续章节。以下附上一个简单的例子演示一下浅克隆是怎么回事:

原型实体

  这个实体跟前面比起来,携带的成员变量变成了一个List(这个list在类创建的时候就创建好了,内存分配在堆中),其他操作和前面的产品并无本质差异。

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
31
32
33
34
35
36
37
38
39
40
41
42
package com.linc.dp.Prototype;

import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;

/**
* 原型产品抽象
*
* @author Lin.C
* @date 2019/6/11 7:52
*/
public class PrototypeExt implements Cloneable {

@Getter
@Setter
private List<String> list = new ArrayList<>();

public void add(String name) {
list.add(name);
}

@Override
public String toString() {
return "Prototype{" +
"list='" + list + '\'' +
'}';
}

@Override
public PrototypeExt clone() {
PrototypeExt clone = null;
try {
clone = (PrototypeExt) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clone;
}
}

客户类

  通过客户端的clone操作我们会发现,克隆后的实体成员变量,将前一个实体已有的值带了过来,当然实际使用时我们是不希望看到这种情况出现的(这种情况就是浅克隆引起的),因为对于引用数据类型,clone时只是把该对象的地址做了拷贝,在堆中对应的实际上还是同一块内存区域(String虽然作为引用数据类型,但是在clone时jvm是把它当做基本数据类型处理的)。

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
package com.linc.client;

import com.linc.dp.Prototype.PrototypeExt;

/**
* 原型模式客户端(浅克隆)
*
* @author Lin.C
* @date 2019/6/11 8:00
*/
public class PrototypeExtClient {

public static void main(String[] args) {
PrototypeExt prototypeExt = new PrototypeExt();

prototypeExt.add("Jack");
System.out.println(prototypeExt);

PrototypeExt cloner = prototypeExt.clone();
cloner.add("Rose");
System.out.println(cloner);
}
}

// Output
Prototype{list='[Jack]'}
Prototype{list='[Jack, Rose]'}

------2019 Lin.C ------