设计模式-2-创建型

1. 创建型设计模式

设计模式可以分为创建型(Creational)、结构型(Structural)和行为型(Behavioral)三大类。

创建型设计模式主要关注对象的创建过程,以提高代码的灵活性和可复用性。

以下是一些常见的创建型设计模式:

  1. 单例模式(Singleton):确保一个类只有一个实例,并提供一个全局访问点。

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

  3. 抽象工厂模式(Abstract Factory):提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

  4. 建造者模式(Builder):将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

  5. 原型模式(Prototype):通过复制现有的实例来创建新的实例,而不是通过新建一个实例。

下文将会将这些设计模式进一步拆解。

2. 设计模式解析

2.1. 单例模式(Singleton)

模式介绍

单例模式是一种确保一个类只有一个实例,并提供一个全局访问点的设计模式。这种模式在需要控制资源访问时非常有用,如配置管理器或线程池。单例模式的核心在于全局只有一个实例,并且提供一个访问它的全局访问点。单例模式可以控制客户对实例的访问,可以在实例化时延迟加载,也可以隐藏实例的实现细节。

使用场景

  • 唯一性:当系统需要一个全局唯一对象时,如日志记录器、线程池等。
  • 资源共享:当资源需要被多个模块共享时,如配置信息、连接池等。
  • 控制资源消耗:当对象创建成本较高且全局返回内会频繁使用时,使用单例模式可以减少资源消耗。

优缺点分析

优点:

  • 确保一个类只有一个实例,节省系统资源。
  • 提供一个全局访问点,方便访问。
  • 可以控制实例化过程,例如在实例化时进行权限检查。

缺点:

  • 滥用可能导致系统设计不灵活,增加系统的耦合度。
  • 在多线程环境下需要额外处理同步问题,可能导致性能下降。
  • 单例类的职责过重,违背了单一职责原则。

2.1.1. 饿汉式

饿汉式单例模式是一种在类加载时就完成实例化的单例实现方式。由于其在类加载阶段就“急切”地创建了实例,因此得名“饿汉式”。 这种模式确保了在任何情况下都只有一个实例,并且在提供全局访问点的同时,保证了线程安全。

1
2
3
4
5
6
7
8
9
public class Singleton {
private static final Singleton instance = new Singleton();

private Singleton() {}

public static Singleton getInstance() {
return instance;
}
}

饿汉式单例的优点

  1. 线程安全:由于单例对象在类加载时就已经被创建,而类加载过程在Java中是线程安全的,因此饿汉式单例模式天生就是线程安全的,不需要额外的同步措施。
  2. 简单直观:实现起来相对简单,只需要在类中定义一个私有静态变量并初始化,再提供一个公共静态方法来返回这个变量即可。这种简单的实现方式使得代码易于理解和维护。
  3. 执行效率较高:由于实例在类加载时就已经创建,因此在后续的调用中不需要进行额外的同步或实例化操作,可以直接返回实例,这使得获取对象的速度很快。

饿汉式单例的缺点

  1. 资源效率不高:无论是否使用该实例,都会在类加载时创建实例,可能会导致资源的浪费。如果单例对象的创建过程比较复杂,或者程序运行初期并不需要使用该单例对象,那么这种方式就会造成资源的浪费。
  2. 可能导致启动缓慢:如果单例对象的初始化过程比较耗时,会导致程序启动时的延迟。这是因为在类加载时就需要完成实例的创建和初始化,这可能会延长程序的启动时间。
  3. 缺乏灵活性:由于实例在类加载时就已经创建,这限制了单例对象的创建时机,使得无法根据实际需要来动态创建实例。在某些需要根据环境或配置动态创建单例对象的场景下,饿汉式单例模式就显得不够灵活。

2.1.2. 懒汉式(线程不安全)

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
private static Singleton instance;

private Singleton() {}

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

这种单例实现懒加载方式简单,然而这多线程场景下会出现线程安全问题。即当多个线程同时触发初始化时,可以有多个线程同时通过 instance == 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
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
62
63
64
65
66
public class UnsafeSingleton {
private static UnsafeSingleton instance;
private UnsafeSingleton() {
}
public static UnsafeSingleton getInstance() {
if (instance == null) {
instance = new UnsafeSingleton();
}
return instance;
}

public static void clearInstance() {
instance = null;
}
}

