享元模式-Flyweight Pattern

序言

在软件开发的世界里,效率和资源利用率始终是开发者们追求的重要目标。随着系统规模的不断扩大,如何优化内存使用、提升性能,成为设计模式中不可忽视的一环。享元模式,以其独特的视角,为这一目标提供了一种精巧的解决方案。

享元模式源于对共享和复用机制的深刻洞察。它通过共享相似或相同的数据,减少对象创建的数量,从而降低内存占用,提高系统性能。这种模式尤其适用于那些具有大量相似对象的应用场景,如文档编辑、游戏开发等。

然而,享元模式并非银弹,它也有其适用的条件和局限性。在追求高效与节约的同时,我们不得不面对系统的复杂性增加、代码维护难度提高等挑战。因此,深入了解和合理运用享元模式,对于软件工程师来说,既是一项必备的技能,也是一次对设计原则和工程实践的全面考验。

接下来,让我们一同探索享元模式的奥秘,从概念的剖析到实际应用的演绎,从优点的展现到缺点的反思,全方位地理解这一设计模式的精髓,以便在软件开发的道路上,更加从容和明智地前行。

定义

Use sharing to support large numbers of fine-grained objects efficiently.

运用共享技术有效地支持大量细粒度的对象。

结构

有一个停车场云平台,这个云平台对接了不同的停车场厂商。云平台与停车场的通信采用一套标准协议,而云平台与停车场之间有一层代理层,代理层负责接收云平台的指令,然后下发给停车场。

$2停车场云平台厂商A接口代理厂商B接口代理厂商C接口代理停车场系统A1停车场系统A2停车场系统B停车场系统C标准协议标准协议标准协议

停车场平台与代理层通信采用一个标准客户端。该客户端实现了与代理通信的全部协议细节。如果我们每次下发指令都创建客户端,那么我们都需要不停的创建重复的对象。同时客户端还要与代理层建立连接,又会消耗一定的系统资源。那最好的方式就是针对每个代理,我们仅在需要时创建一个客户端,在后续与这个代理的通信过程中,都使用这个客户端。

$2ParkingTypeABCParkingProxyClientopen();ParkingProxyClientImplParkingProxyClientImpl(type:ParkingType)open();ParkingProxyClientFactoryclientMap: Map<ParkingType, ParkingProxyClient>getParkingClient(type: ParkingType): ParkingProxyClient

代码实现

以下是上面例子的简单实现,这里不实现具体业务。仅为说明享元模式的原理。

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
public enum ParkingType {
A,B,C
}

public interface ParkingProxyClient {

/**
* 执行抬杆操作
*
* @param parkingId 停车场ID
*/
void open(String parkingId);
}

public class ParkingProxyClientImpl implements ParkingProxyClient {

private final ParkingType parkingType;

public ParkingProxyClientImpl(ParkingType parkingType) {
this.parkingType = parkingType;
this.connect();
}

@Override
public void open(String parkingId) {
System.out.println("代理:" + parkingType.name() + ",停车场:" + parkingId + ",执行抬杆操作");
}

private void connect() {
System.out.println("读取停车代理:" + parkingType.name() + "的配置,并建立连接");
}
}

public class ParkingProxyClientFactory {

private final Map<ParkingType, ParkingProxyClient> clientMap = new HashMap<>();

public ParkingProxyClient getParkingClient(ParkingType parkingType) {
if (this.clientMap.containsKey(parkingType)) {
System.out.println("代理:" + parkingType + "的客户端已初始化,直接使用已创建的客户端。");
return this.clientMap.get(parkingType);
}

ParkingProxyClient client = new ParkingProxyClientImpl(parkingType);
clientMap.put(parkingType, client);
return client;
}

}

使用示例

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

public static void main(String[] args) {

ParkingProxyClientFactory factory = new ParkingProxyClientFactory();
ParkingProxyClient client = factory.getParkingClient(ParkingType.A);
client.open("A01");

System.out.println("-------------------------");

ParkingProxyClient client2 = factory.getParkingClient(ParkingType.B);
client2.open("B01");

System.out.println("-------------------------");

ParkingProxyClient client3 = factory.getParkingClient(ParkingType.A);
client.open("A02");
}
}

