创建型设计模式

与对象创建有关,它包含单例模式、工厂方法模式、抽象工厂模式、建造者模式、原型模式。虽然简单工厂模式不是 Gof 提出的创建型设计模式,但是它对理解抽象工厂模式有帮助。


单例模式

定义

保证一个类仅有一个实例,而且自行实例化,并提供一个访问它的全局访问点。(即是定义,也是本质。本质即是控制实例数量。)

客户端(Client)通过单例类 Singleton 的 getInstance 方法来获取实例对象。

单例注意点:

  1. 构造函数私有
  2. 含有一个该类的静态私有对象
  3. 有一个静态的公有的函数用于创建或获取它本身的静态私有对象
  4. 线程同步

线程安全

代码所在进程中存在多个线程同时运行,可能会同时运行一段代码,如果每次运行的结果和单线程一样,而且其它变量值也和预期一样,就是线程安全的。
或者说:一个类或程序所提供的接口对于线程来说是原子操作,或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说不用考虑同步问题,那就是线程安全的。

计算机中使用场景

线程池,缓存,日志对象,打印机,显卡的驱动程序对象等多少具有资源管理器的功能。

单例模式的使用场景

在一个系统中,要求一个类有且仅有一个对象,它的具体使用场景如下:

  • 整个项目需要一个共享访问点或共享数据。
  • 创建一个对象需要消耗的资源过多,比如访问 I/O 或者数据库等资源。
  • 工具类对象。

下面介绍几种形式的单例模式,至于选用则取决于项目本身情况:是否为复杂的并发环境,或者是否需要控制单例对象的资源消耗。

饿汉式

这种方式在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快。这种方式基于类加载机制,避免了多线程的同步问题。在类加载的时候就完成实例化,没有达到懒加载的效果。如果从始至终未使用过这个实例,则会造成内存的浪费。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 饿汉式
* 空间换时间。
* 线程安全的,因为虚拟机保证只会装载一次,装载类时不会发生并发。
*/
public class Singleton {
/**
* 类初始化时,已经自行实例化,
* 范围是一个虚拟机的范围,因为装载类的功能是虚拟机。
*/
private static final Singleton mSingleton = new Singleton();

private Singleton(){}

/**
* 静态工厂方法
*/
public static Singleton getInstance(){
return mSingleton;
}
}

懒汉式

线程不安全的写法:声明了一个静态对象,在用户第一次调用时初始化。这虽然节约了资源,但第一次加载时需要实例化,反应稍慢一些,而且在多线程时不能正常工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {

private static Singleton mSingleton ;

private Singleton(){}

public static Singleton getInstance(){
if (mSingleton == null){
mSingleton = new Singleton();
}
return mSingleton;
}
}

线程安全的写法:这种写法能够在多线程中很好地工作,但是每次调用 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
/**
* 懒汉式(线程安全)
* 时间换空间
* 有延迟加载和缓存的思想
*/
public class Singleton {

/**
* 第一次调用时,实例化自己
*/
private static Singleton mSingleton ;

/**
* 私有化,避免在外部被实例化
* (除了反射机制)
*/
private Singleton(){}

/**
* 加锁保证线程安全,但会降低整个访问速度,
* 而且每次都要判断,可用双重检查枷锁。
*/
public static synchronized Singleton getInstance(){
if (mSingleton == null){
mSingleton = new Singleton();
}
return mSingleton;
}
}

饿汉式和懒汉式的区别

从名字来说,饿汉是类一旦加载,单例则初始化完成。而懒汉则比较懒,当调用getInstance时,才会去初始化实例。

  • 线程安全:
    饿汉式天生线程安全的,可直接用于多线程而不会出问题。
    懒汉式本身是非线程安全的,可以有几种写法,如静态内部类的方式,double-lock的方式。在资源加载和性能方面有些区别。
  • 资源加载和性能:
    饿汉式在类创建同时就实例化一个静态对象出来,不管之后是否适用,都会占据一定内存。但相应,第一次调用更快,因为资源已经初始化。
    懒汉式会延迟加载,第一次使用该单例时才会实例化对象出来。如果初始化时,做的工作比较多,性能上会有些延迟,之后和饿汉式一样。

