亦称:缓存、Cache、Flyweight

享元是一种结构型设计模式,它摒弃了在每个对象中保存所有数据的方式,通过共享多个对象所共有的相同状态,在有限的内存容量中载入更多对象。

换句话来说, 享元会将不同对象的相同数据进行缓存以节省内存。

背景

开发一款简单的游戏: 玩家们在地图上移动并相互射击。

  • 决定实现一个真实的粒子系统, 并将其作为游戏的特色。
  • 大量的子弹、导弹和爆炸弹片会在整个地图上穿行,为玩家提供紧张刺激的游戏体验。

开发完成后,你推送提交了最新版本的程序,并在编译游戏后将其发送给了一个朋友进行测试。

  • 尽管该游戏在你电脑上完美运行,但是你朋友却无法长时间进行游戏:游戏总是会在他的电脑上运行几分钟后崩溃。
  • 在研究了几个小时的调试消息记录后,你发现导致游戏崩溃的原因是内存容量不足。

真正的问题与粒子系统有关。 每个粒子(一颗子弹、一枚导弹或一块弹片)都由包含完整数据的独立对象来表示。

  • 当玩家在游戏中鏖战进入高潮后的某一时刻,游戏将无法在剩余内存中载入新建粒子,于是程序就崩溃了。

解决方案

引入享元

Game-1

仔细观察粒子Particle类,可能会注意到颜色(color)和精灵图(sprite)这两个成员变量所消耗的内存要比其他变量多得多。

  • 更糟糕的是, 对于所有的粒子来说,这两个成员变量所存储的数据几乎完全一样(比如所有子弹的颜色和精灵图都一样)。
  • 每个粒子的另一些状态(坐标、 移动矢量和速度)则是不同的。因为这些成员变量的数值会不断变化。这些数据代表粒子在存续期间不断变化的情景,
  • 但每个粒子的颜色和精灵图则会保持不变。

对象的常量数据通常被称为内在状态,其位于对象中,其他对象只能读取但不能修改其数值。

对象的其他状态常常能被其他对象 “从外部” 改变, 因此被称为外在状态

Game-2

享元模式建议不在对象中存储外在状态,而是将其传递给依赖于它的一个特殊方法。

  • 程序只在对象中保存内在状态,以方便在不同情景下重用。 (相同内在状态的对象保留一个即可,且内在状态不可被修改)
  • 这些对象的区别仅在于其内在状态(与外在状态相比,内在状态的变体要少很多),因此所需的对象数量会大大削减。

假如能从粒子类中抽出外在状态,那么我们只需三个不同的对象(子弹、 导弹和弹片)就能表示游戏中的所有粒子。

我们将这样一个仅存储内在状态的对象称为享元

外在状态存储

Game-3

在大部分情况中,外在状态会被移动到容器对象中,也就是我们应用享元模式前的聚合对象中。

在我们的例子中,容器对象就是主要的游戏Game对象,将所有粒子存储在名为粒子particles的成员变量中。

  • 为了能将外在状态移动到这个类中,需要创建多个数组成员变量来存储每个粒子的坐标、方向矢量和速度。
  • 需要另一个数组来存储指向代表粒子的特定享元的引用。
  • 这些数组必须保持同步对应,这样你才能够使用同一索引来获取关于某一个粒子的所有数据。

更优雅的解决方案是创建独立的情景类来存储外在状态和对享元对象的引用。 在该方法中, 容器类只需包含一个情景类数组,数组元素存储每个粒子的享元引用和外在状态。

这样的话情景对象数量不是会和不采用该模式时的对象数量一样多吗?

  • 的确如此, 但这些对象要比之前小很多
  • 消耗内存最多的成员变量已经被移动到很少的几个享元对象中了
  • 现在一个享元大对象会被上千个情境小对象复用,因此无需再重复存储数千个大对象的数据。

享元与不可变性

由于享元对象可在不同的情景中使用,必须确保其状态不能被修改。

享元类的状态只能由构造函数的参数进行一次性初始化,不能对其他对象公开其设置器或公有成员变量。

享元工厂

为了能更方便地访问各种享元,可以创建一个工厂方法来管理已有享元对象的缓存池

  • 工厂方法从客户端处接收目标享元对象的内在状态作为参数
  • 如果它能在缓存池中找到所需享元,则将其返回给客户端
  • 如果没有找到,它就会新建一个享元,并将其添加到缓存池中。

flyweight

适用场景

在程序必须支持大量对象且没有足够的内存容量时使用享元模式。

应用该模式所获的收益大小取决于使用它的方式和情景,在下列情况中最有效:

  • 程序需要生成数量巨大的相似对象
  • 这将耗尽目标设备的所有内存
  • 对象中包含可抽取且能在多个对象间共享的重复状态

实现步骤

  1. 将需要改写为享元的类成员变量拆分为两个部分:
    • 内在状态:包含不变的、可在许多对象中重复使用的数据的成员变量
    • 外在状态:包含每个对象各自不同的情景数据的成员变量
  2. 保留类中表示内在状态的成员变量,并将其属性设置为不可修改。这些变量仅可在构造函数中获得初始数值。
  3. 找到所有使用外在状态成员变量的方法,为在方法中所用的每个成员变量新建一个参数,并使用该参数代替成员变量。
  4. 有选择地创建工厂类来管理享元缓存池,负责在新建享元时检查已有的享元。
    • 如果选择使用工厂,客户端就只能通过工厂来请求享元,需要将享元的内在状态作为参数传递给工厂
  5. 客户端必须存储和计算外在状态(情景)的数值,只有这样才能调用享元对象的方法。
  6. 为了使用方便,外在状态和引用享元的成员变量可以移动到单独的情景类中。

