文章中如果有图看不到,可以点这里去 csdn 看看。从那边导过来的,文章太多,没法一篇篇修改好。

深入浅出设计模式【十一、享元模式】

一、享元模式介绍

在软件系统中,有时需要创建大量细粒度的对象(例如,文档中的每一个字符、棋盘上的每一个棋子、地图上的每一棵树)。如果这些对象除了少数几个参数外其余都相同,那么大量创建它们会导致高昂的内存开销和性能损耗。

享元模式通过区分对象的“内部状态”和“外部状态”来解决这个问题。内部状态是对象中可以共享的、不变的部分,它独立于对象的场景。外部状态是对象中随场景变化而变化的部分,不能共享。享元模式将内部状态分离出来进行共享,而外部状态由客户端在使用时提供。这样,只需要一个共享对象(享元)就可以代表所有具有相同内部状态的对象,极大地减少了所需对象的数量。

二、核心概念与意图

  1. 核心概念

    • 享元 (Flyweight): 描述一个接口,通过这个接口享元可以接受并作用于外部状态。
    • 具体享元 (Concrete Flyweight): 实现享元接口,并为内部状态增加存储空间。它必须是可共享的对象,其内部状态必须与场景无关。
    • 非共享具体享元 (Unshared Concrete Flyweight): 并非所有的享元子类都需要被共享。不能共享的享元通常将外部状态作为内部状态存储(但这并非强制)。
    • 享元工厂 (Flyweight Factory): 创建并管理享元对象。它确保合理地共享享元:当用户请求一个享元时,工厂提供一个已创建的实例(如果存在),或者创建一个新的实例(如果不存在)。
    • 客户端 (Client): 维持一个对享元的引用;计算或存储一个或多个外部状态。
  2. 意图

    • 运用共享技术有效地支持大量细粒度的对象
    • 通过共享相同的内在状态,避免大量非常相似类的开销
    • 减少内存占用,提升性能

三、适用场景剖析

享元模式在以下场景中非常有效:

  1. 一个应用程序使用了大量的对象: 这会导致很大的存储开销。
  2. 对象的大多数状态都可以变为外部状态: 即对象的大部分状态可以从其场景中分离出来。
  3. 剥离外部状态后,可以用相对较少的共享对象取代大量对象
  4. 应用程序不依赖于对象标识: 由于享元对象可以被共享,因此从概念上讲,它不再是同一个对象。客户端不能对享元对象做身份测试(如 == 比较),这通常不会成为问题,因为享元对象通常封装的是原始数据(如字符、数字)。

典型例子

  • 文本编辑器: 每个字符都是一个对象,其内部状态是字符代码(如 ‘a’),外部状态是它的位置(行、列)和格式(字体、颜色)。成千上万个 ‘a’ 字符共享同一个享元对象。
  • 图形应用: 大量相同的树、石头、士兵等。内部状态是纹理、模型,外部状态是位置、大小。
  • 游戏开发: 大量相同或相似的游戏实体。
  • 数据库连接池、线程池: 在某种意义上,池化技术是享元模式的一种变体和应用。

四、UML 类图解析(Mermaid)

以下UML类图清晰地展示了享元模式的结构和角色间的关系:

Client
-extrinsicState
FlyweightFactory
-pool: Map<String, Flyweight>
+getFlyweight(key)
«interface»
Flyweight
+operation(extrinsicState)
ConcreteFlyweight
-intrinsicState
+operation(extrinsicState)
UnsharedConcreteFlyweight
-allState
+operation(extrinsicState)
  • Flyweight (享元接口): 声明一个接口,享元对象通过该接口可以接受并作用于外部状态 (extrinsicState)。
  • ConcreteFlyweight (具体享元)
    • 实现享元接口。
    • 包含内部状态 (intrinsicState),该状态必须是可共享的,且独立于其场景。
    • operation(extrinsicState) 方法的实现必须使用传入的外部状态来执行逻辑。
  • UnsharedConcreteFlyweight (非共享具体享元): 并非所有的享元子类都需要被共享。它可能直接存储所有状态,但仍然通过统一的接口与客户端交互。
  • FlyweightFactory (享元工厂)
    • 核心管理角色。通常维护一个对象池 (pool: Map<String, Flyweight>)。
    • 键 (key) 通常代表内部状态。
    • 当客户端请求一个享元时,工厂检查池中是否存在具有相同内部状态的享元。如果存在,则返回它;如果不存在,则创建一个新的享元,将其放入池中,然后返回它。
  • Client (客户端)
    • 负责计算或存储外部状态 (extrinsicState)。
    • FlyweightFactory 获取 Flyweight 对象。
    • 在调用享元对象的操作时,将外部状态作为参数传入。