双重检查模式(DCL)

这种写法在 getInstance 方法中对 Singleton 进行了两次判空,第一次是为了不必要的同步,第二次是在 Singleton 等于 null 的情况下才创建实例。在这里使用 volatile 会或多或少地影响性能,但考虑到程序的正确性,牺牲这点性能还是值得的。DCL 的优点是资源利用率高。第一次执行 getInstance 时单例对象才被实例化,效率高。其缺点是第一次加载时反应稍慢一些,在高并发环境下也有一定的缺陷。DCL 虽然在一定程度上解决了资源的消耗和多余的同步、线程安全等问题,但其还是在某些情况会出现失效的问题,也就是 DCL 失效。这里建议用静态内部类单例来替代 DCL。

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
/**
* double-lock(双重检查加锁)
*
* 做了两次 null 检查,确保只有第一次调用时才会做同步,
* 既保证线程安全,又避免每次都同步的性能损耗。
*/
public class Singleton {
/**
* volatile:被它修饰的变量的值,将不会被本地线程缓存,
* 所有对该变量的读写都直接操作共享内存的,
* (读时会从主线程重新 copy,写时会同步到主线程去。)
* 从而保证多个线程能正确的处理该变量。
*/
private volatile static Singleton mSingleton;

private Singleton(){}

/**
* 相比于直接锁住方法,这种缩小锁影响范围的方式更加高效。
* 如锁方法,线程只能一个一个依次执行整个方法体,会造成大量阻塞时间,
* 另外,之所以处理线程安全问题,只是因为在 getInstance() 方法前几次被并发执行时,
* 可能会有多个 mSingleton == null 为 true 的结果,从而出现创建多个对象情况,
* 而一旦有线程完成创建实例操作,不考虑其他修改方法情况下,
* 对于 getInstance() 这种只读操作,其方法内部就不再存在线程安全问题,
* 所以,如果已创建对象,完全可以线程并行执行 getInstance() 方法。
* (创建对象同步执行,后异步执行。)
*/
public static Singleton getInstance(){
if (mSingleton == null){
// 同步块
// 如果实例不存在,加锁同步。
synchronized (Singleton.class){
// 为什么做两次判断
// 因为高并发下,可能两个线程都通过了第一个if,
// 若A先抢到锁,new了个对象,释放锁,
// 然后B在抢到锁,B会再new一个对象。
if (mSingleton == null){
mSingleton = new Singleton();
}
}
}
return mSingleton;
}
}

静态内部类

