二十三种设计模式[11] - 享元模式(Flyweight Pattern)
摘要
享元模式,对象结构型模式的一种。在《设计模式 - 可复用的面向对象软件》一书中将之描述为“ 运用共享技术有效地支持大量细粒度的对象 ”。
在享元模式中,通过工厂方法去统一管理一个对象的创建,在创建对象前会尝试复用工厂类中缓存的已创建对象,如果未找到可重用的对象则创建新的对象实例并将其缓存。以此来减少对象的数量达到减少内存开销的目的。
在学习享元模式前,需对工厂方法有所了解。可参考二十三种设计模式[1] - 工厂方法(Factory Method)。
结构
- Flyweight(享元接口):所有享元类的接口;
- ConcreteFlyweight(享元):可被共享的类,封装了自身的内部状态(工厂判断缓存中是否存在可复用对象的依据,可以被共享但不可被修改);
- UnsharedConcreteFlyweight(非共享享元):不可被共享的类,通常为复合Flyweight对象;
- FlyweightFactory(享元工厂):用来创建并管理Flyweight对象。当用户请求一个Flyweight时,首先尝试重用缓存中的对象,其次创建新对象;
注:
- 在享元模式中,可以被共享的内容称为内部状态,它是工厂用来判断缓存中是否存在可复用对象的依据,不可被修改;
- 在享元模式中,需要用户设置并且不能被共享的内容称为外部状态。
示例
考虑一个画图工具,这个画图工具提供了各种颜色的圆形和长方形。在设计这个工具时,可能会创建一个Shape接口以及实现了这个接口的Circle和Rectangle类,在Circle和Rectangle类中存在一个Color属性来标识自身的颜色。当我们使用这个工具画出100个图形时,系统中也会分别创建这100个图形的实例。也就是说,这个工具画出的图形数量越多占用的内存越大。并不是一个合理的设计。
现在我们用享元模式重新设计这个画图工具。首先,创建一个Shape接口以及接口的实现类Circle和Rectangle。在Circle和Rectangle中存在一个string类型的属性来标识自身显示的文本。Shape的实例同一由工厂FlyweightFactory提供,在FlyweightFacatory中存在一个Shape的Dictionary作为享元的缓存池,每当客户向工厂请求Shape的实例时,优先尝试重用缓存池中的实例。具体实现如下。
-
单纯享元模式
public interface IShape { void Draw(ConsoleColor color, int x, int y); } public class Circle : IShape { private string _text = string.Empty; public Circle(string text) { this._text = text; Console.WriteLine($"开始实例化Circle [{text}]"); } public void Draw(ConsoleColor color, int x, int y) { Console.WriteLine($"Circle Draw [Text: {this._text}, Color: {color.ToString()}, 坐标x: {x} y: {y}]"); } } public class Rectangle : IShape { private string _text = string.Empty; public Rectangle(string text) { this._text = text; Console.WriteLine($"开始实例化Rectangle [{text}]"); } public void Draw(ConsoleColor color, int x, int y) { Console.WriteLine($"Rectangle Draw [Text: {this._text}, Color: {color.ToString()}, 坐标x: {x} y: {y}]"); } } public class FlyweightFactory { private FlyweightFactory() { } private static FlyweightFactory _instance = null; private static object _sysLock = new object(); public static FlyweightFactory Instance { get { if(_instance == null) { lock (_sysLock) { if(_instance == null) { _instance = new FlyweightFactory(); } } } return _instance; } } private Dictionary<string, IShape> _shapePool = new Dictionary<string, IShape>(); public int PoolSize => this._shapePool.Count; public IShape CreateCircle(string text) { IShape shape = this._shapePool.ContainsKey(text) && this._shapePool[text] is Circle ? this._shapePool[text] : null; if(shape == null) { shape = new Circle(text); this._shapePool.Add(text, shape); } return shape; } public IShape CreateRectangle(string text) { IShape shape = this._shapePool.ContainsKey(text) && this._shapePool[text] is Rectangle ? this._shapePool[text] : null; if (shape == null) { shape = new Rectangle(text); this._shapePool.Add(text, shape); } return shape; } } static void Main(string[] args) { IShape circleA = FlyweightFactory.Instance.CreateCircle("很圆的圆形"); IShape circleB = FlyweightFactory.Instance.CreateCircle("很圆的圆形"); IShape rectangleA = FlyweightFactory.Instance.CreateRectangle("很方的长方形"); circleA.Draw(ConsoleColor.Red, 20, 30); circleB.Draw(ConsoleColor.Yellow, 10, 130); rectangleA.Draw(ConsoleColor.Blue, 25, 60); Console.WriteLine($"享元池长度:{FlyweightFactory.Instance.PoolSize}"); Console.WriteLine($"circleA与circleB是否指向同一内存地址:{object.ReferenceEquals(circleA, circleB)}"); Console.ReadKey(); }
示例中,string类型的属性_text作为Circle和Rectangle的内部状态被共享。当然也可以将Color作为它们的内部状态,但在示例中Color作为外部状态使用的目的是为了使不同颜色的图形依然能够使用同一对象(只要它们的Text相同)。由于我们只需要一个享元工厂的实例,所以将其设计成单例模式(工作中通常也是这样做的)。通过运行结果发现,虽然我们通过工厂获取了两个Circle类的引用,但它们都指向同一块内存地址。通过这种方式,能够有效减少内存的开销。
-
复合享元模式
所谓复合享元,就是将若干个单纯享元使用组合模式组合成的非共享享元对象。当我们需要为多个内部状态不同的享元设置相同的外部状态时,可以考虑使用复合享元。复合享元本身不可被共享,但其可以分解成单纯享元对象。
public interface IShape { void Draw(ConsoleColor color, int x, int y); void Add(IShape shape); } public class Circle : IShape { private string _text = string.Empty; public Circle(string text) { this._text = text; Console.WriteLine($"开始实例化Circle [{text}]"); } public void Draw(ConsoleColor color, int x, int y) { Console.WriteLine($"Circle Draw [Text: {this._text}, Color: {color.ToString()}, 坐标x: {x} y: {y}]"); } public void Add(IShape shape) { throw new NotImplementedException("Sorry,I can not execute add function"); } } public class Rectangle : IShape { private string _text = string.Empty; public Rectangle(string text) { this._text = text; Console.WriteLine($"开始实例化Rectangle [{text}]"); } public void Draw(ConsoleColor color, int x, int y) { Console.WriteLine($"Rectangle Draw [Text: {this._text}, Color: {color.ToString()}, 坐标x: {x} y: {y}]"); } public void Add(IShape shape) { throw new NotImplementedException("Sorry,I can not execute add function"); } } public class ShapeComposite : IShape { private List<IShape> shapeList = new List<IShape>(); public ShapeComposite() { Console.WriteLine($"开始实例化ShapeComposite"); } public void Draw(ConsoleColor color, int x, int y) { Console.WriteLine($"ShapeComposite Draw"); foreach (var shape in this.shapeList) { shape.Draw(color, x, y); } } public void Add(IShape shape) { if(shape == null) { return; } this.shapeList.Add(shape); } } public class FlyweightFactory { private FlyweightFactory() { } private static FlyweightFactory _instance = null; private static object _sysLock = new object(); public static FlyweightFactory Instance { get { if(_instance == null) { lock (_sysLock) { if(_instance == null) { _instance = new FlyweightFactory(); } } } return _instance; } } private Dictionary<string, IShape> _shapePool = new Dictionary<string, IShape>(); public int PoolSize => this._shapePool.Count; public IShape CreateCircle(string text) { IShape shape = this._shapePool.ContainsKey(text) && this._shapePool[text] is Circle ? this._shapePool[text] : null; if(shape == null) { shape = new Circle(text); this._shapePool.Add(text, shape); } return shape; } public IShape CreateRectangle(string text) { IShape shape = this._shapePool.ContainsKey(text) && this._shapePool[text] is Rectangle ? this._shapePool[text] : null; if (shape == null) { shape = new Rectangle(text); this._shapePool.Add(text, shape); } return shape; } public IShape CreateComposite() { return new ShapeComposite(); } } static void Main(string[] args) { IShape circleA = FlyweightFactory.Instance.CreateCircle("很圆的圆形"); IShape circleB = FlyweightFactory.Instance.CreateCircle("很圆的圆形"); IShape rectangleA = FlyweightFactory.Instance.CreateRectangle("很方的长方形"); Console.WriteLine("--------------"); IShape shapCompositeA = FlyweightFactory.Instance.CreateComposite(); shapCompositeA.Add(circleA); shapCompositeA.Add(rectangleA); shapCompositeA.Draw(ConsoleColor.Yellow, 10, 130); Console.WriteLine("--------------"); IShape shapCompositeB = FlyweightFactory.Instance.CreateComposite(); shapCompositeB.Add(circleA); shapCompositeB.Add(circleB); shapCompositeB.Add(rectangleA); shapCompositeB.Draw(ConsoleColor.Blue, 25, 60); Console.WriteLine("--------------"); Console.WriteLine($"享元池长度:{FlyweightFactory.Instance.PoolSize}"); Console.WriteLine($"shapCompositeA与shapCompositeB是否指向同一内存地址:{object.ReferenceEquals(shapCompositeA, shapCompositeB)}"); Console.ReadKey(); }
通过复合享元可以确保其包含的所有单纯享元都具有相同的外部状态,而这些单纯享元的内部状态一般是不相等的(如果相等就没有使用价值了)。因为这些单纯享元是在复合享元被实例化之后注入进去的,也就意味着复合享元的内部状态(示例中的shapeList属性)是可变的,因此复合享元不可共享。
享元模式经常和组合模式结合起来表示一个层次结构(叶节点为可共享享元,根节点为非共享享元)。叶节点被共享的结果是,所有的叶节点都不能存储父节点的引用,而父节点只能作为外部状态传给叶节点。
模式补充
-
string的暂留机制
提到享元模式,就不得不提.Net中string类型(不包含StringBuilder)的暂留机制。与享元模式类似,CLR(公共语言运行时)在其内部维护了一个string类型的缓存池,用来存储用户创建的string类型引用。一般地,在程序运行的过程中,当用户创建一个string时,CLR会根据这个string的Hash Code在缓存池中查找相同的string对象。找到则复用这个string对象。没找到则创建这个对象并将其写入缓存池中。String类中也提供了Intern函数来帮助我们主动在缓存池中检索与该值相等的字符串(注意,该函数对比的是string的值而非该值的Hash Code。参考String.Intern)。验证方式如下。
static void Main(string[] args) { string a = "test"; string b = "test"; string c = "TEST".ToLower(); string d = "te" + "st"; string x = "te"; string y = "st"; string e = x + "st"; string f = x + y; Console.WriteLine(object.ReferenceEquals(a, b)); //True; Console.WriteLine(object.ReferenceEquals(a, c)); //False; Console.WriteLine(object.ReferenceEquals(a, string.Intern(c))); //True; Console.WriteLine(object.ReferenceEquals(a, d)); //True; Console.WriteLine(object.ReferenceEquals(a, e)); //False; Console.WriteLine(object.ReferenceEquals(a, f)); //False; Console.WriteLine(object.ReferenceEquals(e, f)); //False; Console.ReadKey(); }
-
享元与单例的区别
单例模式:类级别的单例,即一个类只能有一个实例;
享元模式:对象级别的单例,即一个类可拥有多个实例,且多个变量引用同一实例;
在单例模式中类的创建是由类本身去控制的,而类的创建逻辑并不属于这个类本身的业务逻辑,并且单例模式是严格控制其在所有线程中只能存在一个实例。而在享元模式中,并不限制这个类的实例数量,只是保证对象在同一内部状态下只存在一个实例,并且它的实例化过程由享元工厂控制。
总结
当系统中存在大量相同或相似的对象时,使用享,模式能够有效减少对象的创建,从而达到提高性能、减少内存开销的效果。享元的内部状态越少可复用的条件也就越少,类的复用次数也就越多,但该模式的难点在于如何合理的分离对象的内部状态和外部状态。对象的内部状态与外部状态的分离也意味着程序的逻辑更加复杂化。
以上,就是我对享元模式的理解,希望对你有所帮助。
示例源码:https://gitee.com/wxingChen/DesignPatternsPractice
系列汇总:https://www.cnblogs.com/wxingchen/p/10031592.html
本文著作权归本人所有,如需转载请标明本文链接(https://www.cnblogs.com/wxingchen/p/10078622.html)