设计模式(十二)结构型:享元模式详解

发布于:2025-07-28 ⋅ 阅读:(12) ⋅ 点赞:(0)

设计模式(十二)结构型:享元模式详解

享元模式(Flyweight Pattern)是 GoF 23 种设计模式中的结构型模式之一,其核心价值在于通过共享大量细粒度对象来有效支持大规模对象的创建与管理,从而显著减少内存占用和系统开销。它适用于系统中存在大量相似对象的场景,通过分离“可变的外部状态”与“不可变的内部状态”,使多个对象可以共享相同的内部状态实例,从而实现资源的高效复用。享元模式是性能优化的关键手段,广泛应用于文本编辑器(字符格式)、图形系统(图标、样式)、游戏开发(粒子系统、NPC 模板)、数据库连接池、线程池等需要管理大量轻量级对象的系统中。

一、详细介绍

享元模式解决的是“对象数量过多导致内存溢出或性能下降”的问题。在某些应用中,可能需要创建成千上万甚至上百万个对象,如文档中的每个字符、地图上的每个图元、游戏中的每个子弹。如果每个对象都独立存储其全部状态,内存消耗将极其巨大。

享元模式的核心思想是:“共享”而非“重复创建”。它将对象的状态分为两类:

  • 内部状态(Intrinsic State):存储在享元对象内部,不随环境变化,可以被多个对象共享。例如,字符的字体、字号、颜色;图形的形状、线型;游戏单位的模型、攻击力等。
  • 外部状态(Extrinsic State):依赖于上下文,随环境变化,不能被共享,必须由客户端在运行时传入。例如,字符在文档中的位置;图形的坐标;游戏单位的当前血量、位置等。

通过将内部状态提取出来并共享,而将外部状态由客户端管理并在调用时传入,享元模式实现了对象的“池化”管理。系统不再为每个逻辑对象创建一个完整实例,而是从“享元池”中获取共享的内部状态对象,并结合外部状态完成操作。

该模式包含以下核心角色:

  • Flyweight(享元接口):定义享元对象对外提供的操作接口,通常包含一个方法接收外部状态作为参数。
  • ConcreteFlyweight(具体享元类):实现 Flyweight 接口,存储内部状态。它是可共享的,通常设计为不可变对象(immutable)以保证线程安全。
  • UnsharedConcreteFlyweight(非共享具体享元):某些情况下,并非所有对象都适合共享,此类对象不参与共享机制,但实现相同接口。
  • FlyweightFactory(享元工厂):负责管理享元对象的创建与共享。它通常使用集合(如 Map)缓存已创建的享元对象,确保相同内部状态只创建一次。客户端通过工厂获取享元实例。
  • Client(客户端):维护外部状态,并在需要时从工厂获取享元对象,然后将外部状态传递给享元对象的方法以完成操作。

享元模式的关键优势:

  • 节省内存:大量对象共享内部状态,显著减少实例数量。
  • 提升性能:减少对象创建与垃圾回收开销。
  • 支持大规模系统:使处理海量对象成为可能。

与“对象池模式”相比,享元关注状态分离与共享,对象池关注对象生命周期管理;享元通常用于不可变状态的共享,而对象池用于可重用对象(如数据库连接)的复用。

二、享元模式的UML表示

以下是享元模式的标准 UML 类图:

implements
implements
creates and manages
uses
uses
«interface»
Flyweight
+operation(extrinsicState: Object)
ConcreteFlyweight
-intrinsicState: String
+operation(extrinsicState: Object)
UnsharedConcreteFlyweight
+operation(extrinsicState: Object)
FlyweightFactory
-flyweights: Map<String, Flyweight>
+getFlyweight(key: String)
Client
-extrinsicState: Object
+doWork()

图解说明

  • Flyweight 接口定义操作方法,接收外部状态。
  • ConcreteFlyweight 存储内部状态(如 intrinsicState),实现 operation()
  • FlyweightFactory 使用 Map 缓存享元对象,getFlyweight(key) 确保唯一实例。
  • Client 持有外部状态,在调用时传入享元对象。
  • 客户端通过工厂获取享元,结合外部状态完成操作。

三、一个简单的Java程序实例及其UML图

以下是一个文本编辑器中字符渲染的示例,展示如何使用享元模式共享字符的字体、颜色等格式信息。

Java 程序实例
import java.util.HashMap;
import java.util.Map;

// 享元接口:字符格式
interface CharacterFormat {
    void display(String content, int x, int y);
}

// 具体享元类:共享的字符格式(内部状态)
class SharedCharacterFormat implements CharacterFormat {
    private final String font;
    private final int size;
    private final String color;

    // 构造时初始化内部状态,且不可变
    public SharedCharacterFormat(String font, int size, String color) {
        this.font = font;
        this.size = size;
        this.color = color;
    }

    // 外部状态(content, x, y)由客户端传入
    @Override
    public void display(String content, int x, int y) {
        System.out.printf("🖋️ Render '%s' at (%d,%d) | Font: %s, Size: %d, Color: %s\n",
                content, x, y, font, size, color);
    }

