一、为何需要享元模式(Flyweight)?
假如在网页中渲染这样的一个画面:大小不一的星星铺满了整个画布,并且都在不断的进行移动闪烁着。一批星星消失了,另一批又从另一边缘处出现。
要实现这样的渲染效果,在程序中就得需要创建这些星星,然后将它们一个个画上去。
有个问题就是,如果我们还得按照平常创建对象的方式,对每一颗出现的星星都创建一遍。一旦星星从画布上消失,就销毁并释放内存。这么做的话,系统就得要开销大量的内存空间,而且频繁进行创建销毁的操作会影响程序运行的性能。到最后我们只能看到这样的效果:一打开网页后,画布上的那些星星隔了一段时间才全部出现,滚动网页也不流畅。
为了解决“隔了一段时间才全部出现”和“滚动网页也不流畅”的问题(因为存储大量对象而导致开销大量内存、频繁创建销毁对象而导致程序的性能下降),我们就应该尽可能少的生成那些存在相同状态的星星。
其实在繁星点点的夜空中,总会有很多相似或者相同的星星存在着。因此我们可以只创建一颗星星作为一个共享对象,来代表这些与之相似的同类星星。在渲染的时候,只对这个共享对象重复的画上去就行了。
享元模式的定义:运用共享技术有效的支持大量细粒度的对象。
从以上的定义,我们先分析“享元”和”细粒度“这两个关键词。
- 享元即共享元对象。元这个词,就好比数据库表中的一组若干个元数据,元数据是数据的最小单位。所以元对象的意思也就是,一组同类对象中的每一个元对象。既然这些同类对象都是相同状态的对象,我们只需要创建一个共享元对象来代表它们。
- 细粒度如颗粒大小一样,即这个对象并不庞大而复杂。所以享元对象就应当是结构简单的对象。
但是我们怎么区分对象之间是否存在相同状态,然后将他们归类为某一组同类对象呢。于是可以对这些对象内的某个属性作为唯一标识,来判断是否为同类对象。比如星星,唯一标识可以是星星的半径值,也可以是速度值等。在这里我们选择的是以半径作为唯一标识。如图所示:
(在享元对象容器,每一个享元对象之间的半径值不相同,都代表着各自一组与自己半径相同的星星。)
享元对象是有了,但是我们编程中,起码也要保证唯一标识的半径值,是不能因为程序的变化而变化吧,即对象的外部不能对半径进行修改。这时候就有了外部状态和内部状态的区别。
外部状态: 存在于享元对象的外部。因环境变化而变化。(环境变化即客户端发生的状态变化)
内部状态: 存在于享元对象的内部。不能因环境变化而变化。
-
外部状态是客户端进行的活动变化,比如为星星的位置进行随机布局,控制星星消失和出现的个数等等。
-
内部状态是对象固有的状态,即一颗半径大小为4的星星,半径大小是固有的,客户端把它画上去时,不能强行使它变大变小。要保持内部状态,对象内的所有属性和状态都应当被保护起来,如对象内的所有字段都被设置为私有的访问机制。
特点:
- 减少创建对象的数量,使用享元对象来代表一组同类对象。
结构:
抽象享元(Flyweight):具体享元类的基类。规定了具体享元要实现的方法,,也可以接受并作用于外部状态。(接受并作用于:传外部状态的参数到该方法内,以处理外部状态,但不改变享元内部状态。)
具体享元类(ConcreteFlyweight):实现抽象享元的方法。如果存在内部状态(即在享元容器里没有存在该享元时,就实例化一个),则增加存储空间。
享元工厂类(FlyweightFactory):创建和管理享元对象。
客户端类(Client):所有享元对象的引用,并存储对应的外部状态。(引用:把星星画上去;存储外部状态:存储星星的位置和出现的个数等)
适合应用场景特点:
- 需要大量细粒度的对象,来完成某个功能。
- 大量对象有存在着相同的状态。
- 实际例子:线程池中的共享线程,解决频繁创建销毁线程的问题,字符串常量池中的共享字符串,解决重复创建存在值相同的字符串的问题…
二、例子
需求:
在视频剪辑中,用户想要在画布上实现满天星的效果,就做了如下参数的设置:
1)星星的总数为1000000,并且这些星星的大小和个数都是随机的;
2)星星的半径大小在 1~10 范围;
3)勾选默认星星的速度、闪烁频率、亮度都跟半径的大小有关,所以不用用户来自定义这些参数的值。
设计分析:
- 以半径作为唯一标识,一个享元对象对应一组半径相同的对象。
- 享元对象最多有 10 个。需要渲染的对象有1000000 个。
1、定义抽象享元和具体享元类
//Flyweight:抽象享元(星星抽象)
public interface IStar
{
void draw();
}
//ConcreteFlyweight:具体享元类(星星)
public class Star:IStar
{
private int Radius;
private double Brightness;
private double Twinkle;
private double Speed;
public Star(int radius)
{
Radius = radius;
//Brightness = ...;
//Twinkle = ...;
//Speed = ...;
}
//1、args:外部状态可作为参数传入,但不能改变 Star 的内部状态
public void draw(/*args:若有外部实例传入*/)
{
//处理一些与外部有关的逻辑...
//外部参数不能改变 Star 类的所有属性值和其他内部状态
}
//2、如果该方法在客户端进行调用,而不是在享元工厂创建 Star 类的享元对象时调用:
//那么该方法的内部逻辑是错误的,是因为不能通过外部 val 改变 Brightness 值。
public void setBrightness(double val)
{
Brightness = val;
}
}
2、定义享元工厂类
//FlyweightFactory:享元工厂类(创建和管理享元对象)
public class FlyweightFactory
{
//享元对象的容器
private Dictionary<int, IStar> StarsDict = new Dictionary<int, IStar>();
//创建和获取享元对象,radius 为标识享元对象的参数。
public IStar getStar(int radius)
{
IStar star = null;
//在容器中是否存在跟 radius 值相同的对象
StarsDict.TryGetValue(radius,out star);
if(star == null)//若不存在,则创建一个享元并存入到容器里
{
star = new Star(radius);
StarsDict.Add(radius,star);
}
return star;
}
}
3、主程序
//主程序
class Program
{
static void Main(string[] args)
{
Random random = new Random(10);
//享元模式-----------------
FlyweightFactory factory = new FlyweightFactory();
for (int i = 0; i < 1000000; i++)
{
//随机生成半径大小为1~10范围的星星。
var star = factory.getStar(random.Next(1, 10));
star.draw();//对外部状态的影响
}
//-------------------------
//非享元模式----------------
List<IStar> starList = new List<IStar>();
for (int i = 0; i < 1000000; i++)
{
//创建了 1000000 个对象,执行整个循环的时间久。
var star = new Star(random.Next(1, 10));
starList.Add(star);
star.draw();
}
//--------------------------
//验证非享元模式和享元模式,在当前程序里使用内存大小的情况:
var process = Process.GetCurrentProcess();
var memorySize = process.PrivateMemorySize64 / (1024 * 1024);//单位为 M.
Console.WriteLine(memorySize);
//某次运行验证的结果:
//享元模式,内存占用:17M;非享元模式,内存占用:60M;
Console.ReadLine();
}
}