设计模式 - 享元模式

享元模式的英文是flyweight pattern,不知道哪位大哥第一个把flyweight翻译成享元的,真牛,翻译的很形象。个人认为享元模式还是挺复杂的。

面向对象编程给我们带来了非常多的好处,但是同时它也有弊端,如果一个系统拥有非常非常多的对象,那么内存消耗会比较大,而且比较难管理。比如:

1. 游戏里面的粒子系统,大量的微粒,如果为每个微粒创建一个对象的话,那么对象数量是相当的恐怖;

2. 编辑软件,编辑软件里面会有大量的字符,如果一个文件有1M的字符,为每个字符创建一个对象的话,那么就有100万的对象;

3. 想象一个图形里面由一万根线条组成,那么就得有1万的对象。

上面的几个例子有一个共性:有大量类似的对象。其中很多对象可能具有一样的属性,甚至是完全一样的。比如第二个例子,100万字符里面,不同的字符不多,英文字母只有26个,加上大小写也就52个,就算再加上标点等等,我们假设只有100个不同的字符,如果我们能用100个字符对象来表示100万个字符,那是多大的进步啊。享元模式就是用来解决大量对象的问题的。

 

意图:

运用共享技术有效地支持大量细粒度的对象。

 

结构:

设计模式 - 享元模式_第1张图片

上图中,我给出了FlyweightFactory成员函数GetFlyweight的伪代码,从伪代码我们就可以发现FlyweightFactory里面保存了一个享元对象池。这个函数基本逻辑就是:

1. 如果池中已经有了符合条件的对象,就直接返回;

2. 如果池中没有符合条件的对象,就创建一个新对象并且加入到池中以供后面的客户使用。

享元对象池是享元模式的一个特征,也就是说享元模式一定需要维护一个享元对象池。维护享元对象池是需要一定代价的,如果系统里面的对象数量不多,那么使用享元模式得不偿失,比不使用享元模式更糟糕。所以,使用享元模式的一个基本前提就是:系统里面有大量的对象。

我们继续沿用前面讲过的图形例子,现在我们假如我们有一张图片,里面需要1万条直线(其中5000条实线,5000条虚线)。我们先把CLine类加上几个属性,如下面的代码所示:

class CGraphic
{
public:
	virtual void Draw() = 0;
};

class CLine: public CGraphic
{
public:
	CLine(): _xPos(0), _yPos(0), _len(10), _width(1), _color(0){}

	virtual void Draw()
	{
		std::cout << "Draw line\n";
	}

	void SetPos(int x, int y){_xPos = x; _yPos = y;}

protected:
	int _xPos;
	int _yPos;
	float _len;
	float _width;
	int   _color;
};

class CSolidLine: public CLine
{
public:
	virtual void Draw()
	{
		std::cout << "Draw solid line\n";
	}
};

class CDottedLine: public CLine
{
public:
	virtual void Draw()
	{
		std::cout << "Draw dotted line\n";
	}
};

CSolidLine表示实线,CDottedLine表示虚线。

CLine类现在有5个属性,起始位置x坐标,y坐标,线条长度,线条宽度和线条颜色。我们给起始位置加了个设置函数。OK,现在我们可以这么来画1万条直线:

for (int i = 0; i < 5000; i++)
	{
		CLine* line = new CSolidLine();
		line->SetPos(i, 0);
		line->Draw();

		delete line;
	}

	for (int i = 0; i < 5000; i++)
	{
		CLine* line = new CDottedLine();
		line->SetPos(i, 100);
		line->Draw();

		delete line;
	}

通过上面的代码我们画了5000条实线和5000条虚线。

上面的调用代码调用了1万次new和1万次delete。这个例子里面我们每次用完CLine对象后就删除了,假如实际情况是不可以删除的,那么这里就会存在1万个CLine对象。

现在,我们使用享元模式来看看效果。CLine类没有任何变化,我们只需要加入一个factory类:

设计模式 - 享元模式_第2张图片

class CLineFactory
{
public:
	static CLineFactory* GetInst()
	{
		static CLineFactory factory;
		return &factory;
	}