public class UnsafeSingletonThread extends Thread{
private final CountDownLatch latch;
private UnsafeSingleton singleton;

public UnsafeSingletonThread(String name, CountDownLatch latch) {
setName(name);
this.latch = latch;
}

@Override
public void run() {
try {
latch.await();
singleton = UnsafeSingleton.getInstance();
} catch (InterruptedException e) {
// handle exception
}
}

public UnsafeSingleton getSingleton() {
return singleton;
}
}

public static void main(String[] args) {
int testRoundCount = 10000;

int failCount = 0;
for (int i=0;i<testRoundCount;i++) {
// 清理环境
UnsafeSingleton.clearInstance();

CountDownLatch latch = new CountDownLatch(1);

UnsafeSingletonThread thread1 = new UnsafeSingletonThread("thread-1", latch);
UnsafeSingletonThread thread2 = new UnsafeSingletonThread("thread-2", latch);
thread1.start();
thread2.start();

latch.countDown();
UnsafeSingleton singleton1 = thread1.getSingleton();
UnsafeSingleton singleton2 = thread2.getSingleton();
if (singleton1 != singleton2) {
failCount += 1;
}

UnsafeSingleton.clearInstance();
System.out.printf("test round: %s , fail count: %s %n",i , failCount);
}
}

运行以上代码测试,两个线程在10000次竞争中会触发约5-12次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

2.1.2. 懒汉式(线程安全)

为了解决上文中懒汉式线程冲突的问题,因此有了基于锁的线程安全解决方案。

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
private static Singleton instance;

private Singleton() {}

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

这种线程安全的懒汉式单例实现是最为简单的,即通过在方法上加锁实现实例初始化的线程安全。

然而很明显的是,方法级的锁粒度对于解决实例初始化线程安全而言过大了。实例的初始化仅会进行一次,但后续的每一次获取实例都会加锁,从而影响性能。

因此更为合理的方案是双重检查锁定(Double-Checked Locking)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

通过第一次校验可以使得在初始化完成之后,后续的请求不再需要走加锁逻辑。而第二次校验,则是保障实例的初始化不会重复。

第二次校验原因:可能存在两个线程同时通过第一次校验,虽然由于锁的存在会同步地执行同步块代码,然而同步块代码依旧会执行多次。因此需要进行二次校验。

2.2. 工厂方法模式(Factory Method)

模式介绍

工厂方法模式是一种创建型设计模式,它定义了一个创建对象的接口,但由子类决定要实例化的类。工厂方法让类的实例化推迟到子类进行。这种模式的主要目的是通过定义一个创建对象的接口来让子类决定实例化哪一个类。工厂方法模式让类的实例化推迟到子类进行,使得新增对象的创建不会影响调用方的代码。

这种模式的主要目的是封装对象的创建过程,并且能够通过继承和多态性来控制对象的创建过程。它提供了一种机制,可以在不知道具体类的情况下创建对象,从而使得代码更加灵活和可扩展。

使用场景

  1. 创建对象过程复杂:当创建对象需要很多步骤或者复杂的逻辑时,使用工厂方法模式可以将这些步骤封装起来。
  2. 需要创建一系列相关或依赖对象:当需要创建的对象是一系列相关或相互依赖的对象时,工厂方法模式可以提供一种统一的创建接口。
  3. 新增对象创建逻辑时,不修改已有代码:当新增对象的创建逻辑时,不需要修改已有的代码,只需要增加新的工厂类即可。

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface Product { 
void use();
}

public class ConcreteProduct implements Product {
public void use() {
System.out.println("Using ConcreteProduct");
}
}

public abstract class Creator {
public abstract Product factoryMethod();
}

public class ConcreteCreator extends Creator {
public Product factoryMethod() {
return new ConcreteProduct();
}
}

优缺点分析

优点

  1. 代码扩展性好:新增产品类时,只需要新增一个具体的工厂类,不需要修改已有代码,符合开闭原则。
  2. 封装性好:客户端不需要知道具体的产品类,只需要知道工厂接口,提高了模块间的解耦。
  3. 提供了统一的创建接口:客户端通过工厂接口来创建对象,不需要关心对象的具体创建细节。

缺点

  1. 系统复杂度增加:每增加一个产品类,就需要增加一个具体的工厂类,这可能会增加系统的复杂度。
  2. 工厂类数量增加:随着产品类的增加,工厂类的数量也会增加,可能会导致系统难以管理。

