设计模式的艺术之道–享元模式
声明:本系列为刘伟老师博客内容总结(http://blog.youkuaiyun.com/lovelion),博客中有完整的设计模式的相关博文,以及作者的出版书籍推荐
本系列内容思路分析借鉴了刘伟老师的博文内容,同时改用C#代码进行代码的演示和分析(Java资料过多 C#表示默哀).
本系列全部源码均在文末地址给出。
本系列开始讲解结构型模式,关注如何将现有类或对象组织在一起形成更加强大的结构。
不同的结构型模式从不同的角度组合类或对象,它们在尽可能满足各种面向对象设计原则的同时为类或对象的组合提供一系列巧妙的解决方案。
- 类结构型模式
关心类的组合,由多个类组合成一个更大的系统,在类结构型模式中一般只存在继承关系和实现关系 - 对象结构型模式
关心类与对象的组合,通过关联关系,在一个类中定义另一个类的实例对象,然后通过该对象调用相应的方法
7种常见的结构型模式
享元模式
在软件系统中,有时候会存在资源浪费的情况,例如在计算机内存中存储了多个完全相同或者非常相似的对象,如果这些对象的数量太多将导致系统运行代价过高,内存属于计算机的“稀缺资源”,不应该用来“随便浪费”,那么是否存在一种技术可以用于节约内存使用空间,实现对这些相同或者相似对象的共享访问呢?
1.1定义
-享元模式 (Flyweight Pattern):通过共享技术实现相同或相似对象的重用。
- 通常用一个享元池来存储这些对象。
- 享元池(Flyweight Pool):存储共享实例对象的地方。
1.2情景实例
问题描述
- 围棋棋子的设计
菜鸟软件公司开发人员通过对围棋软件进行分析,发现在围棋棋盘中包含大量的黑子和白子,它们的形状、大小都一模一样,只是出现的位置不同而已。如果将每一个棋子都作为一个独立的对象存储在内存中,将导致该围棋软件在运行时所需内存空间较大,如何降低运行代价、提高系统性能是Sunny公司开发人员需要解决的一个问题。为此引入了享元模式。
初步思路
使用享元模式来设计该围棋软件的棋子对象,享元模式结构较为复杂,一般结合工厂模式一起使用,在它的结构图中包含了一个享元工厂类。
UML类图
实例关键代码
namespace Flyweight
{
class IgoChessmanFactory
{
private static IgoChessmanFactory instance = new IgoChessmanFactory();
private Hashtable ht; //使用Hashtable来存储享元对象,充当享元池
private IgoChessmanFactory()
{
ht = new Hashtable();
IgoChessman black, white;
black = new BlackIgoChessman();
ht.Add("b", black);
white = new WhiteIgoChessman();
ht.Add("w", white);
}
//返回享元工厂类的唯一实例
public static IgoChessmanFactory GetInstance()
{
return instance;
}
//通过key来获取存储在Hashtable中的享元对象
public IgoChessman GetIgoChessman(string color)
{
return (IgoChessman)ht[color];
}
}
abstract class IgoChessman
{
public abstract string GetColor();
public void Display()
{
Console.WriteLine("棋子颜色:" + this.GetColor());
}
}
class BlackIgoChessman : IgoChessman
{
public override string GetColor()
{
return "黑色";
}
}
class Program
{
static void Main(string[] args)
{
IgoChessman black1, black2, black3, white1, white2;
IgoChessmanFactory factory;
//获取享元工厂对象
factory = IgoChessmanFactory.GetInstance();
//通过享元工厂获取三颗黑子
black1 = factory.GetIgoChessman("b");
black2 = factory.GetIgoChessman("b");
black3 = factory.GetIgoChessman("b");
Console.WriteLine("判断两颗黑子是否相同:" + (black1 == black2));
//通过享元工厂获取两颗白子
white1 = factory.GetIgoChessman("w");
white2 = factory.GetIgoChessman("w");
Console.WriteLine("判断两颗白子是否相同:" + (white1 == white2));
//显示棋子
black1.Display();
black2.Display();
black3.Display();
white1.Display();
white2.Display();
Console.ReadLine();
}
}
}
现有缺点(未来变化)
开发人员通过对围棋棋子进行进一步分析,发现虽然黑色棋子和白色棋子可以共享,但是它们将显示在棋盘的不同位置,如何让相同的黑子或者白子能够多次重复显示且位于一个棋盘的不同地方?
如何改进
就是将棋子的位置定义为棋子的一个外部状态,在需要时再进行设置。客户端进行设置外部状态。(通过传递参数设置,不要设置在棋子的自身字段属性中 否则就属于棋子了)。
内部状态(Intrinsic State):存储在享元对象内部并且不会随环境改变而改变的状态,内部状态可以共享(例如:字符的内容 白色棋子 黑色棋子)
外部状态(Extrinsic State):随环境改变而改变的、不可以共享的状态。享元对象的外部状态通常由客户端保存,并在享元对象被创建之后,需要使用的时候再传入到享元对象内部。一个外部状态与另一个外部状态之间是相互独立的(例如:字符的颜色和大小 棋子在棋盘的位置)
改进UML类图
实例关键代码
//工厂类同上
abstract class IgoChessman
{
public abstract string GetColor();
public void Display()
{
Console.WriteLine("棋子颜色:" + this.GetColor());
}
public void Display(Coordinates coord)
{
Console.WriteLine("棋子颜色:{0},棋子位置:{1},{2}", this.GetColor(),coord.X,coord.Y);
}
}
class Coordinates
{
private int x;
private int y;
public Coordinates(int x, int y)
{
this.x = x;
this.y = y;
}
public int X
{
get { return x; }
set { x = value; }
}
public int Y
{
get { return y; }
set { y = value; }
}
}
class Program
{
static void Main(string[] args)
{
IgoChessman black1,black2,black3,white1,white2;
IgoChessmanFactory factory;
//获取享元工厂对象
factory = IgoChessmanFactory.GetInstance();
//通过享元工厂获取三颗黑子
black1 = factory.GetIgoChessman("b");
black2 = factory.GetIgoChessman("b");
black3 = factory.GetIgoChessman("b");
Console.WriteLine("判断两颗黑子是否相同:" + (black1 == black2));
//通过享元工厂获取两颗白子
white1 = factory.GetIgoChessman("w");
white2 = factory.GetIgoChessman("w");
Console.WriteLine("判断两颗白子是否相同:" + (white1 == white2));
//显示棋子,同时设置棋子的坐标位置
black1.Display(new Coordinates(1, 2));
black2.Display(new Coordinates(3, 4));
black3.Display(new Coordinates(1, 3));
black1.Display();
white1.Display(new Coordinates(2, 5));
white2.Display(new Coordinates(2, 4));
Console.Read();
}
}
可能疑惑:以五子棋为例,只有一个白棋对象和和一个黑棋对象,假如我第一个白棋坐标是(1,1),但是当我下第二个白棋(2,2)的时候岂不是第一个白起会消失?也就是说棋盘上只会有 一颗白棋与一颗黑棋!
注意此时的坐标是人为给定的 并不是棋子自带的属性,下到 2,2点,相当于你在2,2点贴了一个白棋图片。外部的状态是不能共享的,需要单独的空间保存。
1.3模式分析
动机和意图
- 如果一个软件系统在运行时所创建的相同或相似对象数量太多,如何避免系统中出现大量相同或相似的对象?
一般结构
- 享元模式包含4个角色:
- Flyweight(抽象享元类):声明了具体享元类公共的方法,可以向外界提供享元对象的内部状态,也可以通过方法来设置外部状态。
- ConcreteFlyweight(具体享元类):其实例称为享元对象;在具体享元类中为内部状态提供了存储空间
- UnsharedConcreteFlyweight(非共享具体享元类):不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建,并且作为外部状态注入给具体享元对象。
- FlyweightFactory(享元工厂类):一个用来返回共享对象的工厂类
(1) 将具有相同内部状态的对象存储在享元池中,享元池中的对象是可以实现共享的
(2) 需要的时候将对象从享元池中取出,即可实现对象的复用
(3) 客户端向取出的对象设置不同外部状态,得到一系列相似的对象,而这些对象在内存中实际上只存储一份
享元模式UML类图

改进后的优点
- 可以减少内存中对象的数量,使得相同或者相似的对象在内存中只保存一份,从而可以节约系统资源,提高系统性能
- 外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享
现存的缺点
- 使得系统变得复杂,需要分离出内部状态和外部状态
- 享元模式需要将享元对象的部分状态外部化,而读取外部状态将使得运行时间变长
-
优化空间
单纯享元模式和复合享元模式
1.单纯享元模式
在单纯享元模式中,所有的具体享元类都是可以共享的,不存在非共享具体享元类
2.复合享元模式
将一些单纯享元对象使用组合模式加以组合,还可以形成复合享元对象,这样的复合享元对象本身不能共享,但是它们可以分解成单纯享元对象,而后者则可以共享。
通过复合享元模式,可以确保复合享元类CompositeConcreteFlyweight中所包含的每个单纯享元类ConcreteFlyweight都具有相同的外部状态,而这些单纯享元的内部状态往往可以不同。如果希望为多个内部状态不同的享元对象设置相同的外部状态,可以考虑使用复合享元模式。
复合享元类(本身不能共享)中包含抽象享元类的集合,集合中可以有多个具体的享元类(不同外部状态),通过操作方法可以注入相同的外部状态。比如操作一波War3的英雄以及小兵进行移动到某个区域位置,英雄是具体的享元类,炮车小兵,近战小兵和远程小兵也是,框选状态就是集合添加过程,鼠标点击要移动的区域位置,就是设置了相同的外部状态。
注意事项
1.与其他模式的联用
享元模式通常需要和其他模式一起联用,几种常见的联用方式如下:
(1)在享元模式的享元工厂类中通常提供一个静态的工厂方法用于返回享元对象,使用简单工厂模式来生成享元对象。
(2)在一个系统中,通常只有唯一一个享元工厂,因此可以使用单例模式进行享元工厂类的设计。
(3)享元模式可以结合组合模式形成复合享元模式,统一对多个享元对象设置外部状态。
2.享元模式与String类
C#String使用了享元模式。
适用场景
(1) 一个系统有大量相同或者相似的对象,造成内存的大量耗费
(2)在使用享元模式时需要维护一个存储享元对象的享元池,在需要多次重复使用享元对象时
举例:Word字体的装饰功能 字体本身是为了显示 但是可以变成新的颜色 变成新的字体 新的字号
享元模式和对象池
从某种角度享元模式和对象池可以认为是同一种技术。
享元模式原型一般同类型只有一个储存在原型池中,如黑白棋子各一个 ,并且外部状态是通过参数传递设置。
对象池中的共享实例(如FPS的子弹),可能会因为使用频繁而产生多个,客户端使用时,先取出对象池汇总实例,同时对外部状态进行设置(子弹初始位置 初始速度等),对象使用完毕需要放回对象池。
实例源代码
GitHub地址
百度云地址:链接: https://pan.baidu.com/s/1nvoVz6p 密码: rhxh
本文深入探讨享元模式的原理及应用场景,通过围棋棋子实例展示了如何利用享元模式减少内存中对象数量,节省系统资源并提升性能。文章还讨论了模式的优缺点,并介绍了单纯享元模式与复合享元模式的区别。
237

被折叠的 条评论
为什么被折叠?



