【西瓜带你学设计模式 | 第十四期 - 享元模式】享元模式 —— 内外状态分离与对象共享实现、优缺点与适用场景

文章目录

    • 前言
    • [1. 享元模式是什么?](#1. 享元模式是什么?)
    • [2. 享元模式解决什么问题?](#2. 享元模式解决什么问题?)
    • [3. 核心结构](#3. 核心结构)
      • [3.1 Flyweight(抽象享元角色)](#3.1 Flyweight(抽象享元角色))
      • [3.2 ConcreteFlyweight(具体享元角色)](#3.2 ConcreteFlyweight(具体享元角色))
      • [3.3 FlyweightFactory(享元工厂 / 享元池)](#3.3 FlyweightFactory(享元工厂 / 享元池))
      • [3.4 Client(客户端)](#3.4 Client(客户端))
    • [4. 实现思路](#4. 实现思路)
    • [5. 示例](#5. 示例)
      • [5.1 Flyweight:字符接口(共享对象)](#5.1 Flyweight:字符接口(共享对象))
      • [5.2 ConcreteFlyweight:具体字符(被复用对象)](#5.2 ConcreteFlyweight:具体字符(被复用对象))
      • [5.3 FlyweightFactory:享元工厂/缓存池](#5.3 FlyweightFactory:享元工厂/缓存池)
      • [5.4 Client:客户端调用(外部状态随调用变化)](#5.4 Client:客户端调用(外部状态随调用变化))
    • [6. 优缺点](#6. 优缺点)
      • [6.1 优点](#6.1 优点)
      • [6.2 缺点](#6.2 缺点)
    • [7. 和其他模式怎么区分?](#7. 和其他模式怎么区分?)
      • [7.1 享元 vs 单例](#7.1 享元 vs 单例)
      • [7.2 享元 vs 代理](#7.2 享元 vs 代理)
      • [7.3 享元 vs 组合](#7.3 享元 vs 组合)
    • [8. 适用场景](#8. 适用场景)
    • [9. 总结](#9. 总结)

前言

在很多系统里,我们都会遇到同一个现象:

  • 同类对象会被创建得很多(例如:相同字符、相同颜色、相同提示文本、相同菜单项...)
  • 但它们的大部分状态是相同的,只是少部分状态(比如"位置、上下文、时间、用户态信息")会变化
  • 对象创建和内存占用不断上涨,最后变成性能和成本问题

享元模式(Flyweight Pattern) 想解决的核心就是:

尽可能复用"已经创建过的对象",把重复的东西共享起来,只保留真正变化的部分给外部。


1. 享元模式是什么?

享元模式是一种结构型设计模式,通过"共享对象"的方式减少内存消耗。

关键点是:把对象拆成两类状态:

  • 内部状态(Intrinsic State):不随外部变化,适合共享(通常是固有的、可缓存的)
  • 外部状态(Extrinsic State):随上下文变化,不适合共享(例如:位置、渲染参数、用户信息等)

把相同的对象"尽量少创建",用 Key 找到已有对象复用;变化的部分由外部传入。


2. 享元模式解决什么问题?

  1. 应用里大量重复对象,数量可能非常大
  2. 重复对象中一部分状态相同
  3. 创建这些对象的成本(内存/时间)很高
  4. 能把"对象的**可变部分"**从对象内部抽出去

如果你把所有状态都放在对象里,那就很难复用了------因为每个对象都会"独一无二"。


3. 核心结构

享元模式常见的结构如下:

3.1 Flyweight(抽象享元角色)

定义共享对象需要实现的行为,并且接受外部状态参数

内部状态在享元对象中缓存;外部状态在调用时传入。

3.2 ConcreteFlyweight(具体享元角色)

真正被缓存复用的对象类型。内部通常会存:

  • 内部状态(Intrinsic State)
  • 行为逻辑:使用内部状态 + 外部状态完成工作

3.3 FlyweightFactory(享元工厂 / 享元池)

负责"创建/获取享元":

  • 对某个 Key(例如颜色值、字符值、样式ID)先查缓存
  • 有就直接复用
  • 没有就创建并放入缓存

3.4 Client(客户端)

客户端不直接 new 具体享元,而是:

  • 用外部参数 + 内部Key 去工厂获取享元
  • 然后把外部状态传给享元执行

4. 实现思路

实现时通常为把"可变状态"挪出去:

  1. 抽象一个 Flyweight 接口(方法里带外部状态参数)
  2. 写具体享元,缓存内部状态(只存可共享的部分)
  3. 做一个工厂(Map/缓存池)用 Key 管理享元
  4. 客户端通过工厂拿享元,再传外部状态执行

5. 示例

终端/画布上会画很多字符,但字符本身(内部状态)只有类型不同;位置等(外部状态)每次都不同。

  • 内部状态 :字符本身(char
  • 外部状态 :绘制位置(x, y

5.1 Flyweight:字符接口(共享对象)

java 复制代码
public interface Glyph {
    void draw(int x, int y); // x,y 是外部状态
}

5.2 ConcreteFlyweight:具体字符(被复用对象)

java 复制代码
public class ConcreteGlyph implements Glyph {
    private final char ch; // 内部状态:字符本身(可共享)

    public ConcreteGlyph(char ch) {
        this.ch = ch;
    }

    @Override
    public void draw(int x, int y) {
        System.out.println("绘制字符 '" + ch + "' 到 (" + x + ", " + y + ")");
    }
}

5.3 FlyweightFactory:享元工厂/缓存池

java 复制代码
import java.util.HashMap;
import java.util.Map;

public class GlyphFactory {
    private final Map<Character, Glyph> cache = new HashMap<>();

    public Glyph getGlyph(char ch) {
        // 内部状态的 Key:字符 char
        return cache.computeIfAbsent(ch, k -> new ConcreteGlyph(k));
    }
}

5.4 Client:客户端调用(外部状态随调用变化)

java 复制代码
public class Client {
    public static void main(String[] args) {
        GlyphFactory factory = new GlyphFactory();

        Glyph a = factory.getGlyph('A');
        a.draw(10, 20);

        Glyph a2 = factory.getGlyph('A');
        a2.draw(100, 200);

        Glyph b = factory.getGlyph('B');
        b.draw(30, 40);
    }
}

会发现:

  • 'A' 只会创建一次(后续复用)
  • draw(x,y) 每次都带不同位置(外部状态)

6. 优缺点

6.1 优点

  1. 显著减少对象数量,降低内存占用
  2. 减少创建开销,提升性能
  3. 对于"重复多、共享可能高"的场景非常有效

6.2 缺点

  1. 拆分内部/外部状态增加复杂度
  2. 客户端必须传入外部状态,接口设计要更谨慎
  3. 享元池可能带来管理成本(缓存、过期策略等)
  4. 如果内部状态设计不好,复用率会很低,甚至得不偿失

7. 和其他模式怎么区分?

7.1 享元 vs 单例

  • 单例:保证"一个实例"
  • 享元:保证"同一类/同一 Key 的实例尽量少",可能有多个(按 Key 缓存)

7.2 享元 vs 代理

  • 代理:控制访问/延迟加载/权限等
  • 享元:共享对象本体,减少重复创建与内存占用

7.3 享元 vs 组合

  • 组合:树形结构统一处理
  • 享元:共享对象状态减少资源消耗

8. 适用场景

  • 大量对象可归类,且存在大量可共享部分
  • 内部状态相对稳定,外部状态在运行中变化
  • 典型如:文本编辑/渲染、图形绘制(颜色、字体、样式)、缓存规则对象、棋盘/网格格子等

如果发现对象里"重复字段很多",而"真正每次变化的字段很少",那可以使用享元模式


9. 总结

享元模式通过区分内部状态与外部状态,把大量可共享对象放入享元池复用;客户端从工厂按 Key 获取享元,再把变化的外部状态传入执行,从而显著减少内存与创建开销。

相关推荐
大黄说说2 小时前
Go语言并发编程:Goroutine与Channel构建的CSP模型
java·后端·spring
Flittly2 小时前
【SpringAIAlibaba新手村系列】(12)RAG 检索增强生成技术
java·人工智能·spring boot·spring·ai
葡萄城技术团队2 小时前
Claude Code Buddy 小析:一个非核心功能,如何体现产品的细节完成度
android·java·microsoft
小胖java2 小时前
音乐推荐系统
java·spring boot
2401_827499992 小时前
python核心语法05-模块
java·前端·python
鱼鳞_2 小时前
Java学习笔记_Day23(双列集合)
java·笔记·学习
蜡台2 小时前
Android Studio Gradlew JDK配置
java·gradle·android studio·intellij-idea
yaoxin5211232 小时前
375. Java IO API - 列出目录内容
java·开发语言·python
.豆鲨包2 小时前
【Android】OkHttp的使用及封装
android·java·okhttp