2.3. 抽象工厂模式(Abstract Factory)

模式介绍

抽象工厂模式是一种创建型设计模式,是工厂方法模式的进一步抽象,它提供了一个创建一系列相关或相互依赖对象的接口,而不需要指定它们具体的类。这种模式的目的是抽象和封装对象的创建过程,使得系统更容易扩展和维护。

抽象工厂模式允许系统在不指定具体类的情况下创建一系列相关对象,这些对象通常属于同一产品族,但彼此之间可能是不同类的对象。这种模式通过定义一个抽象工厂接口来规范具体工厂的行为,每个具体工厂类实现了这个接口,并创建具体的产品对象。

使用场景

  1. 需要创建一系列相关或相互依赖的对象:当系统中需要生成多个产品对象,而这些对象之间存在一定的关系时。
  2. 系统需要提供多个系列的产品对象:例如,一个图形界面库可能需要提供不同操作系统风格的按钮、窗口等控件,每个系列的产品对象都是相互关联的。
  3. 增加新的产品族时,不修改已有系统:当新增一个产品系列时,不需要修改已有的代码,只需要增加一个新的具体工厂和相应的产品类即可。

代码演示

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
62
63
// 产品接口A
public interface ProductA {
void use();
}

// 产品接口B
public interface ProductB {
void use();
}

// 具体产品类A1
public class ConcreteProductA1 implements ProductA {
public void use() {
System.out.println("Using ConcreteProductA1");
}
}

// 具体产品类A2
public class ConcreteProductA2 implements ProductA {
public void use() {
System.out.println("Using ConcreteProductA2");
}
}

// 具体产品类B1
public class ConcreteProductB1 implements ProductB {
public void use() {
System.out.println("Using ConcreteProductB1");
}
}

// 具体产品类B2
public class ConcreteProductB2 implements ProductB {
public void use() {
System.out.println("Using ConcreteProductB2");
}
}

// 抽象工厂接口
public interface AbstractFactory {
ProductA createProductA();
ProductB createProductB();
}

// 具体工厂类1
public class ConcreteFactory1 implements AbstractFactory {
public ProductA createProductA() {
return new ConcreteProductA1();
}
public ProductB createProductB() {
return new ConcreteProductB1();
}
}

// 具体工厂类2
public class ConcreteFactory2 implements AbstractFactory {
public ProductA createProductA() {
return new ConcreteProductA2();
}
public ProductB createProductB() {
return new ConcreteProductB2();
}
}

优缺点分析

优点

  1. 隔离具体类的生成:客户端不需要知道具体的产品类,只需要知道抽象工厂接口。
  2. 提供一致的接口:客户端通过抽象工厂接口来创建产品,不需要关心具体的产品实现。
  3. 易于扩展:新增产品族时,只需要新增一个具体的工厂和相应的产品类,不需要修改已有代码。

缺点

  1. 系统复杂度增加:每增加一个产品族,就需要增加一个抽象产品和相应的具体产品,以及一个具体的工厂类。
  2. 产品类之间的耦合:产品类必须在同一个工厂中被创建,这限制了产品的组合。

抽象工厂模式是一种强大的设计模式,它通过提供一个创建一系列相关对象的接口,使得系统更容易扩展和维护。这种模式特别适合于需要生成一系列相关或相互依赖对象的场景。

然而,抽象工厂模式本身的实现就较为复杂,因此在实际应用中需要根据具体需求来权衡是否使用。

2.4. 建造者模式(Builder)

模式介绍

建造者模式是一种创建型设计模式,它将一个复杂对象的构建过程和它的表示分离,使得同样的构建过程可以创建不同的表示。建造者模式通常用于创建那些包含多个属性、多个状态和配置的复杂对象。

建造者模式的核心思想是将复杂的对象构建过程封装起来,提供一个“建造者”接口,通过这个接口,可以逐步构建对象的各个部分,最终得到一个完整的对象。这种方式使得对象的创建过程更加清晰,并且可以在不同的场景下构建出不同的对象。

使用场景

  1. 对象的创建过程非常复杂:当对象的构造函数参数列表过长,或者参数很多都是可选的,建造者模式可以提供更好的解决方案。
  2. 需要创建不可变对象:建造者模式可以创建出一旦构建完成就不可更改的对象,增加了对象的安全性。
  3. 相同的构建过程需要创建不同的对象:如果多个对象共享相同的构建过程,但构建的中间状态不同,建造者模式可以提供灵活的构建方式。