    // 用于工厂中作为缓存的 key
    public String getKey() {
        return font + "-" + size + "-" + color;
    }
}

// 享元工厂:管理字符格式的共享实例
class CharacterFormatFactory {
    private static final Map<String, CharacterFormat> pool = new HashMap<>();

    public static CharacterFormat getFormat(String font, int size, String color) {
        String key = font + "-" + size + "-" + color;
        return pool.computeIfAbsent(key, k -> new SharedCharacterFormat(font, size, color));
    }

    public static int getPoolSize() {
        return pool.size();
    }
}

// 客户端:文本编辑器
public class FlyweightPatternDemo {
    public static void main(String[] args) {
        System.out.println("📝 文本编辑器使用享元模式渲染字符\n");

        // 模拟文档中多个字符的渲染
        // 字符 'H' 在位置 (0,0)
        CharacterFormat format1 = CharacterFormatFactory.getFormat("Arial", 12, "black");
        format1.display("H", 0, 0);

        // 字符 'e' 在位置 (10,0),使用相同格式
        CharacterFormat format2 = CharacterFormatFactory.getFormat("Arial", 12, "black");
        format2.display("e", 10, 0);

        // 字符 'l' 在位置 (20,0),使用相同格式
        CharacterFormat format3 = CharacterFormatFactory.getFormat("Arial", 12, "black");
        format3.display("l", 20, 0);

        // 字符 'W' 在位置 (0,20),使用不同格式
        CharacterFormat format4 = CharacterFormatFactory.getFormat("Times New Roman", 14, "blue");
        format4.display("W", 0, 20);

        // 字符 'o' 在位置 (30,0),使用原始格式
        CharacterFormat format5 = CharacterFormatFactory.getFormat("Arial", 12, "black");
        format5.display("o", 30, 0);

        // 验证共享:format1, format2, format3, format5 应为同一实例
        System.out.println("\n✅ 享元池中唯一格式实例数量: " + CharacterFormatFactory.getPoolSize());
        System.out.println("🔍 format1 == format2: " + (format1 == format2));
        System.out.println("🔍 format2 == format5: " + (format2 == format5));
        System.out.println("🔍 format1 == format4: " + (format1 == format4));

        System.out.println("\n💡 结论:相同格式被共享,不同格式独立创建。");
    }
}
实例对应的UML图(简化版)
implements
creates and manages
uses
uses
«interface»
CharacterFormat
+display(content: String, x: int, y: int)
SharedCharacterFormat
-font: String
-size: int
-color: String
+display(content: String, x: int, y: int)
+getKey()
CharacterFormatFactory
-pool: Map<String, CharacterFormat>
+getFormat(font: String, size: int, color: String)
+getPoolSize()
Client
+main(args: String[])

运行说明

  • SharedCharacterFormat 存储字体、大小、颜色等内部状态,不可变。
  • CharacterFormatFactory 使用 Map 缓存享元对象,确保相同格式只创建一次。
  • 客户端调用 getFormat() 获取享元,并传入字符内容和坐标(外部状态)进行渲染。
  • 输出显示:多个字符共享同一格式实例,享元池大小远小于字符数量。

四、总结

特性 说明
核心目的 共享细粒度对象,减少内存占用
实现机制 分离内部状态(共享)与外部状态(客户端传入)
优点 显著节省内存、减少对象创建开销、支持大规模对象系统
缺点 增加系统复杂性、需仔细划分内外状态、可能引入线程安全问题
适用场景 大量相似对象(字符、图形、粒子)、状态可分离、内部状态为主
不适用场景 对象数量少、状态难以分离、外部状态占比大、需要频繁修改内部状态

享元模式使用建议

  • 内部状态应设计为不可变对象,确保线程安全。
  • 享元工厂通常使用单例模式实现。
  • 外部状态应尽量轻量,避免成为新的性能瓶颈。
  • 在 Java 中,String 常量池、Integer.valueOf() 缓存(-128~127)是享元思想的体现。

架构师洞见:
享元模式是“空间换时间”与“状态管理”的高级应用。在现代系统中,其思想已扩展到缓存设计数据压缩虚拟化技术中。例如,在前端框架中,React 的 key 机制和虚拟 DOM diff 算法隐含了享元思想——复用 DOM 节点而非重建;在游戏引擎中,ECS(实体-组件-系统)架构通过共享组件数据实现高效渲染;在大数据处理中,列式存储通过共享重复值实现压缩。

未来趋势是:享元模式将与持久化数据结构结合,在函数式编程中实现高效共享;在AI 推理中,模型权重作为“内部状态”被多个推理请求共享;在元宇宙3D 渲染中,海量图元和材质将依赖享元机制实现流畅交互。

掌握享元模式,有助于设计出高效、可扩展、资源友好的系统。作为架构师,应在识别到“海量相似对象”时,主动引入享元模式进行优化。享元不仅是模式,更是资源管理的哲学——它教会我们:真正的效率,来自于对“共享”与“分离”的深刻洞察。


网站公告

今日签到

点亮在社区的每一天
去签到