引言
本篇将以一个例子来带你了解享元模式。
假设你开了一家面馆,面条的种类(如拉面、刀削面)就像是享元模式中的内部状态,这些是面的基底,可以提前准备好并共享。而每碗面不同的配料(如牛肉、鸡蛋、蔬菜)则像是外部状态,这些是每次顾客点单时可能不同的部分。
享元的核心意图
-
减少大量相似对象的创建,节约内存占用
-
把对象状态分为:
-
内部状态(Intrinsic):可共享、不可变。
-
外部状态(Extrinsic):因场景不同而变化,不共享。
-
-
通过共享内在状态,只要外部状态不同,就组合成一个完整对象,兼顾节省和多样。
该模式组成角色
- Flyweight(享元接口/抽象类)
定义接受外部状态的方法,如operation(extrinsicState)
。
无论是拉面还是刀削面,都需要实现一个“做面”的操作。操作是享元接口,定义制作面条的基本步骤,比如煮面、加配料等。 - ConcreteFlyweight(具体享元类)
保存内部状态,如拉面和刀削面就像是具体享元类。
它们各自有固定的面条种类(内部状态),但都实现了“做面”的操作。当顾客点单时,面馆会根据选择的面条种类,使用相应的具体享元类来制作面条。 - UnsharedFlyweight(非享元类,可选)
代表那些不允许共享的状态,通常作为客户端独立处理。
可乐不属于正常面馆应该常备的食材,根据不同的顾客要求来做一定的定制化进货,可乐就是非享元类。 - FlyweightFactory(享元工厂)
利用缓存(通常是哈希结构),管理创建与共享逻辑,客户端通过 factory 获取享元对象。
面馆厨房就像是享元工厂,负责准备和提供各种面条种类。当顾客点单时,厨房会根据订单中的面条种类,直接提供已经准备好的面条基底。 - Client(客户端)
维护和传递外部状态,调用 factory 获取享元,再执行操作。
顾客点单时,会选择面条种类和配料。面馆会根据顾客的选择,使用厨房提供的面条基底(享元对象),并加上顾客指定的配料(外部状态),来完成一碗面的制作。
模式结构图
如上图所示:
-
Client
依赖FlyweightFactory
; -
Factory
缓存并复用ConcreteFlyweight
; -
Client
将extrinsicState
传入享元实例,并调用operation()
核心流程代码实例
1. 识别状态
-
内部状态:面条基底(如拉面、刀削面)。
-
外部状态:顾客为面条选择的配料(如牛肉、鸡蛋、蔬菜、豆腐、海鲜等)。
2. 创建享元接口
interface Flyweight {
void operation(Map<String, String> extrinsicData);
}
3. 享元对象实现
// 具体享元实现(拉面)
class LaMian implements Flyweight {
private String intrinsicState = "拉面基底"; // 内部状态:拉面基底
@Override
public void operation(Map<String, String> extrinsicData) {
// 操作:制作拉面,加入外部状态(配料)
System.out.println("使用" + intrinsicState + ",加入" + extrinsicData.get("配料") + ",开始制作拉面。");
}
}
// 具体享元实现(刀削面)
class DaoXiaoMian implements Flyweight {
private String intrinsicState = "刀削面基底"; // 内部状态:刀削面基底
@Override
public void operation(Map<String, String> extrinsicData) {
// 操作:制作刀削面,加入外部状态(配料)
System.out.println("使用" + intrinsicState + ",加入" + extrinsicData.get("配料") + ",开始制作刀削面。");
}
}
//LaMian和DaoXiaoMian类作为具体享元类,它们包含了面条基底作为内部状态(intrinsicState)。
//这些内部状态在享元对象创建时被初始化,并在后续的操作中共享。不同的顾客订单可以共享相同的面条基底对象,只要他们选择的面条种类相同。
// 非享元类(可乐)
class UnsharedFlyweight {
public void unsharedOperation(String drink) {
// 处理非享元类的操作,例如检查饮料是否可用
if (drink.equals("可乐") && !isAvailable()) {
System.out.println("抱歉,可乐已售罄。");
} else {
System.out.println("享受您的" + drink + "。");
}
}
private boolean isAvailable() {
// 模拟检查饮料是否可用的逻辑
// 这里可以添加实际的库存检查逻辑
return false; // 假设可乐已售罄
}
}
4. 享元工厂
class FlyweightFactory {
private Map<String, Flyweight> flyweights = new HashMap<>(); // 存储享元对象的字典
public Flyweight get(String key) {
// 根据键(面条种类)获取或创建享元对象
if (!flyweights.containsKey(key)) {
if (key.equals("拉面")) {
flyweights.put(key, new LaMian());
} else if (key.equals("刀削面")) {
flyweights.put(key, new DaoXiaoMian());
} else {
throw new IllegalArgumentException("未知的面条种类");
}
}
return flyweights.get(key);
}
}
当flyweights.containsKey()方法发现请求的面条种类不存在于映射中时,它会创建一个新的享元对象(如LaMian或DaoXiaoMian的实例),并将其添加到映射中。
这个过程确保了相同类型的面条基底对象只被创建一次,后续的请求将共享这个已存在的对象。
5. 客户端使用
class Client {
private FlyweightFactory factory = new FlyweightFactory(); // 创建享元工厂实例
private UnsharedFlyweight unsharedFlyweight = new UnsharedFlyweight(); // 创建非享元类实例
public void orderNoodles(String noodleType, String toppings) {
// 顾客点单面条,根据面条种类和配料制作面条
Flyweight noodle = factory.get(noodleType); // 从工厂获取享元对象
Map<String, String> extrinsicData = new HashMap<>();
extrinsicData.put("配料", toppings);
noodle.operation(extrinsicData); // 调用享元对象的操作,传入外部状态(配料)
}
public void orderDrink(String drink) {
// 顾客点单饮料,使用非享元类处理
unsharedFlyweight.unsharedOperation(drink);
}
}
6. 运行结果
public class Main {
public static void main(String[] args) {
Client client = new Client();
client.orderNoodles("拉面", "牛肉, 鸡蛋"); // 顾客1点拉面,加牛肉和鸡蛋
client.orderNoodles("刀削面", "蔬菜, 豆腐"); // 顾客2点刀削面,加蔬菜和豆腐
client.orderNoodles("拉面", "海鲜"); // 顾客3点拉面,加海鲜
client.orderDrink("可乐"); // 顾客4点可乐
}
}
通过享元工厂的管理和享元对象的共享,代码示例展示了享元模式如何有效地减少内存消耗。相同类型的面条基底对象只被创建一次,避免了重复创建和存储相同对象的内存开销。同时,通过共享享元对象,代码也提高了性能,因为创建和初始化对象的操作被减少,从而加快了订单处理速度。
此即典型缓存 + 不变享元的高级实现 。
7. UML图
适用场景
- 对象数量巨大,但它们有一部分状态完全相同。
图形应用中大量相同纹理模型;
文本编辑器中的字符渲染; - 内外状态可以明确区分,且内部状态支持共享。
缓存模式(如数据库连接池、对象池 - 重复创建对象非常耗内存或影响性能 。
Java 中的String
常量池机制。
优缺点分析
优点
-
大幅减少重复对象、节省内存;
-
提高系统性能,对小而多重复对象尤其有效;
-
强制实现对象状态不可变,安全性提升。
缺点
-
增加设计复杂度,需拆分状态;
-
客户端必须管理外部状态,使用更繁;
-
如果外部状态多且多变,分享性下降,复杂度增加。
进阶要点
-
状态不可变性:保证享元对象共享安全,如建成
final
,无 setter。 -
缓存机制优化:可使用弱引用(如
WeakHashMap
)自动回收闲置享元。
小结
享元模式通过 将相同的部分抽出共享,保留变化的外部状态,有效减少大规模对象创建带来的内存和性能问题。
它本质上是一种结构优化模式,适用于大量相同对象场景。设计时要权衡复杂度与性能收益,合理拆分状态和部署工厂模式。