享元模式
1/20/24About 7 min
享元模式可以通过共享对象减少系统中低等级的、详细的对象数目。如果一个类实例包含用来互换使用的相同信息,该模式允许程序通过共享一个接口来避免使用多个具有相同信息的类实例所带来的开销。

主要角色:
- 抽象享元(Flyweight)角色:
这通常是一个接口或抽象类,定义了具体享元类公共的方法和内部状态(Intrinsic State)的访问方式。 内部状态是存储在享元对象内部,并且在整个系统中可以被多个享元对象共享的状态。 抽象享元角色也可能提供一个方法来接收外部状态(Extrinsic State),外部状态是不能被共享的,它依赖于具体的使用场景。 - 具体享元(Concrete Flyweight)角色:
实现抽象享元角色所定义的接口,为内部状态提供了存储空间,并实现了相应的操作。 具体享元对象是可共享的,系统中对于具有相同内部状态的对象只创建一个实例。 通常结合单例模式或其他机制来确保同一内部状态对应的具体享元只有一个实例。 - 享元工厂(Flyweight Factory)角色:
负责创建和管理享元对象,客户端不直接创建具体享元对象,而是通过享元工厂获取。 工厂会根据需要重用已有的享元对象,或者在没有可用享元对象时才创建新的实例。 - 客户端(Flyweight Client)角色:
客户端并不直接操作具体享元对象,而是通过抽象享元接口与享元对象交互,并向享元对象传入外部状态。
信息
通过这种设计,当系统中有大量相似对象时,可以通过享元模式大大节省内存消耗并提高程序性能。例如,在抢火车票的例子中,每种座位类别(如硬座、软座、硬卧等)可以作为一个具体享元类,它们共享诸如起始站、终点站和价格等不变的内部状态,而购买者的个人信息则作为外部状态由客户端在请求时传入。
代码实现
抽象享元
抽象享元定义了传入外部状态的方法。如果子类之间的内部状态是不互通的,可以将内部状态定义到子类中。建议是有一个通用的状态,并且这个状态是可以通过工厂定位到唯一的实例的。
public abstract class Flyweight {
public abstract void operation(String extrinsicState);
}具体享元
具体享元实现了抽象享元中的抽象行为,并且存储了内部状态。
public class ConcreteFlyweight extends Flyweight {
private String intrisicState;
public ConcreteFlyweight(String intrisicState) {
this.intrisicState = intrisicState;
}
@Override
public void operation(String extrinsicState) {
System.out.println("ConcreteFlyweight operation, extrinsicState:" + extrinsicState);
}
}之后我们仿照上面的代码定义UnshareConcreteFlyweight。
享元工厂
享元工厂是享元对象的工厂,它负责创建和管理享元对象,享元工厂是单例模式。如果不存在享元则创建一个新的享元对象;否则直接从内存中获取。
public class FlyweightFactory {
private static Map<String, Flyweight> flyweights = new HashMap<>();
public static Flyweight getConcreteFlyweight(String state) {
if (!flyweights.containsKey(state)) {
flyweights.put(state, new ConcreteFlyweight(state));
}
return flyweights.get(state);
}
}客户端
客户端通过享元工厂获取具体享元对象,并传入外部状态。
public class FlyweightClient {
public static void main(String[] args) {
Flyweight flyweight1 = FlyweightFactory.getConcreteFlyweight("state1");
flyweight1.operation("state1");
}
}与其他设计模式的对比
享元模式 vs 单例模式
| 特性 | 享元模式 | 单例模式 |
|---|---|---|
| 目的 | 共享细粒度对象,减少对象数量 | 确保类只有一个实例 |
| 实例数量 | 可以有多个实例,但相同内部状态的实例只创建一个 | 只有一个实例 |
| 内部状态 | 共享的、不变的状态 | 无内部/外部状态之分 |
| 外部状态 | 非共享的、可变的状态,由客户端传入 | 无外部状态 |
| 适用场景 | 系统中有大量相似对象,需要节省内存 | 需要确保类只有一个实例 |
享元模式 vs 原型模式
| 特性 | 享元模式 | 原型模式 |
|---|---|---|
| 目的 | 共享细粒度对象,减少对象数量 | 通过复制现有对象创建新对象 |
| 创建方式 | 复用已有对象,避免重复创建 | 复制现有对象 |
| 状态共享 | 内部状态共享,外部状态由客户端传入 | 所有状态都被复制 |
| 适用场景 | 系统中有大量相似对象,需要节省内存 | 创建对象成本较高,或需要保持对象状态 |
享元模式 vs 工厂方法模式
| 特性 | 享元模式 | 工厂方法模式 |
|---|---|---|
| 目的 | 共享细粒度对象,减少对象数量 | 创建对象,隐藏创建逻辑 |
| 创建方式 | 复用已有对象,避免重复创建 | 创建新对象 |
| 实例管理 | 管理对象池,复用对象 | 创建对象,不管理对象生命周期 |
| 适用场景 | 系统中有大量相似对象,需要节省内存 | 需要隐藏对象创建逻辑 |
总结
享元模式是一种结构型设计模式,它通过共享技术有效地支持大量细粒度的对象,减少系统中的对象数量,从而节省资源。这种模式的核心思想是将对象的内部状态(共享部分)和外部状态(非共享部分)分离,使得多个对象可以共享同一个内部状态,从而减少内存消耗。
核心思想
将对象的内部状态和外部状态分离,内部状态是共享的、不变的,外部状态是可变的、非共享的,由客户端传入。
主要角色
- 抽象享元(Flyweight):定义了享元对象的接口,通常包含一个接收外部状态的方法。
- 具体享元(ConcreteFlyweight):实现抽象享元接口,存储内部状态,是可共享的对象。
- 享元工厂(FlyweightFactory):负责创建和管理享元对象,客户端通过工厂获取享元对象。
- 客户端(Client):使用享元对象,传入外部状态。
实现方式
- 内部状态与外部状态分离:将对象的状态分为内部状态和外部状态,内部状态是共享的,外部状态由客户端传入。
- 享元工厂:使用享元工厂管理享元对象,复用已有对象,避免重复创建。
- 对象池:通过对象池存储享元对象,提高对象的复用率。
优点
- 减少对象数量:通过共享技术,减少系统中的对象数量,从而节省内存。
- 提高性能:减少对象的创建和销毁次数,提高系统性能。
- 提高可维护性:将内部状态和外部状态分离,便于维护和扩展。
- 符合开闭原则:可以方便地添加新的享元类,无需修改客户端代码。
缺点
- 增加系统复杂度:需要将对象的状态分为内部状态和外部状态,增加了系统的复杂度。
- 可能影响性能:如果外部状态过多,可能会影响系统性能。
- 设计难度增加:需要合理设计内部状态和外部状态,确保内部状态是可以共享的。
适用场景
- 系统中有大量相似对象:需要处理大量相似的对象,导致内存消耗过大。
- 对象的大部分状态可以外部化:对象的状态可以分为内部状态和外部状态,内部状态是共享的,外部状态由客户端传入。
- 需要节省内存:系统内存资源有限,需要减少对象数量。
- 对象的创建成本较高:对象的创建成本较高,需要复用已有对象。
实际应用
在实际开发中,享元模式常用于:
- 文本编辑器:字符对象的共享,减少字符对象的数量。
- 图形系统:图形元素(如点、线、面)的共享。
- 数据库连接池:共享数据库连接,减少连接的创建和销毁。
- 字符串常量池:Java中的字符串常量池就是享元模式的应用。
- 游戏开发:游戏中的大量相似对象(如树木、岩石、敌人等)的共享。