概念

享元模式(Flyweight Pattern)是一种结构型设计模式,它旨在通过共享对象来最小化内存使用和提高性能


享元模式通常涉及两个核心角色:享元(Flyweight)和享元工厂(Flyweight Factory)

  • 享元(Flyweight)

    表示一个可共享的对象,包含了内部状态和外部状态

    内部状态是对象共享的部分,它存储在享元对象内部,而外部状态是对象特有的部分,它存储在享元对象外部,并在需要时通过参数传递给享元对象

  • 享元工厂(Flyweight Factory)

    用于创建和管理享元对象

    享元工厂通常包含一个享元对象的池子(或者缓存),用于存储和复用已创建的享元对象,并在需要时返回给客户端


享元模式的核心思想是通过共享对象来最小化内存使用和提高性能

当有多个相似的对象需要创建时,享元模式可以将其中的一部分共享出来,从而节省内存空间

这种模式的优点在于,可以减少对象的创建数量,提高系统的性能和可扩展性

享元模式适用于以下情况:

  • 当需要创建大量相似对象,并且这些对象之间有一些共同的部分时,可以使用享元模式
  • 当希望减少对象的创建数量,提高系统性能和可扩展性时,享元模式也是一个很好的选择


举个简单的例子,考虑一个文本编辑器中的字符对象

文本编辑器可能需要创建大量的字符对象来表示文本内容,而很多字符对象可能是相同的(如空格、换行符等)

享元模式可以将其中的一些字符对象共享出来,从而节省内存空间,提高系统性能


实现条件

  1. 存在大量相似对象

    享元模式适用于存在大量相似对象,并且这些对象之间有一些共享的状态或者行为的情况

  2. 需要节省内存空间

    享元模式适用于需要节省内存空间的情况,因为享元模式可以将对象的内部状态和外部状态分离,从而避免了创建大量相似对象所带来的内存浪费

  3. 外部状态相对固定

    享元模式适用于外部状态相对固定的情况,因为享元模式将对象的内部状态和外部状态分离,因此外部状态需要相对稳定

  4. 需要对对象进行复用

    享元模式适用于需要对对象进行复用的情况,因为享元模式通过共享已有的对象来避免创建新的对象,从而提高了对象的复用性

  5. 需要降低系统的复杂度

    享元模式适用于需要降低系统的复杂度的情况,因为享元模式可以将对象的内部状态和外部状态分离,从而降低了对象的复杂度


优点

  1. 减少内存使用

    享元模式可以有效减少相似对象的内存使用,通过共享相同的对象实例来节省内存空间

  2. 提高性能

    由于减少了对象的数量,享元模式可以提高系统的性能,减少了对象的创建和销毁次数,从而提高了系统的响应速度。

  3. 对象复用

    享元模式可以实现对象的复用,通过共享已有的对象实例来避免创建新的对象,从而提高了对象的复用性

  4. 简化对象管理

    享元模式可以简化对象的管理,因为所有共享的对象实例都由工厂类来管理,减少了对象的创建和销毁的逻辑

  5. 分离内部状态和外部状态

    享元模式将对象的内部状态和外部状态分离,使得可以共享内部状态,而外部状态可以独立变化,提高了系统的灵活性


缺点

  1. 可能引起线程安全问题

    如果多个线程同时访问享元对象,并且对外部状态进行修改,可能会引起线程安全问题,需要额外的同步措施来保证线程安全

  2. 增加系统复杂度

    享元模式需要对对象的内部状态和外部状态进行分离,并且需要维护一个共享池,可能会增加系统的复杂度

  3. 可能导致代码混乱

    如果对象的内部状态和外部状态没有良好地分离,可能会导致代码混乱,降低了系统的可维护性

  4. 不适用于所有情况

    享元模式适用于存在大量相似对象,并且需要节省内存空间的情况,但并不适用于所有情况,特别是对象的外部状态变化频繁的情况


实现方式

创建基础的类跟工厂类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* 表示一个字符及其相关操作的类。
*/
class Character {
/**
* 构造一个字符对象。
* @param {string} char 一个长度为1的字符串。
* @throws {Error} 如果输入不是一个长度为1的字符串,抛出错误。
*/
constructor(char) {
if (typeof char !== 'string' || char.length !== 1) {
throw new Error('Character must be a single character string.')
}
this.char = char
}

/**
* 在指定位置显示字符。
* @param {number[]} position 一个包含两个数字的数组,表示位置。
* @throws {Error} 如果位置不是一个包含两个数字的数组,抛出错误。
*/
display(position) {
if (!Array.isArray(position) || position.length !== 2) {
throw new Error('Position must be an array of two numbers.')
}
console.log(`Character '${this.char}' displayed at position (${position[0]}, ${position[1]})`)
}
}

/**
* 用于创建和管理字符对象的工厂类。
*/
class CharacterFactory {
#characters = {} // 用于存储已创建的字符对象的私有属性

/**
* 获取一个字符对象,如果不存在则创建。
* @param {string} char 一个长度为1的字符串,表示要获取的字符。
* @return {Character} 返回对应的字符对象。
* @throws {Error} 如果输入不是一个长度为1的字符串,抛出错误。
*/
getCharacter(char) {
if (typeof char !== 'string' || char.length !== 1) {
throw new Error('Character must be a single character string.')
}

if (!(char in this.#characters)) {
this.#characters[char] = new Character(char)
}
return this.#characters[char]
}
}

// 导出一个CharacterFactory的实例
export default new CharacterFactory()


使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import CharacterFactory from '../FlyweightPattern'

try {
// 尝试创建并显示字符对象
// 首次请求字符'A'的实例
const charA = CharacterFactory.getCharacter('A')
charA.display([1, 1])

// 首次请求字符'B'的实例
const charB = CharacterFactory.getCharacter('B')
charB.display([2, 2])

// 再次请求字符'A'的实例,预期将复用之前的实例而不是创建新实例
const anotherCharA = CharacterFactory.getCharacter('A')
anotherCharA.display([3, 3])

// 验证两个字符'A'的实例是否相同
console.log(charA === anotherCharA) // 预期输出 true,表明两个'A'字符引用了同一实例
} catch (error) {
// 捕获并输出可能发生的错误
console.error(error)
}


场景

  1. 游戏开发

    在游戏中,可能存在大量相同类型的对象,比如敌人、子弹、粒子等。这些对象的外部状态(位置、速度等)可能不同,但内部状态(外观、行为等)是相同的

    通过使用享元模式,游戏可以共享相同类型的对象,从而提高性能和降低内存占用

  2. Web 开发

    在 Web 应用程序中,可能存在大量重复的数据,比如页面元素、样式、图标等。通过使用享元模式,可以共享这些数据,并减少加载时间和网络带宽的消耗

  3. 图形编辑器

    在图形编辑器中,用户可以创建和编辑大量的图形对象,比如线条、圆形、矩形等

    通过使用享元模式,可以共享相同类型的图形对象,从而减少内存消耗,并提高绘图性能

  4. 操作系统

    在操作系统中,可能存在大量相同类型的资源,比如文件、进程、线程等

    通过使用享元模式,可以共享这些资源,并提高系统的整体性能和响应速度


源代码