设计模式 · 享元模式(Flyweight Pattern)java

文章目录

    • 前言
    • 一、核心定义
    • 二、标准体系结构图
    • 三、场景推演
    • 四、实战案例
      • [4.1 需求分析](#4.1 需求分析)
      • [4.2 架构图](#4.2 架构图)
        • [4.2.1 普通方式架构图](#4.2.1 普通方式架构图)
        • [4.2.2 享元模式架构图](#4.2.2 享元模式架构图)
      • [4.3 时序图](#4.3 时序图)
        • [4.3.1 普通代码时序图](#4.3.1 普通代码时序图)
        • [4.3.2 享元模式时序图](#4.3.2 享元模式时序图)
      • [4.4 代码分析](#4.4 代码分析)
        • [4.4.1 普通代码](#4.4.1 普通代码)
        • [4.4.2 享元模式代码](#4.4.2 享元模式代码)
    • 总结

前言

在实际开发中,我们经常会遇到一种情况:系统里有大量对象,但这些对象中很多数据是重复的

比如:

复制代码
游戏地图中有 10 万棵树:
每棵树都有坐标、颜色、纹理、树种。

秒杀系统中有很多活动查询:
每次查询都有活动名称、活动描述、开始时间、结束时间、库存信息。

随着业务规模的扩张,这些海量的对象会像无底洞一样吞噬系统的内存,导致频繁的垃圾回收(GC),甚至引发系统崩溃(OOM)

对于这种情况,如何用最少的资源办最多的事?

如果每次都创建一个完整的对象,就像每个学生都要一本完全一样的教材,但学校却给每个人重新打印一遍,这显然会浪费大量资源。

共享元模式就是为了解决这种"重复对象过多,内存浪费严重"的问题。

其核心思想是:

相同的内容共享,不同的外部内容形成。
本文代码:https://github.com/likerhood/CodeDesignWork/tree/main/codedesign10.0-0 和 10.0-1


一、核心定义

享元模式的核心思想非常纯粹:运用共享技术,有效地支持大量细粒度对象的复用

它的定义可以这样理解:

通过共享已经存在的对象,减少大量相似对象的创建,从而降低内存消耗,提高系统访问效率。

更直白地说:

  1. 不要每次都 new 一个完整对象;
  2. 能共享的部分就共享;
  3. 不能共享的部分再单独传入;

这里的思想和原型模式比较接近。

在共享元模式中,最重要的是区分两个状态:

状态 意义 是否共享 举例说明
内在状态 对象中稳定、不变、可复用的数据 可以分享 活动名称、活动描述、树的纹理、树的颜色
外部在状态 每个对象突出、经常变化的数据 不分享 库存数量、树的坐标、当前用户状态

类比在购物节的秒似杀活动中:

  1. 活动ID、活动名称、活动描述、开始时间、结束时间,这些通常是不变的,可以共享。

  2. 库存数量、已售数量、用户下单状态,这些是变化的,不适合共享。

所以享元模式的本质是:完整对象 = 共享对象 + 外部状态


二、标准体系结构图

享元模式的标准体系通常包含以下核心角色:

  1. FlyweightFactory(享元工厂):负责创建和管理享元对象。当客户端请求时,工厂会检查池中是否已有符合要求的对象,如果有则直接返回,没有则创建新对象并放入池中。
  2. Flyweight(抽象享元接口):规定了具体享元类必须实现的方法,通常会接收外部状态作为参数。
  3. ConcreteFlyweight(具体享元类):实现了接口,并为内部状态提供存储空间。

这里的重点在于:

  1. 客户端不直接创建共享对象;
  2. 客户端通过享元工厂获取共享对象;
  3. 享元工厂内部维护一个对象池;
    • 如果对象已经存在,直接复用;
    • 如果对象不存在,再创建并放入对象池。

获取享元对象
管理对象池
Client
+operation()
FlyweightFactory
-Map<String, Flyweight> pool
+getFlyweight(key) : Flyweight
<<interface>>
Flyweight
+operation(extrinsicState)
ConcreteFlyweight
-intrinsicState
+operation(extrinsicState)


三、场景推演

在游戏开发中,通常需要渲染成千上万的树木来构成一片广袤的森林。如果每一棵树都包含完整的品种信息、高分辨率的树皮贴图模型(几十MB大小)以及具体的坐标位置,一百万棵树就会直接撑爆显存。

但仔细推演我们会发现:森林里的树虽然多,但"品种"其实只有寥寥几种(比如橡树、松树、白桦树)。树的品种和贴图模型是永远不变的(内部状态) ,而树在地图上的 X、Y 坐标是每棵树独有的(外部状态)

JAVA 复制代码
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

// 享元对象:树的类型,保存可共享的内部状态
class TreeType {

    // 内部状态:树的品种
    private final String name;

    // 内部状态:树皮贴图
    private final String barkTexture;

    // 内部状态:树的模型数据
    private final String modelData;

    public TreeType(String name, String barkTexture, String modelData) {
        this.name = name;
        this.barkTexture = barkTexture;
        this.modelData = modelData;
    }

    // 外部状态通过参数传入
    public void render(int x, int y) {
        System.out.println(
                "渲染树木:" + name +
                ",坐标:(" + x + ", " + y + ")" +
                ",贴图:" + barkTexture +
                ",模型:" + modelData
        );
    }
}

// 享元工厂:负责创建和复用 TreeType
class TreeFactory {

    private static final Map<String, TreeType> TREE_TYPE_POOL = new HashMap<>();

    public static TreeType getTreeType(String name, String barkTexture, String modelData) {
        String key = name + "_" + barkTexture + "_" + modelData;

        if (!TREE_TYPE_POOL.containsKey(key)) {
            System.out.println("创建新的树类型:" + name);
            TREE_TYPE_POOL.put(key, new TreeType(name, barkTexture, modelData));
        }

        return TREE_TYPE_POOL.get(key);
    }

    public static int getTreeTypeCount() {
        return TREE_TYPE_POOL.size();
    }
}

// 具体树对象:保存每棵树独有的外部状态
class Tree {

    // 外部状态:每棵树的位置不同
    private final int x;
    private final int y;

    // 引用共享的树类型对象
    private final TreeType treeType;

    public Tree(int x, int y, TreeType treeType) {
        this.x = x;
        this.y = y;
        this.treeType = treeType;
    }

    public void render() {
        treeType.render(x, y);
    }
}

// 森林类:管理大量树木
class Forest {

    private final List<Tree> trees = new ArrayList<>();

    public void plantTree(int x, int y, String name, String barkTexture, String modelData) {
        TreeType treeType = TreeFactory.getTreeType(name, barkTexture, modelData);
        Tree tree = new Tree(x, y, treeType);
        trees.add(tree);
    }

    public void render() {
        for (Tree tree : trees) {
            tree.render();
        }
    }

    public int getTreeCount() {
        return trees.size();
    }
}

// 测试类
public class FlyweightTreeDemo {

    public static void main(String[] args) {
        Forest forest = new Forest();

        // 种植很多棵橡树
        forest.plantTree(10, 20, "橡树", "oak_bark_texture.png", "oak_model.obj");
        forest.plantTree(30, 50, "橡树", "oak_bark_texture.png", "oak_model.obj");
        forest.plantTree(80, 120, "橡树", "oak_bark_texture.png", "oak_model.obj");

        // 种植很多棵松树
        forest.plantTree(200, 300, "松树", "pine_bark_texture.png", "pine_model.obj");
        forest.plantTree(250, 360, "松树", "pine_bark_texture.png", "pine_model.obj");

        // 种植白桦树
        forest.plantTree(400, 500, "白桦树", "birch_bark_texture.png", "birch_model.obj");

        forest.render();

        System.out.println("----------------------");
        System.out.println("森林中的树木总数:" + forest.getTreeCount());
        System.out.println("实际创建的树类型数量:" + TreeFactory.getTreeTypeCount());
    }
}

最核心的地方是这句:

java 复制代码
TreeType treeType = TreeFactory.getTreeType(name, barkTexture, modelData);

它表示:

种树时不要每次都创建完整树模型,而是先去享元池里找有没有相同类型的树。如果有,就直接复用。

这些数据不再每棵树都复制一份,而是相同类型的树共享一份。

Tree 对象还是创建了 6 个,但 TreeType 这种重量级、可共享的对象只创建了 3 个。


四、实战案例

4.1 需求分析

秒杀活动查询接口通常会返回活动 ID、活动名称、活动描述、开始时间、结束时间、库存等信息。

普通实现中,每次请求都会重新创建完整的活动对象,活动基础信息和库存信息一起被反复构建。

但在真实秒杀场景中,活动名称、描述、开始/结束时间等属于相对稳定的内部状态,而库存已用数量属于频繁变化的外部状态。

享元模式的核心思路就是:复用稳定对象,只把变化数据放在外部动态补充,从而减少对象创建和内存占用。

4.2 架构图

4.2.1 普通方式架构图
4.2.2 享元模式架构图

4.3 时序图

4.3.1 普通代码时序图

Stock Activity ActivityController ApiTest Stock Activity ActivityController ApiTest queryActivityInfo(10001L) new Activity() setId/setName/setDesc/setTime new Stock(1000, 1) setStock(stock) return Activity

4.3.2 享元模式时序图

Stock RedisUtils Shared Activity ActivityFactory ActivityController ApiTest Stock RedisUtils Shared Activity ActivityFactory ActivityController ApiTest alt [缓存不存在] [缓存存在] queryActivityInfo(10001L) getActivity(10001L) new Activity() activityMap.put(id, activity) return cached Activity getStockUsed() used new Stock(1000, used) setStock(stock) return Activity

4.4 代码分析

4.4.1 普通代码

普通实现的核心问题在于每次请求都会重新创建 Activity,并把活动基础信息、活动时间、库存信息全部硬编码到接口逻辑里。

java 复制代码
public Activity queryActivityInfo(Long id) {
    Activity activity = new Activity();
    activity.setId(10001L);
    activity.setName("图书嗨乐");
    activity.setDesc("图书优惠券分享激励分享活动第二期");
    activity.setStartTime(new Date());
    activity.setStopTime(new Date());
    activity.setStock(new Stock(1000, 1));
    return activity;
}

这种方式简单直接,但当请求量变大时,大量重复对象会被创建。

活动基础信息本身并不会频繁变化,却在每一次查询中重新构造,造成不必要的内存和对象创建成本。

4.4.2 享元模式代码

享元模式版本引入 ActivityFactory,通过 Map<Long, Activity> 缓存活动对象。

活动 ID、名称、描述、时间等稳定信息作为内部状态被共享;库存已用数量作为外部状态,从 RedisUtils 中动态读取后重新设置。

java 复制代码
public class ActivityFactory {

    static Map<Long, Activity> activityMap = new HashMap<>();

    public static Activity getActivity(Long id) {
        Activity activity = activityMap.get(id);
        if (null == activity) {
            activity = new Activity();
            activity.setId(10001L);
            activity.setName("图书嗨乐");
            activity.setDesc("图书优惠券分享激励分享活动第二期");
            activity.setStartTime(new Date());
            activity.setStopTime(new Date());
            activityMap.put(id, activity);
        }
        return activity;
    }
}

public Activity queryActivityInfo(Long id) {
    Activity activity = ActivityFactory.getActivity(id);
    Stock stock = new Stock(1000, redisUtils.getStockUsed());
    activity.setStock(stock);
    return activity;
}

整体来看,享元模式把"不变的活动信息"和"变化的库存信息"拆开处理:前者缓存复用,后者按请求动态补充。这样可以减少重复对象创建,也让业务结构更贴合高并发秒杀查询场景。

总结

享元模式是空间换时间(或共享换空间)的经典体现。通过构建缓存池和状态拆分,它能在高并发、大数据量的场景下,将内存占用成千上万倍地压缩。

虽然它极大提升了性能,但也引入了状态分离的系统复杂度。

在实际应用中(如 Java 的 String 常量池、数据库连接池、线程池),我们需要时刻警惕多线程环境下的线程安全问题,确保享元对象的"内部状态"绝对不可被篡改。

相关推荐
Royzst9 小时前
图书管理案例
java·开发语言
AI大法师9 小时前
从 Adobe 焕新看品牌系统升级:Logo、主色、字体与产品体验如何重新对齐
大数据·人工智能·adobe·设计模式
带刺的坐椅9 小时前
SolonCode v2026.5.21 发布,Web 能看项目,IM 能找队友
java·ai编程·数字员工·soloncode·终端智能体
dunky10 小时前
副本机制与 ISR 设计:为什么 Kafka 这么快又这么可靠
java
夕除10 小时前
spring boot 9
java·mysql·spring
执明wa10 小时前
从 T 到协变逆变
java·开发语言·数据结构
XiYang-DING10 小时前
【Java EE】 TCP—异常情况处理
java·tcp/ip·java-ee
lianghyan10 小时前
List.stream().min
java·开发语言
贵慜_Derek10 小时前
《从零实现 Agent 系统》连载 03|控制循环:感知—决策—行动—反思
人工智能·设计模式·架构