代码演示

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
public class Product {
private String partA;
private String partB;
private String partC;

public Product(Builder builder) {
this.partA = builder.partA;
this.partB = builder.partB;
this.partC = builder.partC;
}

public static class Builder {
private String partA;
private String partB;
private String partC;

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

public Builder partB(String partB) {
this.partB = partB;
return this;
}

public Builder partC(String partC) {
this.partC = partC;
return this;
}

public Product build() {
return new Product(this);
}
}
}

优缺点分析

优点

  1. 封装性好:隐藏了复杂的构建过程,外部只需要通过建造者接口来构建对象。
  2. 创建不可变对象:建造者模式可以构建出一旦构建完成就不可更改的对象。
  3. 相同的构建过程可以创建不同的对象:通过不同的方法链调用来创建不同的对象。

缺点

  1. 创建新的具体建造者类:每增加一个产品类,可能需要增加一个新的具体建造者类,增加了系统的复杂度。
  2. 可能存在过多的参数:如果产品类属性过多,建造者模式可能会导致建造者类中的参数过多,难以管理。

建造者模式是一种非常适合构建复杂对象的设计模式。它通过封装复杂的构建过程,使得对象的构建更加灵活和清晰。建造者模式特别适合于创建不可变对象和需要多种配置的产品对象。

2.5. 原型模式(Prototype)

模式介绍

原型模式是一种创建型设计模式,它通过复制现有的实例来创建新的实例,而不是通过新建一个实例。这种模式的主要目的是通过复制现有的实例来创建新的实例,从而避免了复杂的构造过程。原型模式使得对象的复制变得更加简单和高效。

使用场景

  1. 创建对象成本较高:当对象的创建需要消耗大量资源,如时间或内存时,使用原型模式可以减少这些开销。
  2. 需要通过复制来创建新对象:当系统需要通过复制一个已有对象来创建新对象,而不是新建一个对象时。
  3. 需要保留对象的历史状态:原型模式可以用于实现对象的版本控制,通过复制对象的历史状态来创建新版本。

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface Prototype {
Prototype clone();
}

public class ConcretePrototype implements Prototype {
private String field;

public ConcretePrototype(String field) {
this.field = field;
}

public Prototype clone() {
return new ConcretePrototype(field);
}
}

优缺点分析

优点

  1. 简化对象创建过程:原型模式通过复制现有对象来创建新对象,简化了对象的创建过程。
  2. 提高对象创建的效率:对于创建过程复杂或成本高的对象,原型模式可以提高效率。
  3. 保留对象状态:原型模式可以复制对象的内部状态,用于实现对象的复制或版本控制。

缺点

  1. 复制过程可能复杂:对于包含循环引用的对象,复制过程可能会变得复杂,需要额外处理。
  2. 性能问题:如果复制对象包含大量资源,复制过程可能会消耗较多的时间和内存。
  3. 深拷贝与浅拷贝问题:需要明确复制对象是深拷贝还是浅拷贝,这可能影响对象的复制结果。

3. 总结分析

创建型设计模式的核心目标是“封装复杂性”和“提高灵活性”。这些模式通过不同的策略来实现这些目标:

  • 单例模式 通过控制实例化过程,确保全局只有一个实例,从而封装了对唯一资源的访问复杂性。
  • 工厂方法模式和抽象工厂模式 通过封装对象的创建过程,将客户代码与具体类的实例化逻辑解耦,提高了系统的灵活性和可扩展性。
  • 建造者模式 通过分离构建过程和对象表示,封装了复杂对象的构建复杂性,使得对象的构建更加清晰。
  • 原型模式 通过复制现有对象来创建新对象,封装了对象创建的复杂性,特别是在对象创建成本高的情况下。

创建型设计模式提供了强大的工具来解决对象创建过程中的问题,但它们并不是万能的。在实际开发中,我们需要根据具体的业务需求和场景来选择最合适的模式,并合理地实现它们。


设计模式-2-创建型
https://blog.linum.top/2024/11/24/设计模式-2-创建型/
作者
linum
发布于
2024年11月24日
更新于
2024年12月27日
许可协议