意图

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

问题

假如你希望在长时间工作后放松一下,所以开发了一款简单的游戏:玩家们在地图上移动并相互射击。你决定实现一个真实的粒子系统,并将其作为游戏的特色。大量的子弹、导弹和爆炸弹片会在整个地图上穿行,为玩家提供紧张刺激的游戏体验。

开发完成后,你推送提交了最新版本的程序,并在编译游戏后将其发送给了一个朋友进行测试。尽管该游戏在你的电脑上完美运行,但是你的朋友却无法长时间进行游戏:游戏总是会在他的电脑上运行几分钟后崩溃。在研究了几个小时的调试消息记录后,你发现导致游戏崩溃的原因是内存容量不足。朋友的设备性能远比不上你的电脑,因此游戏运行在他的电脑上时很快就会出现问题。

真正的问题与粒子系统有关。每个粒子(一颗子弹、一枚导弹或一块弹片)都由包含完整数据的独立对象来表示。当玩家在游戏中鏖战进入高潮后的某一时刻,游戏将无法在剩余内存中载入新建粒子,于是程序就崩溃了。

解决方案

仔细观察粒子Particle类,你可能会注意到颜色(color)和精灵图(sprite)这两个成员变量所消耗的内存要比其他变量多得多。更糟糕的是,对于所有的粒子来说,这两个成员变量所存储的数据几乎完全一样(比如所有子弹的颜色和精灵图都一样)。

每个粒子的另一些状态(坐标、移动矢量和速度)则是不同的。因为这些成员变量的数值会不断变化。这些数据代表粒子在存续期间不断变化的情景,但每个粒子的颜色和精灵图则会保持不变。

对象的常量数据通常被称为内在状态,其位于对象中,其他对象只能读取但不能修改其数值。而对象的其他状态常常能被其他对象“从外部”改变,因此被称为外在状态。

享元模式建议不在对象中存储外在状态,而是将其传递给依赖于它的一个特殊方法。程序只在对象中保存内在状态,以方便在不同情景下重用。这些对象的区别仅在于其内在状态(与外在状态相比,内在状态的变体要少很多),因此你所需的对象数量会大大削减。

让我们回到游戏中。假如能从粒子类中抽出外在状态,那么我们只需三个不同的对象(子弹、导弹和弹片)就能表示游戏中的所有粒子。你现在很可能已经猜到了,我们将这样一个仅存储内在状态的对象称为享元。

享元:该术语来自拳击,代表50公斤级的拳击手。

外在状态存储

那么外在状态会被移动到什么地方呢?总得有类来存储它们,对不对?在大部分情况中,它们会被移动到容器对象中,也就是我们应用享元模式前的聚合对象中。

在我们的例子中,容器对象就是主要的游戏Game对象,其会将所有粒子存储在名为粒子particles的成员变量中。为了能将外在状态移动到这个类中,你需要创建多个数组成员变量来存储每个粒子的坐标、方向矢量和速度。除此之外,你还需要另一个数组来存储指向代表粒子的特定享元的引用。这些数组必须保持同步,这样你才能够使用同一索引来获取关于某个粒子的所有数据。

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

稍等! 这样的话情景对象数量不是会和不采用该模式时的对象数量一样多吗?的确如此,但这些对象要比之前小很多。消耗内存最多的成员变量已经被移动到很少的几个享元对象中了。现在,一个享元大对象会被上千个情境小对象复用,因此无需再重复存储数千个大对象的数据。

享元与不可变性

由于享元对象可在不同的情景中使用,你必须确保其状态不能被修改。享元类的状态只能由构造函数的参数进行一次性初始化,它不能对其他对象公开其设置器或公有成员变量。

享元工厂

为了能更方便地访问各种享元,你可以创建一个工厂方法来管理已有享元对象的缓存池。工厂方法从客户端处接收目标享元对象的内在状态作为参数,如果它能在缓存池中找到所需享元,则将其返回给客户端;如果没有找到,它就会新建一个享元,并将其添加到缓存池中。

你可以选择在程序的不同地方放入该函数。最简单的选择就是将其放置在享元容器中。除此之外,你还可以新建一个工厂类,或者创建一个静态的工厂方法并将其放入实际的享元类中。

结构

  1. 享元模式只是一种优化。在应用该模式之前,你要确定程序中存在与大量类似对象同时占用内存相关的内存消耗问题,并且确保该问题无法使用其他更好的方式来解决。
  2. 享元(Flyweight)类包含原始对象中部分能在多个对象中共享的状态。同一享元对象可在许多不同情景中使用。享元中存储的状态被称为“内在状态”。传递给享元方法的状态被称为“外在状态”。
  3. 情景(Context)类包含原始对象中各不相同的外在状态。情景与享元对象组合在一起就能表示原始对象的全部状态。
  4. 通常情况下,原始对象的行为会保留在享元类中。因此调用享元方法必须提供部分外在状态作为参数。但你也可将行为移动到情景类中,然后将连入的享元作为单纯的数据对象。
  5. 客户端(Client)负责计算或存储享元的外在状态。在客户端看来,享元是一种可在运行时进行配置的模板对象,具体的配置方式为向其方法中传入一些情景数据参数。
  6. 享元工厂(Flyweight Factory)会对已有享元的缓存池进行管理。有了工厂后,客户端就无需直接创建享元,它们只需调用工厂并向其传递目标享元的一些内在状态即可。工厂会根据参数在之前已创建的享元中进行查找,如果找到满足条件的享元就将其返回;如果没有找到就根据参数新建享元。