第一次加载 Singleton 类时并不会初始化 mSingleton,只有第一次调用 getInstance 方法时虚拟机加载 SingletonHolder 并初始化 mSingleton。这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。所以,推荐使用静态内部类单例模式。

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
/**
* 静态内部类
* 更好一些,
* 既实现了线程安全,又避免了同步带来的性能影响。
*
* 利用了 classloader 的机制来保证初始化 instance 时只有一个线程,
* 所以也是线程安全的,同时没有性能损耗。
*/
public class Singleton {
/**
* 类级的内部类,也就是静态类的成员式内部类,
* 该内部类的实例与外部类的实例没有绑定关系,
* 而且只有被调用时才会装载,
* 从而实现了延迟加载。
*/
private static class SingletonHolder{
/**
* 静态初始化器,由 JVM 来保证线程安全。
* 内部类加载:
* 无论是否是静态,在第一次使用时才会被加载,
* 类加载时有一种机制叫缓存机制,
* 第一次加载成功之后会被缓存起来,而且一般一个类不会加载多次。
*/
private static final Singleton mSingleton = new Singleton();
}

/**
* 私有化构造函数
*/
private Singleton(){}

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

kotlin:

1
2
3
4
5
6
7
8
9
class Singleton private constructor() {
companion object {
val instance = SingletonHolder.holder
}

private object SingletonHolder {
val holder= Singleton()
}
}

枚举

枚举单例的优点就是简单,但是大部分应用开发很少用枚举,其可读性并不是很高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 枚举
* 《高效Java第二版》,单元素的枚举类型为最佳方法。
*/
public enum Singleton {
/**
* 定义了一个枚举的元素,
* 它就代表了 Singleton 的一个实例。
*/
INSTANCE;

/**
* 示意方法,
* 单例可以有自己的操作。
*/
public void whateverMethod(){
// 功能树立
}

}

默认枚举实例的创建是线程安全的,并且在任何情况下都是单例。在上面的几种单例模式实现中,有一种情况下其会重新创建对象,那就是反序列化:将一个单例实例对象写到磁盘再读回来,从而获得了一个实例。反序列化操作提供了 readResolve 方法,这个方法可以让开发人员控制对象的反序列化。在上述几个方法示例中,如果要杜绝单例对象被反序列化时重新生成对象,就必须加入如下方法:

1
2
3
private Object readResolve() throws ObjectStreamException{
return singleton;
}

登记式单例(可忽略)

实际上维护了一组单例类实例,将这些实例存放再Map(登记薄)中,对于已经登记过的实例,Map则直接返回。否则,先登记,再返回。其实,内部实现还是饿汉式。


工厂模式

顾名思义,它会创建一些东西。确切地说,它会创建对象。工厂模式的用途是借助通用接口将逻辑与使用分开。

1
2
3
4
5
6
7
8
9
/**
* FileName: 面包
* Profile: 通用接口
*/
public interface Bread {

String name();
String calories(); // 卡路里
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* FileName: 法国棍子面包
* Profile: 具体类
*/
public class Baguette implements Bread{
@Override
public String name() {
return "Baguette";
}

@Override
public String calories() {
return " : 65 kcal";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* FileName: 小面包
* Profile: 具体类
*/
public class Roll implements Bread{
@Override
public String name() {
return "Roll";
}

@Override
public String calories() {
return " : 75 kcal";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* FileName: 不同种类的面包工厂
* Profile: 工厂
*/
public class BreadFactory {

public Bread getBread(String breadType){
if (breadType == "RAG"){
return new Baguette();
}else if (breadType == "ROL"){
return new Roll();
}
return null;
}
}
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
class MainActivity : AppCompatActivity() {

// private static final String TAG = "tag";
private val DEBUG_TAG = "tag"

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// BreadFactory breadFactory = new BreadFactory();
// Bread bread = breadFactory.getBread("ROL");
// Log.d("TAG",new StringBuilder()
// .append(bread.name())
// .append(bread.calories())
// .toString());

val breadFactory = BreadFactory()
val breadROL = breadFactory.getBread("ROL")
Log.e(
DEBUG_TAG, "${breadROL.name()}${breadROL.calories()}"
)
Log.e(DEBUG_TAG, "-----------------------------")
val breadRAG = breadFactory.getBread("RAG")
Log.e(
DEBUG_TAG, "${breadRAG.name()}${breadRAG.calories()}"
)

// 打印结果:
// E/tag: Roll : 75 kcal
// E/tag: -----------------------------
// E/tag: Baguette : 65 kcal
}
}

简单工厂模式

又叫做静态工厂方法模式,属于创建型设计模式,但是并不属于 23 种 GoF 设计模式之一。

定义:这是由一个工厂对象决定创建出哪一种产品类的实例。

在简单工厂模式中有如下角色:

  • Factory:工厂类,这是简单工厂模式的核心,它负责实现创建所有实例的内部逻辑。工厂类的创建产品类的方法可以被外界直接调用,创建所需的产品对象。
  • IProduct:抽象产品类,这是简单工厂模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。
  • Product:具体产品类,这是简单工厂模式的创建目标。

示例

用生产计算机来举例,假设有一个计算机的代工生厂商,它目前已经可以代工生产联想计算机了。随着业务的拓展,这个代工生厂商还要生产惠普和华硕的计算机。这样就需要用一个单独的类来专门生产计算机。

抽象产品类:创建一个计算机的抽象产品类,其有一个抽象方法用于启动计算机。

1
2
3
4
5
6
7
public abstract class Computer {

/**
* 产品的抽象方法,由具体的产品类实现。
*/
public abstract void start();
}

具体产品类:创建各个品牌的计算机。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class LenovoComputer extends Computer{
@Override
public void start() {
System.out.println("联想计算机启动");
}
}

public class HpComputer extends Computer{
@Override
public void start() {
System.out.println("惠普计算机启动");
}
}

public class AsusComputer extends Computer{
@Override
public void start() {
System.out.println("华硕计算机启动");
}
}

工厂类:它提供了一个静态方法 createComputer 用来生产计算机。只需要传入自己想生产的计算机的品牌,它就会实例化相应品牌的计算机对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ComputerFactory{
public static Computer createComputer(String type){
Computer mComputer = null;
switch (type){
case "lenovo":
mComputer = new LenovoComputer();
break;
case "hp":
mComputer = new HpComputer();
break;
case "ausu":
mComputer = new AsusComputer();
break;
}
return mComputer;
}
}

客户端调用工厂类:传入 “hp” 生产出惠普计算机并调用该计算机对象的 start 方法。

1
2
3
4
5
public class CreateComputer{
public static void main(String[] args){
ComputerFactory.createComputer("hp").start();
}
}

使用场景和优缺点

  • 使用场景:
    • 工厂类负责创建的对象比较少。
    • 客户只需知道传入工厂类的参数,而无需关心创建对象的逻辑。
  • 优点:使用户根据参数获得对应的类的实例,避免了直接实例化类,降低了耦合性。
  • 缺点:可实例化的类型在编译期间已经被确定。如果增加新类型,则需要修改工厂,这违背了开放封闭原则。简单工厂需要知道所有要生成的类型,其当子类过多或者子类层次过多时不适合使用。

工厂方法模式

定义:定义一个用于创建对象的接口,让子类决定实例化哪个类。工厂方法使一个类的实例化延迟到其子类。

在工厂方法模式中有如下角色

  • Product:抽象产品类
  • ConcreteProduct:具体产品类,实现 Product 接口。
  • Factory:抽象工厂类,该方法返回一个 Product 类型的对象。
  • ConcreteFactory:具体工厂类,返回 ConcreteProduct 实例。

示例

工厂方法模式的抽象产品类与具体产品类的创建和简单工厂模式是一样的。

创建抽象工厂

1
2
3
4
public abstract class ComputerFactory{
// 用来生产想要的品牌的计算机
public abstract <T extends Computer> T createComputer(Class<T> clz);
}

具体工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 广达代工厂是一个具体的工厂,其继承抽象工厂,通过反射来生产不同厂家的计算机。
*/
public class GDComputerFactory extends ComputerFactory{

@Override
public <T extends Computer> T createComputer(Class<T> clz) {
Computer computer = null;
String className = clz.getName();
try{
// 通过反射来生产不同厂家的计算机
computer = (Computer) Class.forName(className).newInstance();
}catch (Exception e){
e.printStackTrace();
}
return (T) computer;
}
}

客户端调用

1
2
3
4
5
6
7
8
9
10
11
12
public class client{
public static void main(String[] args){
// 客户创建了 GDComputerFactory,并分别生产了各类品牌计算机。
ComputerFactory computerFactory = new GDComputerFactory();
LenovoComputer lenovoComputer = computerFactory.createComputer(LenovoComputer.class);
lenovoComputer.start();
HpComputer hpComputer = computerFactory.createComputer(HpComputer.class);
hpComputer.start();
AsusComputer asusComputer = computerFactory.createComputer(AsusComputer.class);
asusComputer.start();
}
}

工厂方法与简单工厂

对于简单工厂模式,其在工厂类中包含了必要的逻辑判断,根据不同的条件来动态实例化相关的类。对客户来说,这去除了与具体产品的依赖,但与此同时也带来一个问题,如果要增加产品,比如要生产苹果计算机,就需要在工厂类中添加一个 Case 分支条件,这违背了开放封闭原则,对修改也开放了。而工厂方法模式就没有违背这个开放封闭原则。如果需要生产苹果计算机,则无需修改工厂类,直接创建产品即可。


建造者模式

也被称为生成器模式,它是创建一个复杂对象的创建型模式,其将构建复杂对象的过程和它的部件解耦,使得构建过程和部件的表示分离开来。例如要 DIY 一台台式计算机,我们找到 DIY 商家,这时这台计算机的 CPU 、主板或者其它部件都是什么牌子、什么配置的,这些都可以根据需求来变化。对于这些部件的组装成计算机的过程是一样的,我们无需知道组装过程,只需要提供相关部件的牌子和配置就可以了。对于这种情况就可以采用建造者模式,将部件和组装过程分离,使得构建过程和部件都可以自由拓展,两者之间的耦合也降到最低。

定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

在建造者模式中有如下角色:

  • Director:导演类,负责安排已有模块的顺序,然后通知 Builder 开始建造。
  • Builder:抽象 Builder 类,规范产品的组建,一般由子类实现。
  • ConcreteBuilder:具体建造者,实现抽象 Builder 类定义的所有方法,并且返回一个组建好的对象。
  • Product:产品类。

示例:以 DIY 组装计算机的例子来实现

创建产品类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 要组装一台计算机,计算机被抽象为 Computer 类,
* 它有 3 个部件:CPU,主板和内存,
* 并提供了 3 个方法分别用来设置它们。
*/
public abstract class Computer {

private String mCPU;
private String mMainboard;
private String mRam;

public void setmCPU(String mCPU){
this.mCPU = mCPU;
}
public void setmMainboard(String mMainboard){
this.mMainboard = mMainboard;
}
public void setmRam(String mRam){
this.mRam = mRam;
}
}

创建 Builder 类规范产品的组建

1
2
3
4
5
6
7
8
9
10
/**
* 商家组装计算机有一套组装方法的模板,就是一个抽象 Builder 类,
* 里面提供了安装 CPU 、主板和内存的方法,以及组成计算机的 create 方法。
*/
public abstract class Builder{
public abstract void buildCPU(String cpu);
public abstract void buildMainboard(String mainboard);
public abstract void buildRam(String ram);
public abstract Computer create();
}
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
/**
* 商家实现了抽象的 Builder 类,MoonComputerBuilder 类用于组装计算机。
*/
public class MoonComputerBuilder extends Builder{

private Computer mComputer = new Computer();

@Override
public void buildCPU(String cpu) {
mComputer.setmCPU(cpu);
}

@Override
public void buildMainboard(String mainboard) {
mComputer.setmMainboard(mainboard);
}

@Override
public void buildRam(String ram) {
mComputer.setmRam(ram);
}

@Override
public Computer create() {
return mComputer;
}
}

用导演类来统一组装过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 商家的导演类用来规范组装计算机的流程规范,
* 先安装主板,再安装 CPU,最后安装内存并组装成计算机。
*/
public class Director{

Builder mBuild = null;

public Director(Builder build){
this.mBuild = build;
}

public Computer CreateComputer(String cpu,String mainboard,String ram){
// 规范建造流程
this.mBuild.buildMainboard(mainboard);
this.mBuild.buildCPU(cpu);
this.mBuild.buildRam(ram);
return mBuild.create();
}
}

客户端调用导演类

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 最后商家用导演类组装计算机。
* 我们只需要提供自己想要的 CPU、主板和内存就可以了。
*/
public class CreateComputer{
public static void main(String[] args){
Builder mBuilder = new MoonComputerBuilder();
Director mDirector = new Director(mBuilder);
// 组装计算机
mDirector.CreateComputer("i7-6700","华擎玩家至尊","三星 DDR4");
}
}

使用场景和优缺点

  • 使用场景:
    • 当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时。
    • 相同的方法,不同的执行顺序,产生不同的事件结果时。
    • 多个部件或零件都可以被装配到一个对象中,但是产生的运行结果又不相同时。
    • 产品类非常复杂,或者产品类中的调用顺序不同而产生了不同的功能。
    • 在创建一些复杂的对象时,这些对象的内部组成构件间的建造顺序是稳定的,但是对象的内部组成构件面临着复杂的变化。
  • 优点:
    • 使用建造者模式可以使客户端不必知道产品内部组成的细节。
    • 具体的建造者类之间是相互独立的,容易扩展。
    • 由于具体的建造者是独立的,因此可以对建造过程逐步细化,而不对其它的模块产生任何影响。
  • 缺点:
    • 产生多余的 Build 对象以及导演类。

备注

参考资料:

Android 进阶之光