携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 3 天,点击查看活动详情
Intent 意图
利用共享对象的方式,有效支持大量的细粒度对象。
享元(Flyweight)的核心思想很简单:如果一个对象实例一经创建就不可变,那么反复创建相同的实例就没有必要,直接向调用方返回一个共享的实例就行,这样即节省内存,又可以减少创建对象的过程,提高运行速度。
Motivation 动机
- 通过分离对象的特殊的可变部分(外部状态)和固定重复的不可变的部分(内部状态),让原本程序存放大量的享元对象 的方式,改变为存放一个享元对象在不同场景下的小特殊对象,一个享元大对象会被上千个情境小对象复用,减少程序内存的占用,增强程序性能。
Applicability 适用范围
- 系统中存在着大量的相似对象,对象中包含可抽取且能在多个对象间共享的重复状态。
- 细粒度的对象都有比较相近或者相似的外部状态,且内部的状态和外部的环境无关。
Structure 结构
Participate 结构成员
结构中各个类、对象所扮演的角色
-
享元工厂 flyweight factory
- 创建享元对象
- 对已有享元对象进行缓存管理
- 当客户端请求时,已有可用享元对象,则返回该对象;若无,则创建之后返回并持有其缓存
-
抽象享元对象 flyweight — optional 有些时候可以用具体享元代替。
- 定义享元对象的抽象类,同时定义享元对象的外部状态和内部状态的接口及实现。
- 享元中存储的状态被称为 “内在状态”。 传递给享元方法的状态被称为 “外在状态”。
-
【原始类】具体享元对象 concrete flyweight
- 包含享元对象中多个可以共享的状态(内部状态),Notice: 这里的内部状态处理需与外部无关,不能出现既改变了内部也改变的外部的方法
-
【情境类】不可共享享元对象 unshared concrete flyweight
- 定义享元对象的外部状态或者叫做情境状态,情景与享元对象组合在一起就能表示原始对象的全部状态。
- 通常情况下, 原始对象的行为会保留在享元类中。 因此调用享元方法必须提供部分外在状态作为参数。 但你也可将行为移动到情景类中, 然后将连入的享元作为单纯的数据对象。
Cooperation 协作
-
将需要改写为享元的类成员变量拆分为两个部分:
- 内在状态: 包含不变的、 可在许多对象中重复使用的数据的成员变量。
- 外在状态: 包含每个对象各自不同的情景数据的成员变量
-
保留类中表示内在状态的成员变量, 并将其属性设置为不可修改。 这些变量仅可在构造函数中获得初始数值。
-
找到所有使用外在状态成员变量的方法, 为在方法中所用的每个成员变量新建一个参数, 并使用该参数代替成员变量。
-
你可以有选择地创建工厂类来管理享元缓存池, 它负责在新建享元时检查已有的享元。 如果选择使用工厂, 客户端就只能通过工厂来请求享元, 它们需要将享元的内在状态作为参数传递给工厂。
-
客户端必须存储和计算外在状态 (情景) 的数值, 因为只有这样才能调用享元对象的方法。 为了使用方便, 外在状态和引用享元的成员变量可以移动到单独的情景类中。
Consequence 后果
-
Good
- 在存在大量重复相似的系统中,使用享元对象可以节省资源
-
Bad
- 可能需要牺牲速度来换内存
- 代码可能变得复杂,刚接手的同事可能不理解为何要拆分不同的实体状态
Implement
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…