摘要
享元设计模式是一种结构型设计模式,旨在通过共享对象减少内存占用和提升性能。其核心思想是将对象状态分为内部状态(可共享)和外部状态(不可共享),并通过享元工厂管理共享对象池。享元模式包含抽象享元类、具体享元类、非共享具体享元类和享元工厂类。它适用于处理大量相似对象的场景,如文档编辑器中的字符对象。文章还提供了享元模式的实现方式、适合与不适合的使用场景、实战示例以及与其他设计模式的比较。
1. 享元设计模式定义
享元设计模式(Flyweight Pattern) 是一种结构型设计模式,用于减少对象的数量,以节省内存和提高性能。享元模式通过共享内存中已经存在的对象,避免重复创建相同内容的对象,适用于大量相似对象的场景。
1.1.1. 核心思想
把对象状态划分为:
- 内部状态(可共享,存储在享元对象中)
- 外部状态(不可共享,由客户端维护)
享元工厂(FlyweightFactory): 负责管理共享对象的池,复用已有实例。
1.1.2. 举例说明
假设一个文档编辑器中有 10 万个字符,但字符集就 128 个(ASCII),如果每个字符都是独立对象,将浪费大量内存。此时可以:
- 把字符内容作为内部状态(可共享)
- 把字体、大小、颜色作为外部状态(由外部控制)
- 同一个字符内容只创建一次对象,由享元工厂复用
2. 享元设计模式结构
享元模式包含如下角色:
- Flyweight: 抽象享元类
- ConcreteFlyweight: 具体享元类
- UnsharedConcreteFlyweight: 非共享具体享元类
- FlyweightFactory: 享元工厂类
2.1. 享元设计模式类图
2.2. 享元设计模式时序图
3. 享元设计模式实现方式
享元设计模式的实现方式主要围绕 对象共享池 和 内部状态与外部状态的分离。它通过一个享元工厂(FlyweightFactory)来管理共享对象,避免重复创建,从而节省内存。
3.1. 1️⃣ 定义抽象享元接口(Flyweight)
public interface Flyweight {
void operation(String externalState); // 外部状态由调用方传入
}
3.2. 2️⃣ 创建具体享元类(ConcreteFlyweight)
public class ConcreteFlyweight implements Flyweight {
private final String intrinsicState; // 内部状态,能共享
public ConcreteFlyweight(String intrinsicState) {
this.intrinsicState = intrinsicState;
}
@Override
public void operation(String externalState) {
System.out.println("共享[" + intrinsicState + "],非共享[" + externalState + "]");
}
}
3.3. 3️⃣ 创建享元工厂类(FlyweightFactory)
public class FlyweightFactory {
private final Map<String, Flyweight> pool = new HashMap<>();
public Flyweight getFlyweight(String key) {
if (!pool.containsKey(key)) {
pool.put(key, new ConcreteFlyweight(key));
}
return pool.get(key);
}
public int getPoolSize() {
return pool.size();
}
}
3.4. 4️⃣ 客户端调用(Client)
public class Client {
public static void main(String[] args) {
FlyweightFactory factory = new FlyweightFactory();
Flyweight a1 = factory.getFlyweight("A");
Flyweight a2 = factory.getFlyweight("A"); // 重复,不创建新对象
a1.operation("外部状态1");
a2.operation("外部状态2");
System.out.println("共享对象数量:" + factory.getPoolSize()); // 输出:1
}
}
3.5. ✅ 享元设计模式总结
要素 |
说明 |
内部状态 |
可以共享,存储在享元对象中,如字符、类型 |
外部状态 |
每次调用时由客户端传入,不在享元内部存储 |
工厂类 |
管理共享对象的创建与复用 |
缓存池 |
使用 HashMap 或 ConcurrentHashMap 存储共享对象 |
线程安全注意点 |
在并发环境下需保证工厂创建逻辑线程安全(如加锁或使用 ConcurrentMap) |
4. 享元设计模式适合场景
4.1. ✅ 适合使用享元设计模式的场景
场景 |
说明 |
大量重复对象需创建,且状态大部分相同 |
比如:文字编辑器中的字符对象、地图中的草地格子、游戏中的粒子等,能显著减少内存开销。 |
对象创建成本高,希望通过复用来减少系统负担 |
比如:金融系统中共享的“黑名单规则”、“风控维度元数据”等。 |
对象状态可拆分为可共享的内部状态和不可共享的外部状态 |
共享部分放入享元,变动部分交给外部传入,从而实现高复用。 |
系统中存在大量细粒度对象,结构相似、功能一致 |
如图形系统中的形状节点、文档编辑器中的字体、格式等。 |
缓存池或对象池机制的实现场景 |
比如:数据库连接池、线程池、元数据池、图标池、缓存字典等。 |
4.2. ❌ 不适合使用享元模式的场景
场景 |
原因 |
对象状态频繁变化,不可拆分共享/不共享状态 |
对象状态不能被提取为外部状态时,就无法有效共享,甚至会导致共享污染。 |
对象之间差异太大,无法复用或没有可共享部分 |
如果每个对象都是完全不同的个体,享元模式无法带来价值。 |
系统对对象独立性要求高,不允许共享 |
比如线程不安全或敏感业务逻辑要求每个对象独立维护生命周期。 |
对象数量本身不多,内存开销可以接受 |
引入享元结构反而增加了系统复杂度、调试难度,不值得。 |
共享对象内部包含资源引用,如 Socket、File 等 |
资源不允许多个业务共享,使用享元会造成资源冲突或数据错乱。 |
4.3. 🧠 享元模式的场景总结
项目 |
适合使用享元模式 |
不适合使用享元模式 |
对象数量 |
✅ 大量重复 |
❌ 数量少 |
状态结构 |
✅ 可拆分内外状态 |
❌ 状态复杂或强耦合 |
系统压力 |
✅ 内存敏感,需优化 |
❌ 性能足够,优化收益低 |
可复用性 |
✅ 可复用部分明显 |
❌ 无明显共享逻辑 |
系统复杂度 |
✅ 有控制成本价值 |
❌ 小项目/临时代码 |
5. 享元设计模式实战示例
下面是一个 享元设计模式在金融风控系统中的 Spring 实战示例,场景为:风控规则元数据共享池,用于缓存和复用规则的静态定义,减少重复加载和内存占用。在风控系统中,不同的策略规则经常引用相同的“规则定义”(如规则编号、描述、字段映射等)。这些定义是 不可变 的、重复使用 的,适合使用享元模式来缓存复用。
5.1. ✅ 享元接口:RuleDefinition
public interface RuleDefinition {
void evaluate(String param); // 示例行为
}
5.2. ✅ 具体享元类:ConcreteRuleDefinition
public class ConcreteRuleDefinition implements RuleDefinition {
private final String ruleCode;
private final String description;
public ConcreteRuleDefinition(String ruleCode, String description) {
this.ruleCode = ruleCode;
this.description = description;
}
@Override
public void evaluate(String param) {
System.out.println("执行规则 [" + ruleCode + "] - " + description + ",参数:" + param);
}
public String getRuleCode() {
return ruleCode;
}
public String getDescription() {
return description;
}
}
5.3. ✅ 享元工厂:RuleDefinitionFactory
(由 Spring 管理)
@Component
public class RuleDefinitionFactory {
private final Map<String, RuleDefinition> pool = new ConcurrentHashMap<>();
/**
* 获取共享的规则定义
*/
public RuleDefinition getRule(String ruleCode) {
return pool.computeIfAbsent(ruleCode, this::loadRuleDefinition);
}
/**
* 模拟从数据库或配置中加载规则元数据
*/
private RuleDefinition loadRuleDefinition(String ruleCode) {
// 实际情况应从数据库或配置中心加载
System.out.println("加载规则定义:" + ruleCode);
return new ConcreteRuleDefinition(ruleCode, "规则描述_" + ruleCode);
}
public int getPoolSize() {
return pool.size();
}
}
5.4. ✅ 客户端服务:RiskEngineService
(注入使用)
@Service
public class RiskEngineService {
@Autowired
private RuleDefinitionFactory ruleDefinitionFactory;
public void processRisk(String ruleCode, String inputParam) {
RuleDefinition rule = ruleDefinitionFactory.getRule(ruleCode);
rule.evaluate(inputParam);
}
}
5.5. ✅ 启动类或控制器测试(模拟调用)
@RestController
public class RiskController {
@Autowired
private RiskEngineService riskEngineService;
@GetMapping("/risk/test")
public String test() {
riskEngineService.processRisk("R001", "用户A数据");
riskEngineService.processRisk("R001", "用户B数据");
riskEngineService.processRisk("R002", "用户C数据");
return "风控规则执行完毕";
}
}
5.6. ✅ 运行结果示例
加载规则定义:R001
执行规则 [R001] - 规则描述_R001,参数:用户A数据
执行规则 [R001] - 规则描述_R001,参数:用户B数据
加载规则定义:R002
执行规则 [R002] - 规则描述_R002,参数:用户C数据
可见:R001
只加载一次,后续复用;实现了享元模式在 Spring 项目下的实战落地。
要素 |
说明 |
享元类 |
|
享元工厂 |
|
共享池 |
|
注解注入 |
使用 |
应用场景 |
风控规则元数据复用、规则模板复用、评分模型共享 |
6. 享元设计模式思考
6.1. 享元设计模式与原型设计模式?
享元设计模式(Flyweight)和原型设计模式(Prototype)都是创建相关的设计模式,但它们解决的问题、使用方式和结构完全不同。下面是它们的详细对比:
6.1.1. 🆚 Flyweight vs. Prototype
维度 |
享元模式(Flyweight) |
原型模式(Prototype) |
💡 设计模式类型 |
结构型模式 |
创建型模式 |
🎯 目的 |
通过共享对象来减少内存占用和对象数量 |
通过复制已有对象来创建新对象,避免 new 开销 |
📦 关注点 |
对象复用与共享,分离内部状态与外部状态 |
快速创建新对象(特别是复杂结构) |
🧠 实现机制 |
享元工厂维护共享对象池,通过传入外部状态来复用对象 |
使用 方法或拷贝构造函数复制现有对象 |
📂 状态管理 |
内部状态共享,外部状态由使用方维护 |
完整复制所有状态(深拷贝/浅拷贝) |
📈 适合场景 |
- 大量重复对象,如文字编辑器中的字符对象 |
- 克隆原型对象 |
🧩 示例 |
String Pool、Integer.valueOf、数据库连接池 |
原型注册器、工作流模板复制、前端组件克隆等 |
⚠️ 使用注意点 |
- 内外部状态划分要明确 |
- 深拷贝需注意引用类型对象 |
6.1.2. ✅ 简单总结
- 享元模式 = 节省内存、共享对象:适用于大量对象重复的场景。
- 原型模式 = 快速复制、提升性能:适用于快速创建复杂对象的场景。
6.1.3. 📌 举个类比:
类比 |
描述 |
享元 |
比如一个图书馆的“图书”是共享的,用户借用的是引用,图书本身不复制。 |
原型 |
比如一个表格模板,每次创建新文档都是复制模板,然后修改。 |