五、各种实现方式及其优缺点

享元模式的实现关键在于如何管理内部状态和外部状态。

1. 标准实现(接口+工厂+对象池)

即上述UML所描述的方式,这是最经典和推荐的方式。

// 1. Flyweight Interface
public interface Shape {
    void draw(int x, int y, String color); // extrinsicState: x, y, color
}

// 2. Concrete Flyweight (Intrinsic State: shapeType)
public class Circle implements Shape {
    private String type; // Intrinsic State (shared)

    public Circle(String type) {
        this.type = type;
    }

    @Override
    public void draw(int x, int y, String color) { // extrinsicState passed in
        System.out.println("Drawing a " + type + " circle at (" + x + ", " + y + ") with color " + color);
    }
}

// 3. Flyweight Factory
import java.util.HashMap;
import java.util.Map;

public class ShapeFactory {
    private static final Map<String, Shape> circleMap = new HashMap<>();

    public static Shape getCircle(String type) {
        Shape circle = circleMap.get(type);

        if (circle == null) {
            // Create a new one and put it in the pool
            circle = new Circle(type);
            circleMap.put(type, circle);
            System.out.println("Creating circle of type: " + type);
        }
        return circle;
    }
}

// 4. Client
public class Client {
    private static final String[] colors = {"Red", "Green", "Blue", "White", "Black"};
    private static final String[] types = {"Small", "Medium", "Large"};

    public static void main(String[] args) {
        for (int i = 0; i < 20; ++i) {
            // Randomly get a type (intrinsic state key)
            String type = getRandomType();
            // Get the flyweight from Factory
            Shape circle = ShapeFactory.getCircle(type);
            // Use the flyweight, passing extrinsic state
            circle.draw(getRandomX(), getRandomY(), getRandomColor());
        }
    }
    // ... helper methods getRandomType, getRandomX, getRandomY, getRandomColor
}
  • 优点
    • 极大减少内存消耗: 内存中仅存储不同内部状态的对象各一份。
    • 可能提升性能: 减少了对象创建和垃圾回收的开销。
  • 缺点
    • 增加系统复杂性: 需要分离内部状态和外部状态,这使得逻辑变得更加复杂。
    • 可能引入线程安全问题: 享元工厂的池通常是全局的,在多线程环境下需要保证其线程安全。上面的简单实现不是线程安全的。
    • 牺牲查询效率来换取空间: 工厂需要管理一个池,并执行查询操作。

2. 内部状态不可变

这是享元模式的一个关键约束。共享的享元对象其内部状态必须是不可变的。否则,一个客户端的修改会影响所有共享该享元的客户端,导致难以预料的错误。

  • 实现: 将享元对象的内部状态字段声明为 final,并通过构造函数初始化,不提供setter方法。

六、最佳实践

  1. 确保内部状态不可变: 这是享元模式正确运行的基石。内部状态必须在对象创建时初始化,并且在对象的生命周期内永不改变。
  2. 使用工厂模式管理享元: 享元的创建和获取必须通过一个集中的工厂来控制,以确保共享的正确性。
  3. 谨慎处理线程安全: 享元工厂中的对象池通常是共享资源。在多线程应用中,必须使用同步机制(如 ConcurrentHashMap)来保证线程安全。
  4. 与对象池模式区分
    • 享元模式目的是减少内存占用,通过共享对象的不可变部分(内部状态)来实现。对象一旦创建,通常不会销毁。
    • 对象池模式目的是减少对象创建和销毁的开销(如数据库连接、线程)。池中的对象是完整的、可变的,客户从池中借出对象,使用完毕后归还,这些对象的状态会被重置。
  5. 仅在必要时使用: 享元模式引入了额外的复杂性。只有在确实存在大量相似对象并导致性能问题时才应考虑使用。不要过早优化。

七、在开发中的演变和应用