	CLine* GetLine(int type)//0 表示实线, 1 表示虚线
	{
		CLine* line = mapLines[type];
		if(line == NULL)//没有符合条件的享元对象,就需要新创建一个。
		{
			switch(type)
			{
			case 0:
				line = new CSolidLine();
				break;
			case 1:
				line = new CDottedLine();
				break;
			}
			if (line)
			{
				mapLines[type] = line;
			}
		}
		return line;
	}
protected:
	CLineFactory(){}
	std::map<int, CLine*> mapLines;
};

CLineFactory里面保存了一个map对象,map对象的key是CLine的种类,这里我们用一个简单的办法来区分实线和虚线(0 表示实线, 1 表示虚线),关键的地方是GetLine函数,我们可以看到这个函数会先检查是否已经存在符合条件的享元对象,如果有就返回那个对象,如果没有就创建一个新对象,然后放到享元池里面(mapLines)。现在再来看看客户端调用:

for (int i = 0; i < 5000; i++)
	{
		CLineFactory::GetInst()->GetLine(0)->Draw();
	}

	for (int i = 0; i < 5000; i++)
	{
		CLineFactory::GetInst()->GetLine(1)->Draw();
	}

现在通过享元模式,我们发现享元池里面总共就2个对象,一个实线对象,一个虚线对象。相比之前的1万个对象,已经节省了很多内存了。享元模式的优点就是节省内存空间。

我一开始就讲了:个人认为享元模式还是挺复杂的。那么它复杂在哪里呢?看看上面的例子,CLine类里面有5个属性:_xPos, _yPos, _len, _width, _color。其中_xPos和_yPos是可以设置的。我们可以把这5个属性分成:外部状态和内部状态两部分。其中_xPos和_yPos是2个外部状态,而_len, _width和_color是内部状态。上面的例子里面我把3个内部状态是hardcode写死的,也就是说画出来的1万条线,它们这3个属性是一样的,一样的长度,一样的宽度,一样的颜色。那么假如我们需要的线条的长度不一样,该怎么办呢?我们需要增加一个设置长度的函数,比如SetLen(),然后客户端需要调用SetLen()设置这些享元对象的长度(外部状态),就像上面例子里面设置起始位置一样。如果需要改变宽度,那么也得把宽度属性搞成外部状态。说享元模式复杂的其中一个原因就是:很多时候区分外部状态和内部状态并不容易。

我们再来考虑一个问题:例子当中,起始位置是计算出来的,然后再赋值给享元对象。那么假如我们想要知道第1000条实线的位置,怎么办?我想没有办法,因为享元池里面只保存了一个实线对象,这个对象就保存了一条实线的起始位置。例子中保存的是第5000条实线的位置。那么怎么办?通常我们会把这些外部状态保存到另外一个对象里面,比如我们可以创建一个外部状态类,这个类里面保存所有1万条线的状态。OK, 我们现在可以知道任何一条线的起始位置了。那么这里又有个问题,外部状态类岂不是又一个开销?没错,这里我们这里保存了1万个状态,这确实是额外的开销。想象这么种情况,很多时候有些外部状态是一样的,比如其中有100条线的起始位置一样,那么我们的起始位置状态就减少了99个。但是无论如何保存外部状态是一个额外的开销。

 

下面我们总结一下享元模式的一些问题:

1. 有时很难区分内部状态和外部状态;

2. 保存外部状态需要一定的开销,假如能够通过计算的方式来得到外部状态的话,就可以减少外部状态的开销。也就是说以时间换取空间;

3. 管理享元池需要一定的开销,比如查找开销。上面的例子是最最简单的一种情况,实际上,有时候可能这个key也挺难确定,或者key本身也是蛮复杂的对象。那么这个时候的查找可能更加费时。

 

既然享元模式有这么明显的缺点,我们为什么还需要它呢?说来说去就是为了当系统中有大量对象的时候可以共享某些对象以达到节省空间的目的。


 使用享元对象的要点:

1. 区分外部状态,并且从享元对象里面剔除出去;

2. 尽量用计算的方式来得到外部状态,如果不行,就只能使用外部状态池了。当外部状态是计算出来的而非存储的时候,享元模式的节约量达到最大。也就是以计算时间换取内存空间。

 

最后再来关注一个问题:享元对象并不一定都需要被共享。享元模式的结构图里面我们可以看到一个子类UnsharedConcreteFlyweight。享元模式的工厂类在生产这种类型的享元对象的时候,每次都会新创建一个而不会保存到享元池里面去。

你可能感兴趣的:(设计模式 - 享元模式)