什么是享元模式?

享元模式(Flyweight Pattern)是一种结构型设计模式,它通过共享多个对象中相同的部分(内部状态),来减少内存占用和对象数量,从而提高系统性能。

其核心思想是:将对象的状态分为内部状态(可共享、不随环境变化的部分)和外部状态(不可共享、随环境变化的部分)。通过共享内部状态,复用已创建的对象,仅在必要时通过外部状态区分不同场景。

享元模式的核心角色

  1. 享元工厂(Flyweight Factory):负责创建和管理享元对象,确保相同内部状态的对象只被创建一次(复用已有对象)。
  2. 抽象享元(Flyweight):定义享元对象的接口,声明接收外部状态并执行操作的方法。
  3. 具体享元(ConcreteFlyweight):实现抽象享元接口,存储内部状态(可共享),通过外部状态处理具体逻辑。
  4. 非享元(UnsharedConcreteFlyweight):部分场景中,无需共享的对象可作为非享元存在(可选)。

JavaScript 实现享元模式示例:文字渲染系统

以下以 “网页文字渲染” 为例,实现享元模式。场景是:网页中存在大量相同样式(字体、大小、颜色)的文字,这些文字的样式(内部状态)可共享,而位置(x、y 坐标,外部状态)不同。通过享元模式复用样式对象,减少内存消耗。

代码实现
// -------------- 1. 抽象享元(Flyweight):文字样式接口 --------------
class TextStyle {
  // 渲染文字(接收外部状态:位置)
  render(content, x, y) {
    throw new Error("子类必须实现 render 方法");
  }
}

// -------------- 2. 具体享元(ConcreteFlyweight):具体文字样式 --------------
class ConcreteTextStyle extends TextStyle {
  /**
   * 构造函数:存储内部状态(可共享的样式)
   * @param {string} font - 字体
   * @param {number} size - 字号
   * @param {string} color - 颜色
   */
  constructor(font, size, color) {
    super();
    this.font = font; // 内部状态(共享)
    this.size = size; // 内部状态(共享)
    this.color = color; // 内部状态(共享)
    // 记录创建的享元对象(用于演示复用)
    this.id = `style_${Date.now().toString().slice(-4)}`;
    console.log(`创建新样式对象:${this.id}(${font}, ${size}px, ${color})`);
  }

  /**
   * 渲染文字(结合外部状态:位置)
   * @param {string} content - 文字内容
   * @param {number} x - x坐标(外部状态)
   * @param {number} y - y坐标(外部状态)
   */
  render(content, x, y) {
    console.log(
      `[${this.id}] 渲染文字:"${content}",样式:${this.font} ${this.size}px ${this.color},位置:(${x}, ${y})`
    );
  }
}

// -------------- 3. 享元工厂(Flyweight Factory):管理样式对象 --------------
class TextStyleFactory {
  constructor() {
    this.styles = {}; // 缓存享元对象(键:样式唯一标识,值:样式对象)
  }

  /**
   * 获取样式对象(复用已有对象,不存在则创建)
   * @param {string} font - 字体
   * @param {number} size - 字号
   * @param {string} color - 颜色
   * @returns {ConcreteTextStyle} 样式享元对象
   */
  getStyle(font, size, color) {
    // 生成唯一键(基于内部状态)
    const key = `${font}_${size}_${color}`;
    // 若已存在则复用,否则创建新对象
    if (!this.styles[key]) {
      this.styles[key] = new ConcreteTextStyle(font, size, color);
    }
    return this.styles[key];
  }

  // 查看缓存的享元对象数量(用于演示)
  getStyleCount() {
    return Object.keys(this.styles).length;
  }
}

// -------------- 4. 客户端使用 --------------
// 创建享元工厂
const styleFactory = new TextStyleFactory();

// 渲染多个文字(存在重复样式)
function renderTexts() {
  // 场景1:3个"标题"(相同样式:Arial, 24px, 黑色)
  const titleStyle = styleFactory.getStyle("Arial", 24, "black");
  titleStyle.render("标题1", 10, 20);
  titleStyle.render("标题2", 10, 60);
  titleStyle.render("标题3", 10, 100);

  // 场景2:2个"正文"(相同样式:SimSun, 16px, 灰色)
  const bodyStyle1 = styleFactory.getStyle("SimSun", 16, "gray");
  bodyStyle1.render("正文A", 50, 200);
  bodyStyle1.render("正文B", 50, 230);

  // 场景3:2个"注释"(相同样式:SimSun, 12px, 灰色)
  const noteStyle = styleFactory.getStyle("SimSun", 12, "gray");
  noteStyle.render("注释1", 50, 300);
  noteStyle.render("注释2", 50, 320);

  // 场景4:1个"强调文本"(新样式:Arial, 16px, 红色)
  const highlightStyle = styleFactory.getStyle("Arial", 16, "red");
  highlightStyle.render("重点内容", 50, 400);
}

// 执行渲染
renderTexts();

// 输出结果说明:
// 1. 仅创建4个样式对象(标题、正文、注释、强调文本),尽管渲染了8个文字。
// 2. 相同样式的文字复用同一个享元对象(如3个标题共用 titleStyle)。

console.log("\n缓存的样式对象数量:", styleFactory.getStyleCount()); // 输出:4

代码说明

  1. 抽象享元(TextStyle):定义 render 方法,声明文字渲染的接口,需接收外部状态(xy 坐标)。

  2. 具体享元(ConcreteTextStyle)

    • 存储内部状态:font(字体)、size(字号)、color(颜色),这些属性在相同样式的文字中可共享。
    • 实现 render 方法:结合外部状态(xy)渲染文字,展示样式和位置信息。
  3. 享元工厂(TextStyleFactory)

    • 核心逻辑是 getStyle 方法:通过内部状态(fontsizecolor)生成唯一键,若该键对应的样式已存在则复用,否则创建新对象并缓存。
    • 确保相同样式的文字只创建一个享元对象,减少内存占用(示例中 8 个文字仅创建 4 个样式对象)。
  4. 内部状态与外部状态

    • 内部状态:fontsizecolor(可共享,不随位置变化)。
    • 外部状态:xy(不可共享,每个文字位置不同,通过 render 方法传入)。

享元模式的优势

  • 减少对象数量:通过共享内部状态,大幅减少创建的对象数量(如示例中从 8 个减少到 4 个),降低内存消耗。
  • 提高性能:减少对象创建和垃圾回收的开销,尤其适合大量相似对象的场景(如游戏中的粒子、网页中的文字 / 图标)。
  • 分离可变与不可变:将不变的内部状态与可变的外部状态分离,使系统更清晰、易于维护。

常见应用场景

  • 大量相似对象场景:如游戏中的士兵、子弹、粒子效果(共享模型、材质等内部状态,位置、方向等为外部状态)。
  • 资源复用:如网页中的图标(相同图标复用同一个图片对象,位置为外部状态)、字体缓存。
  • 池化技术:如数据库连接池、线程池(共享连接 / 线程对象,请求参数为外部状态)。

注意事项

  • 状态划分:需明确区分内部状态(可共享)和外部状态(不可共享),否则可能因状态耦合导致共享失败。
  • 工厂复杂度:享元工厂需管理缓存和键生成,可能增加代码复杂度,需权衡是否值得优化(仅在对象数量极多且相似时使用)。

享元模式的核心是 “共享相似对象,分离内外状态”,通过复用减少资源消耗,是性能优化的重要模式,尤其适合处理大量重复或相似对象的场景。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