优缺点

优点:

  • 如果程序中有很多相似对象, 将节省大量内存。

缺点:

  • 可能需要牺牲执行速度来换取内存,因为每次调用享元方法时都需要重新计算部分情景数据。
  • 代码会变得更加复杂。

与其它模式的关系

  • 可以使用享元模式实现组合模式树的共享叶节点以节省内存。
  • 享元展示了如何生成大量的小型对象,外观模式则展示了如何用一个对象来代表整个子系统。
  • 如果你能将对象的所有共享状态简化为一个享元对象, 那么享元就和单例模式类似了。 但这两个模式有两个根本性的不同。
    • 只会有一个单例实体,但是享元类可以有多个实体,各实体的内在状态也可以不同。
    • 单例对象可以是可变的,享元对象是不可变的。

示例

在游戏 《反恐精英》 中,恐怖分子和反恐精英身着不同类型的衣物。 假设双方都各有一种服装类型,服装对象嵌入在玩家对象之中.

1
2
3
4
5
6
type player struct {
    dress      dress
    playerType string // 可为 T 或 CT
    lat        int
    long       int
}

假设目前有 5 名恐怖分子和 5 名反恐精英, 一共是 10 名玩家。

  • 方式1:10 个玩家对象各自创建不同的服装对象, 并将其嵌入。 总共会创建 10 个服装对象。
  • 方式2:我们创建两个服装对象:
    • 单一恐怖分子服装对象:其将在 5 名恐怖分子之间共享。
    • 单一反恐精英服装对象:其将在 5 名反恐精英之间共享。

可以看到

  • 方式1中我们总共创建了 10 个服装对象;
  • 方式2中则只有 2 个服装对象。

第二种方法, 就是我们所遵循的享元设计模式。 我们所创建的 2 个服装对象被称为是享元对象

享元模式会从对象中提取出公共部分并创建享元对象。

  • 这些享元对象 (服装) 随后可在多个对象 (玩家) 中分享。
  • 这极大地减少了服装对象的数量, 更棒的是即便创建了更多玩家,也只需这么两个服装对象就足够了。
1
2
3
4
// dress 享元接口
type dress interface {
	getColor() string
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import "image"

// counterTerroristDress 具体享元对象,反恐精英的衣服
type counterTerroristDress struct {
	color string
	img   image.Image // 大内存字段
}

func (c *counterTerroristDress) getColor() string {
	return c.color
}

func newCounterTerroristDress() *counterTerroristDress {
	print("new CT dress")
	return &counterTerroristDress{color: "green"}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import "image"

// terroristDress 具体享元对象,恐怖分子的衣服
type terroristDress struct {
	color string
	img   image.Image // 大内存字段
}

func (t *terroristDress) getColor() string {
	return t.color
}

func newTerroristDress() *terroristDress {
	print("new T dress")
	return &terroristDress{color: "red"}
}
 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
import "fmt"

const (
	// terroristDressType terrorist dress type
	terroristDressType = "tDress"

	// counterTerroristDressType terrorist dress type
	counterTerroristDressType = "ctDress"
)

var (
	dressFactorySingleInstance = &dressFactory{
		dressMap: make(map[string]dress),
	}
)

type dressFactory struct {
	dressMap map[string]dress
}

func (d *dressFactory) getDressByType(dressType string) (dress, error) {
	if d.dressMap[dressType] != nil {
		return d.dressMap[dressType], nil
	}

	if dressType == terroristDressType {
		d.dressMap[dressType] = newTerroristDress()
		return d.dressMap[dressType], nil
	}
	if dressType == counterTerroristDressType {
		d.dressMap[dressType] = newCounterTerroristDress()
		return d.dressMap[dressType], nil
	}

	return nil, fmt.Errorf("Wrong dress type passed")
}

// getDressFactorySingleInstance 单例工厂,返回享元
func getDressFactorySingleInstance() *dressFactory {
	return dressFactorySingleInstance
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Game
type Game struct {
	terrorists        []*Player
	counterTerrorists []*Player
}

func NewGame() *Game {
	return &Game{
		terrorists:        make([]*Player, 0),
		counterTerrorists: make([]*Player, 0),
	}
}

func (c *Game) AddTerrorist() {
	player := newPlayer("T", terroristDressType)
	c.terrorists = append(c.terrorists, player)
	return
}

func (c *Game) AddCounterTerrorist() {
	player := newPlayer("CT", counterTerroristDressType)
	c.counterTerrorists = append(c.counterTerrorists, player)
	return
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

// 客户端
func main() {
	game := NewGame()

	// Add Terrorists,共享T的dress对象
	game.AddTerrorist()
	game.AddTerrorist()
	game.AddTerrorist()
	game.AddTerrorist()

	// Add CounterTerrorists,共享T的dress对象
	game.AddCounterTerrorist()
	game.AddCounterTerrorist()
	game.AddCounterTerrorist()
}

References

guru