享元模式有哪些角色?调用流程是什么?
简化版
享元模式主要包括享元接口、具体享元、享元工厂和客户端。客户端通过享元工厂获取共享对象,再把外部状态传给享元对象的方法执行具体逻辑。
详细版
享元模式常见角色有:
- Flyweight:享元接口,定义接收外部状态的操作。
- ConcreteFlyweight:具体享元,保存内部状态。
- FlyweightFactory:享元工厂,负责缓存和复用享元对象。
- Client:客户端,维护外部状态,并从工厂获取享元。
调用流程通常是:
- 客户端根据内部状态 key 请求享元对象。
- 工厂先查缓存,有则返回,没有则创建并缓存。
- 客户端调用享元方法时传入外部状态。
- 享元对象使用内部状态和外部状态完成工作。
完整版教学
一、Flyweight:享元接口
享元接口定义对象对外提供的行为。它通常会把外部状态作为方法参数。
例如棋子:
interface ChessPiece {
void display(int x, int y);
}
x 和 y 是外部状态,不保存在棋子对象内部,而是在使用时传入。
二、ConcreteFlyweight:具体享元
具体享元保存可共享的内部状态。
class ConcreteChessPiece implements ChessPiece {
private final String color;
public ConcreteChessPiece(String color) {
this.color = color;
}
@Override
public void display(int x, int y) {
System.out.println(color + "棋落在:" + x + "," + y);
}
}
这里 color 是内部状态。黑棋对象可以被很多落子位置复用。
三、FlyweightFactory:享元工厂
享元工厂是享元模式的关键角色。它负责管理共享对象池,保证同样内部状态对应的享元对象可以复用。
class ChessPieceFactory {
private final Map<String, ChessPiece> pool = new HashMap<>();
public ChessPiece getChessPiece(String color) {
return pool.computeIfAbsent(color, ConcreteChessPiece::new);
}
}
客户端不应该随意 new 享元对象,否则共享池就失效了。
四、Client:客户端
客户端负责维护外部状态。
例如棋盘需要记录每一步棋的位置:
class ChessBoard {
private final ChessPieceFactory factory = new ChessPieceFactory();
public void put(String color, int x, int y) {
ChessPiece piece = factory.getChessPiece(color);
piece.display(x, y);
}
}
棋盘知道坐标,棋子知道颜色。两者职责分开。
五、完整调用链
一次享元调用可以拆成:
- 客户端识别需要的内部状态,例如颜色是黑色。
- 客户端向工厂请求黑棋享元。
- 工厂检查对象池。
- 如果对象池已有黑棋,直接返回。
- 如果没有,创建黑棋并放入对象池。
- 客户端传入坐标等外部状态,调用享元行为。
这条链路中,真正避免重复创建的是享元工厂。
六、角色边界要清楚
享元对象不应该保存外部状态,例如棋子不应该保存坐标。否则同一个棋子对象被多个位置共享时,状态会相互覆盖。
享元工厂也不应该承担过多业务逻辑。它主要负责对象复用和生命周期管理,复杂业务应该留在客户端或应用服务层。
七、常见误区与追问
享元流程从完整键开始:客户端把内部状态键交给 Factory,工厂返回1个共享 ConcreteFlyweight;客户端再把本次外部状态传给业务方法。若系统含100个上下文但只有4个内部状态组合,Factory 理想上只维护4个共享实例。UnsharedConcreteFlyweight 则用于确实不能共享的特殊对象。
| 检查维度 | 判定依据 |
|---|---|
| FlyweightFactory | 按键查找、创建并管理共享对象 |
| Context | 保存或计算外部状态并调用享元 |
Context(E) -> Factory(key I) -> Flyweight(I) -> operation(E)
记忆钩子:工厂找“共性”,上下文带“个性”,两者在调用时合并。
- 误区:客户端可以绕过工厂随意 new 共享对象。 这会破坏按键复用,通常应集中由工厂创建。
- 追问:UnsharedConcreteFlyweight 是否必须? 不是,它只是经典结构中为不可共享情况保留的可选角色。
- 误区:Factory 负责保存外部状态。 外部状态随使用变化,应由 Context 或调用者管理。
- 追问:共享对象如何安全发布? 构造完成后通过并发安全映射发布,并保持内部状态不可变。
- 追问:流程测试检查什么? 检查命中复用、不同键隔离,以及外部状态不会写入共享实例。
八、加强记忆
享元模式的角色可以记成“工厂管共享,对象管内部,客户端管外部”:工厂负责复用享元,享元保存可共享状态,客户端在调用时传入变化状态。