设计模式 | 享元模式

290 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 3 天,点击查看活动详情 

Intent 意图

利用共享对象的方式,有效支持大量的细粒度对象

享元(Flyweight)的核心思想很简单:如果一个对象实例一经创建就不可变,那么反复创建相同的实例就没有必要,直接向调用方返回一个共享的实例就行,这样即节省内存,又可以减少创建对象的过程,提高运行速度。

Motivation 动机

  • 通过分离对象的特殊的可变部分(外部状态)和固定重复的不可变的部分(内部状态),让原本程序存放大量的享元对象 的方式,改变为存放一个享元对象在不同场景下的小特殊对象,一个享元大对象会被上千个情境小对象复用,减少程序内存的占用,增强程序性能。

Applicability 适用范围

  • 系统中存在着大量的相似对象,对象中包含可抽取且能在多个对象间共享的重复状态。
  • 细粒度的对象都有比较相近或者相似的外部状态,且内部的状态和外部的环境无关。

Structure 结构

image.png

image.png

Participate 结构成员

结构中各个类、对象所扮演的角色

  • 享元工厂 flyweight factory

    • 创建享元对象
    • 对已有享元对象进行缓存管理
    • 当客户端请求时,已有可用享元对象,则返回该对象;若无,则创建之后返回并持有其缓存
  • 抽象享元对象 flyweight — optional 有些时候可以用具体享元代替。

    • 定义享元对象的抽象类,同时定义享元对象的外部状态和内部状态的接口及实现。
    • 享元中存储的状态被称为 “内在状态”。 传递给享元方法的状态被称为 “外在状态”。
  • 【原始类】具体享元对象 concrete flyweight

    • 包含享元对象中多个可以共享的状态(内部状态),Notice: 这里的内部状态处理需与外部无关,不能出现既改变了内部也改变的外部的方法
  • 【情境类】不可共享享元对象 unshared concrete flyweight

    • 定义享元对象的外部状态或者叫做情境状态,情景与享元对象组合在一起就能表示原始对象的全部状态。
    • 通常情况下, 原始对象的行为会保留在享元类中。 因此调用享元方法必须提供部分外在状态作为参数。 但你也可将行为移动到情景类中, 然后将连入的享元作为单纯的数据对象。

Cooperation 协作

  1. 将需要改写为享元的类成员变量拆分为两个部分:

    • 内在状态: 包含不变的、 可在许多对象中重复使用的数据的成员变量。
    • 外在状态: 包含每个对象各自不同的情景数据的成员变量
  2. 保留类中表示内在状态的成员变量, 并将其属性设置为不可修改。 这些变量仅可在构造函数中获得初始数值。

  3. 找到所有使用外在状态成员变量的方法, 为在方法中所用的每个成员变量新建一个参数, 并使用该参数代替成员变量。

  4. 你可以有选择地创建工厂类来管理享元缓存池, 它负责在新建享元时检查已有的享元。 如果选择使用工厂, 客户端就只能通过工厂来请求享元, 它们需要将享元的内在状态作为参数传递给工厂。

  5. 客户端必须存储和计算外在状态 (情景) 的数值, 因为只有这样才能调用享元对象的方法。 为了使用方便, 外在状态和引用享元的成员变量可以移动到单独的情景类中。

Consequence 后果

  • Good

    • 在存在大量重复相似的系统中,使用享元对象可以节省资源
  • Bad

    • 可能需要牺牲速度来换内存
    • 代码可能变得复杂,刚接手的同事可能不理解为何要拆分不同的实体状态

Implement

image.png

Code

class TreeType {
  name: string = '';
  category: string;
  constructor(category: string) {
    this.category = category;
  }
  setName(name: string) {
    this.name = name;
  }
}

// 杨树
class PopulusTree extends TreeType {
  constructor() {
    super('Populus');
  }
}
// 杉树
class CraibTree extends TreeType {
  constructor() {
    super('Craib');
  }
}

enum TreeCategory {
  DEFAULT = 'default',
  CRABI = 'Craib',
  POPULUS = 'Populus',
}

class Tree {
  x: number = 0;
  y: number = 0;
  type: TreeType;
  
  // 分离可变和不可变的成员对象,不可变对象由工厂输出
  constructor(x: number, y: number, type: TreeCategory) {
    this.x = x;
    this.y = y;
    this.type = TreeFactory.getTreeType(type);
  }
  setPosition(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
  getPosition() {
    console.log(`current ${this.type.category} tree is located at (x:${this.x}, y:${this.y})`);
  }
}
// 造树工厂
class TreeFactory {
  static treeCategoies = {};
  // key method
  static getTreeType(category: string): TreeType {
    switch (category) {
      case TreeCategory.CRABI: {
        if (!this.treeCategoies?.[TreeCategory.CRABI]) {
          console.log('no store, new crabi tree');
          this.treeCategoies[TreeCategory.CRABI] = new CraibTree();
        }
        return this.treeCategoies[TreeCategory.CRABI];
      }
      case TreeCategory.POPULUS: {
        if (!this.treeCategoies?.[TreeCategory.POPULUS]) {
          console.log('no store, new populus tree');
          this.treeCategoies[TreeCategory.POPULUS] = new PopulusTree();
        }
        return this.treeCategoies[TreeCategory.POPULUS];
      }
      default: {
        if (!this.treeCategoies?.[TreeCategory.DEFAULT]) {
          console.log('no store, new default tree');
          this.treeCategoies[TreeCategory.DEFAULT] = new TreeType(TreeCategory.DEFAULT);
        }
        return this.treeCategoies[TreeCategory.DEFAULT];
      }
    }
  }
}

function forest() {
  const treeList: Tree[] = [];

  function plantTree(x, y, treeType) {
    const tree = new Tree(x, y, treeType);
    treeList.push(tree);
  }

  function draw() {
    treeList.forEach((tree) => {
      tree.getPosition();
    });
  }

  plantTree(0, 10, TreeCategory.CRABI);
  plantTree(10, 20, TreeCategory.CRABI);
  plantTree(200, 100, TreeCategory.CRABI);
  plantTree(200, 101, TreeCategory.CRABI);
  plantTree(200, 102, TreeCategory.CRABI);
  plantTree(201, 100, TreeCategory.CRABI);
  draw();
}

forest();

输出结果:

no store, new crabi tree // only init once
current Craib tree is located at (x:0, y:10)
current Craib tree is located at (x:10, y:20)
current Craib tree is located at (x:200, y:100)
current Craib tree is located at (x:200, y:101)
current Craib tree is located at (x:200, y:102)
current Craib tree is located at (x:201, y:100)

Related Pattern 和其他模式直接的关系

  • 组合模式

    • 可以通过使用组合 组合模式和享元模式来节省叶子结点以节省内存

往期文章 index

【设计模式索引 - 持续更新中】 juejin.cn/post/705899…

Reference:

(包括本文参考、或本文的截图及示例出处) refactoringguru.cn/design-patt…