实现方式

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

代码演示

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
using System;
using System.Collections.Generic;
using System.Linq;
// Use Json.NET library, you can download it from NuGet Package Manager
using Newtonsoft.Json;

namespace RefactoringGuru.DesignPatterns.Flyweight.Conceptual
{
// The Flyweight stores a common portion of the state (also called intrinsic
// state) that belongs to multiple real business entities. The Flyweight
// accepts the rest of the state (extrinsic state, unique for each entity)
// via its method parameters.
public class Flyweight
{
private Car _sharedState;

public Flyweight(Car car)
{
this._sharedState = car;
}

public void Operation(Car uniqueState)
{
string s = JsonConvert.SerializeObject(this._sharedState);
string u = JsonConvert.SerializeObject(uniqueState);
Console.WriteLine($"Flyweight: Displaying shared {s} and unique {u} state.");
}
}

// The Flyweight Factory creates and manages the Flyweight objects. It
// ensures that flyweights are shared correctly. When the client requests a
// flyweight, the factory either returns an existing instance or creates a
// new one, if it doesn't exist yet.
public class FlyweightFactory
{
private List<Tuple<Flyweight, string>> flyweights = new List<Tuple<Flyweight, string>>();

public FlyweightFactory(params Car[] args)
{
foreach (var elem in args)
{
flyweights.Add(new Tuple<Flyweight, string>(new Flyweight(elem), this.getKey(elem)));
}
}

// Returns a Flyweight's string hash for a given state.
public string getKey(Car key)
{
List<string> elements = new List<string>();

elements.Add(key.Model);
elements.Add(key.Color);
elements.Add(key.Company);

if (key.Owner != null && key.Number != null)
{
elements.Add(key.Number);
elements.Add(key.Owner);
}

elements.Sort();

return string.Join("_", elements);
}

// Returns an existing Flyweight with a given state or creates a new
// one.
public Flyweight GetFlyweight(Car sharedState)
{
string key = this.getKey(sharedState);

if (flyweights.Where(t => t.Item2 == key).Count() == 0)
{
Console.WriteLine("FlyweightFactory: Can't find a flyweight, creating new one.");
this.flyweights.Add(new Tuple<Flyweight, string>(new Flyweight(sharedState), key));
}
else
{
Console.WriteLine("FlyweightFactory: Reusing existing flyweight.");
}
return this.flyweights.Where(t => t.Item2 == key).FirstOrDefault().Item1;
}

public void listFlyweights()
{
var count = flyweights.Count;
Console.WriteLine($"\nFlyweightFactory: I have {count} flyweights:");
foreach (var flyweight in flyweights)
{
Console.WriteLine(flyweight.Item2);
}
}
}

public class Car
{
public string Owner { get; set; }

public string Number { get; set; }

public string Company { get; set; }

public string Model { get; set; }

public string Color { get; set; }
}

class Program
{
static void Main(string[] args)
{
// The client code usually creates a bunch of pre-populated
// flyweights in the initialization stage of the application.
var factory = new FlyweightFactory(
new Car { Company = "Chevrolet", Model = "Camaro2018", Color = "pink" },
new Car { Company = "Mercedes Benz", Model = "C300", Color = "black" },
new Car { Company = "Mercedes Benz", Model = "C500", Color = "red" },
new Car { Company = "BMW", Model = "M5", Color = "red" },
new Car { Company = "BMW", Model = "X6", Color = "white" }
);
factory.listFlyweights();

addCarToPoliceDatabase(factory, new Car {
Number = "CL234IR",
Owner = "James Doe",
Company = "BMW",
Model = "M5",
Color = "red"
});

addCarToPoliceDatabase(factory, new Car {
Number = "CL234IR",
Owner = "James Doe",
Company = "BMW",
Model = "X1",
Color = "red"
});

factory.listFlyweights();
}

public static void addCarToPoliceDatabase(FlyweightFactory factory, Car car)
{
Console.WriteLine("\nClient: Adding a car to database.");

var flyweight = factory.GetFlyweight(new Car {
Color = car.Color,
Model = car.Model,
Company = car.Company
});

// The client code either stores or calculates extrinsic state and
// passes it to the flyweight's methods.
flyweight.Operation(car);
}
}
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
FlyweightFactory: I have 5 flyweights:
Camaro2018_Chevrolet_pink
black_C300_Mercedes Benz
C500_Mercedes Benz_red
BMW_M5_red
BMW_white_X6

Client: Adding a car to database.
FlyweightFactory: Reusing existing flyweight.
Flyweight: Displaying shared {"Owner":null,"Number":null,"Company":"BMW","Model":"M5","Color":"red"} and unique {"Owner":"James Doe","Number":"CL234IR","Company":"BMW","Model":"M5","Color":"red"} state.

Client: Adding a car to database.
FlyweightFactory: Can't find a flyweight, creating new one.
Flyweight: Displaying shared {"Owner":null,"Number":null,"Company":"BMW","Model":"X1","Color":"red"} and unique {"Owner":"James Doe","Number":"CL234IR","Company":"BMW","Model":"X1","Color":"red"} state.

FlyweightFactory: I have 6 flyweights:
Camaro2018_Chevrolet_pink
black_C300_Mercedes Benz
C500_Mercedes Benz_red
BMW_M5_red
BMW_white_X6
BMW_red_X1

参考原文:享元设计模式