输出

1
2
3
4
5
6
7
8
读取停车代理:A的配置,并建立连接
代理:A,停车场:A01,执行抬杆操作
-------------------------
读取停车代理:B的配置,并建立连接
代理:B,停车场:B01,执行抬杆操作
-------------------------
代理:A的客户端已初始化,直接使用已创建的客户端。
代理:A,停车场:A02,执行抬杆操作

由上面的运行结果可以看出,我们第二次向代理A发送指令的时候使用的就是已经创建过的代理客户端。

在开源框架中的应用

享元模式一个典型的应用就是JDK中的字符串常量池(String Pool),先来看下面的代码

1
2
3
4
5
6
7
8
9
public class StringPoolTest {

public static void main(String[] args) {
String a1 = "aaa";
String a2 = "aaa";

System.out.println(a1 == a2);
}
}

输出:true

我们知道Java中的==对于对象类型变量的比较(8种基本类型外的类型),实际上比较的是变量所指向的内存地址是否一致。以上代码说明a1a2指向的是同一个内存地址。

这一点从断点调试的结果也能看出。

这是因为字段传对象aaa已经被创建过了,这是String会将其放入到常量池中,因此下次使用时是直接从常量池中获取的(但是如果使用new String("aaa"")时,总是会创建一个新的字符串对象)。实现这个功能的核心方法是 Stringintern方法,但是这个方法是一个native方法。无法查看其源码(当然如果有兴趣的话可以阅读JDK源码,从JDK源码中找到具体实现)。

何时使用

享元模式(Flyweight Pattern)通常在以下情况下使用:

  • 大量相似对象: 当系统中存在大量相似的对象,并且这些对象可以共享一些内部状态时,使用享元模式可以帮助减少内存占用,提高系统性能。
  • 内存敏感型应用: 在内存敏感型应用中,如游戏开发或图形编辑软件中,使用享元模式可以有效地管理大量相似对象的内存占用,从而提高系统的性能和响应速度。
  • 减少对象创建: 通过共享内部状态,享元模式可以减少对象的创建,降低系统的内存占用和对象创建的开销。
  • 多个共享对象: 当多个对象需要共享一些内部状态时,使用享元模式可以避免重复创建相似对象,从而减少系统的内存占用。

享元模式适用于需要管理大量相似对象并且希望减少内存占用的情况。

与其他设计模式的联系

享元模式与单例模式和工厂模式有一些相似之处。

  • 单例模式(Singleton Pattern): 享元模式和单例模式都涉及到对对象的共享和复用。单例模式是确保一个类只有一个实例,并提供一个全局访问点,而享元模式是共享多个相似对象的内部状态。
  • 工厂模式(Factory Pattern): 享元模式和工厂模式都与对象的创建和管理有关。工厂模式是用来创建对象的模式,而享元模式是用来共享对象的内部状态,以减少内存占用和提高性能。

虽然享元模式与单例模式和工厂模式有一些相似之处,但它们的主要目的和应用场景有所不同。享元模式主要用于共享大量相似对象的内部状态,以减少内存占用和提高系统性能。

总结

享元模式是一种结构型设计模式,旨在通过共享来优化大量相似对象的内存使用。这种模式尤其适用于那些具有大量相似对象的应用场景,如文档编辑、游戏开发等。

在享元模式中,我们通常需要将对象的状态分为内部状态和外部状态。内部状态是共享的,不会随环境变化而改变,因此可以共享;而外部状态则是随环境变化的,不能共享。这种设计允许系统在运行时创建大量相似的对象,而不会导致内存资源的浪费,因为相同的内部状态是被共享的。

享元模式通过共享机制来解决特定类型的性能问题。在需要优化内存使用和提高性能的场景下,享元模式是一种非常有效的设计策略。

作者

大扑棱蛾子(jaune162@126.com)

发布于

2024-02-24

更新于

2024-10-21

许可协议

评论