享元模式的思想在现代开发中演变为更广泛的概念和应用:

  1. 缓存 (Caching): 享元模式本质上是一种对象级别的缓存策略。它将创建成本高或内存占用大的对象缓存起来复用。现代缓存框架(如Ehcache, Redis)可以看作是分布式、通用化的享元模式实现。
  2. 池化技术 (Pooling): 数据库连接池、线程池是享元思想的延伸。它们共享和复用的是昂贵的资源(连接、线程),而不是简单的对象内部状态。
  3. 内容交付网络 (CDN): 在架构层面,CDN通过在全球边缘节点缓存(共享)静态资源(图片、视频),使用户可以从最近的位置获取,这可以看作是一种宏观的享元模式,外部状态是用户的地理位置。
  4. 驻留 (Intern): 字符串驻留是享元模式最直接的应用。

八、真实开发案例(Java语言内部、知名开源框架、工具)

  1. Java String Pool (字符串常量池)

    • 这是享元模式在Java语言中最经典、最成功的应用。
    • 内部状态: 字符串的字面值(如 "hello")。
    • 享元工厂: JVM中的字符串常量池。
    • 行为: 当使用双引号(String s = "hello")创建字符串时,JVM会首先在池中查找是否存在相同值的字符串。如果找到,则返回该引用;如果未找到,则将其放入池中并返回新引用。
    • 这解释了为什么 String s1 = "hello"; String s2 = "hello";s1 == s2true
  2. Java Integer Cache

    • Java对 Integer 对象也进行了缓存(享元)。
    Integer i1 = 127; // Auto-boxing, gets cached instance
    Integer i2 = 127;
    System.out.println(i1 == i2); // true
    
    Integer i3 = 128;
    Integer i4 = 128;
    System.out.println(i3 == i4); // false (outside default cache range of -128 to 127)
    
    • JVM默认缓存了 -128127 之间的 Integer 对象。通过 valueOf(int) 方法(自动装箱调用)会返回缓存的对象。
  3. Apache Commons Pool 2

    • 虽然它更偏向于对象池模式,但其核心思想与享元模式一脉相承——通过复用对象来提升性能。它提供了一个强大的通用对象池化框架,用于管理如数据库连接、Socket连接等昂贵对象的生命周期。
  4. GUI 工具包中的字型 (Font) 和图标 (Icon) 对象

    • 在Swing等GUI框架中,相同的字体或图标(内部状态)在应用程序中通常只存在一份。控件(如按钮、标签)在绘制时,将自己的位置和文本(外部状态)传递给字体和图标对象进行渲染。
  5. 日志框架

    • 每个 Logger 实例通常按名称(如类名)获取。对同一个名称的 Logger 的获取总是返回同一个实例。这可以看作是一种享元模式,Logger的名称是内部状态。

九、总结

方面总结
模式类型结构型设计模式
核心意图运用共享技术有效地支持大量细粒度的对象,减少内存占用。
关键角色享元(Flyweight), 具体享元(ConcreteFlyweight), 享元工厂(FlyweightFactory), 客户端(Client)
核心概念分离状态内部状态 (intrinsic, 共享, 不可变) vs. 外部状态 (extrinsic, 非共享, 由客户端传入)。
主要优点1. 极大减少内存中对象的数量
2. 可能提升性能(减少GC,减少创建开销)。
主要缺点1. 增加系统复杂性: 需要分离内外状态,逻辑变复杂。
2. 牺牲时间换空间: 工厂查询需要时间。
3. 线程安全问题: 工厂需要线程安全。
适用场景系统中有大量相似对象,且1) 大部分状态可外部化,2) 删除外部状态后可用较少的共享对象替代。
最佳实践内部状态必须不可变;使用工厂集中管理;注意线程安全;与对象池模式区分。
关系与对比vs. 对象池: 享元共享不可变内部状态;对象池共享完整的可变对象(借与还)。
vs. 单例: 享元是“多例”的,工厂根据不同的内部状态返回不同的实例;单例是“单例”的,全局只有一个实例。
现代应用缓存技术池化技术的理论基础之一。
真实案例Java String Pool (典范),Integer CacheApache Commons PoolGUI 资源管理

享元模式是一种“以时间换空间”的经典模式,是优化性能、减少资源消耗的强大工具。它在处理大量重复对象的场景下效果显著。然而,其引入的复杂性也要求开发者在应用时必须仔细权衡,确保真正符合使用场景。理解并正确应用享元模式,是处理高性能、高并发、低内存占用系统设计的关键技能之一。

posted @ 2025-08-29 13:17  NeoLshu  阅读(7)  评论(0)    收藏  举报  来源