今天是国庆节,祝大家国庆节快乐身体健康,在我们面向对象设计过程中,我们常常会面临着对象实例过多的问题,如果对象实例过多这将是我们系统性能提高的一个瓶颈。假设我们要设计一个星空场景,现在我们需要实例星星对象,我们可以实例每一颗星星,但随着我们实例星星对象增多整个场景就越来越慢了,如果你实例了1000+颗星星要你去维护,这可是一个吃力不讨好的工作。我们必须找到一个合适的方法解决以上问题,这就是今天要介绍的享元模式(Flyweight)。
图1享元模式(Flyweight)结构图
享元模式(Flyweight):运用共享的技术有效地支持大量细粒度的对象。
抽象享元角色(Flyweight):此角色是所有的具体享元类的超类,为这些类规定出需要实现的公共接口或抽象类。那些需要外部状态(External State)的操作可以通过方法的参数传入。抽象享元的接口使得享元变得可能,但是并不强制子类实行共享,因此并非所有的享元对象都是可以共享的。
具体享元(ConcreteFlyweight)角色:实现抽象享元角色所规定的接口。如果有内部状态的话,必须负责为内部状态提供存储空间。享元对象的内部状态必须与对象所处的周围环境无关,从而使得享元对象可以在系统内共享。有时候具体享元角色又叫做单纯具体享元角色,因为复合享元角色是由单纯具体享元角色通过复合而成的。
复合享元(UnsharableFlyweight)角色:复合享元角色所代表的对象是不可以共享的,但是一个复合享元对象可以分解成为多个本身是单纯享元对象的组合。复合享元角色又称做不可共享的享元对象。这个角色一般很少使用。
享元工厂(FlyweightFactoiy)角色:本角色负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当地共享。当一个客户端对象请求一个享元对象的时候,享元工厂角色需要检查系统中是否已经有一个符合要求的享元对象,如果已经有了,享元工厂角色就应当提供这个已有的享元对象;如果系统中没有一个适当的享元对象的话,享元工厂角色就应当创建一个新的合适的享元对象。
客户端(Client)角色:本角色还需要自行存储所有享元对象的外部状态。
内部状态与外部状态:在享元对象内部并且不会随着环境改变而改变的共享部分,可以称之为享元对象的内部状态,反之随着环境改变而改变的,不可共享的状态称之为外部状态。
现在让我们通过一个面向对象的文本编辑器设计来说明享元模式的应用。假设我们要设计一个文本编辑器,而且它必须创建字符对象来表示文档中的每个字符,现在让我们考虑字符对象保持什么信息呢?如:字体、字体大小和位置等等信息。
一个文档通常包含许多字符对象,它们需要大容量的内存。值得我们注意的是一般字符都是由数字、字母和其他字符组成的(它们是固定的,可知的),这些字符对象可以共享字体和字体大小等信息,现在它们专有属性只剩下位置了,每个字符对象只需保持它们在文档中的位置就OK了,通过分析我们已经降低了编辑器的内存需求。
图2享元模式(Flyweight)共享对象
/// <summary> /// The 'Flyweight' class. /// </summary> public class Character { // intrinsic state protected char _symbol; protected int _size; protected string _font; // extrinsic state protected Position _position; public void Display(Position position) { Console.WriteLine( String.Format("Symbol: {0} Size: {1} Font: {2} Position: {3} {4}", _symbol, _size, _font, position._x, position._y)); } }
现在我们定义了一个字符享元类,其中符合、字体和字体大小都是内部状态,而位置则是外部状态。
/// <summary> /// A 'ConcreteFlyweight' class /// </summary> public class CharacterA : Character { public CharacterA() { _symbol = 'A'; _size = 10; _font = "宋体"; //_position = new Position(0, 1); } }
接着我们定义具体字符A的享元类,并且对内部状态符号、字体和字体大小进行初始化,而且其他字符B到Z享元类都类似。
图3具体享元模式(ConcreteFlyweight)设计
/// <summary> /// The 'FlyweightFactory' class /// </summary> public class CharacterFactory { // Keeps the character object by specifying key/value. private Dictionary<char, Character> _characters = new Dictionary<char, Character>(); public Character this[char key] { get { Character character = null; // Checked the character whether existed or not, // if the character existed, then directly returns, // otherwise, instantiates a character object. if (_characters.ContainsKey(key)) { character = _characters[key]; } else { string name = this.GetType().Namespace + "." + "Character" + key.ToString(); character = Activator.CreateInstance( Type.GetType(name)) as Character; _characters.Add(key, character); } return character; } } }
现在我们定义了一间字符工厂,通过一个Dictionary<Tkey, Tvalue>来保存字符对象,使用字符值来查找字符对象是否已经创建了,如果查找的字符对象已经存在,那么直接返回该对象,反之就创建字符对象实例。
/// <summary> /// The client. /// </summary> /// <param name="args">The args.</param> [STAThread] static void Main(string[] args) { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new FrmFlyweight()); string text = "ABZABBZZ"; char[] letters = text.ToCharArray(); var characterFactory = new CharacterFactory(); // Creates random position ranges 0 to 100. var rd = new Random(); foreach (char c in letters) { Character character = characterFactory[c]; var p = new Position(rd.Next(0, 100), rd.Next(0, 100)); character.Display(p); } Console.ReadKey(); }
图4享元模式(ConcreteFlyweight)测试结果
接着让我们实现一个享元模式的绘图程序,假设我们的程序要画各种各样的圆,而且圆的属性有形状,位置和颜色,其中形状和颜色是内部状态,而位置是外部状态。
设计分析:
1.提供一个抽象类Shape,让具体的形状如:Circle继承于它
2.定义一个位置结构图记录每个图形的位置
3.设计一间享元图形工厂用来创建图形对象
以上就是我们的享元模式的绘图程序的设计,接下来让我们实现享元模式的绘图程序吧!
/// <summary> /// Shape can be inherited by Circle, Retangle or triangle and so forth. /// Includes a color property and Draw methods. /// </summary> public abstract class Shape { public Color Color { get; set; } public abstract void Draw(Graphics graphics, Position position); }
上述示意代码定义了一个抽象类Shape,我们的具体图形都必须继承于该类。
/// <summary> /// Circle implements Shape. /// </summary> public class Circle : Shape { public Circle(Color color) { Color = color; } /// <summary> /// Draws circle with the specified graphics and position. /// </summary> /// <param name="graphics">The graphics.</param> /// <param name="position">The position of circle.</param> public override void Draw(Graphics graphics, Position position) { var pen = new Pen(Color); graphics.DrawEllipse(pen, position.X - position.R, position.Y - position.R, position.R, position.R); } }
接着我们定义具体图形类Circle,它实现Draw()方法通过Graphics调用DrawEllipse()方法来实现画圆。
/// <summary> /// Generate the position of concrete shape. /// </summary> public struct Position { private int _x; private int _y; private int _r; public Position GetPosition(Form form) { var rd = new Random(); _x = rd.Next(0, form.Width); _y = rd.Next(0, form.Height); float r = _x < _y ? _x : _y; _r = rd.Next(0, (int)r); return this; } public Position(Graphics graphics, int x, int y, int r) { if (x > graphics.DpiX) throw new ArgumentOutOfRangeException("x"); if (y > graphics.DpiY) throw new ArgumentOutOfRangeException("y"); if (r > graphics.DpiY && r > graphics.DpiX) throw new ArgumentOutOfRangeException("r"); _x = x; _y = y; _r = r; } public int X { get { return _x; } } public int Y { get { return _y; } } public int R { get { return _r; } } }
接着我们定义享元工厂负责创建图形对象,如果图形对象不存在就创建该对象,反正直接返回该图形对象。
/// <summary> /// The flyweight factory /// Generates the instance of shape if object not exists, /// otherwish returns the object directly. /// </summary> public class ShapeFactory { // Saves the shape object in Dictionary<Color, Shape> private static readonly Dictionary<Color, Shape> Shapes = new Dictionary<Color, Shape>(); // Gets the object in Dictionray. public Shape this[Color key] { get { Shape shape = null; // if the object exists return directly. // otherwish generates anew one. if (Shapes.ContainsKey(key)) { shape = Shapes[key]; } else { shape = new Circle(key); Shapes.Add(key, shape); } return shape; } } }
现在我们已经完成了享元图形类,由于图形的外部状态包括位置和颜色,前面我们通过随机函数生成随机位置,我们要设计一个拾色板来提供用户选择自定义颜色。
由于时间的关系我们已经把拾色板的界面设置,接下来让我们实现拾色板的具体功能。
首先我们新建一个用户自定义控件命名为ColorPanel,接着我们要处理用户点击选择颜色的事件
// Sets the default color. private Color _color = Color.Black; public delegate void ColorChangedHandler(object sender, ColorChangedEventArgs e); public event ColorChangedHandler ColorChanged; /// <summary> /// Raises the <see cref="E:ColorChanged"/> event. /// </summary> /// <param name="e">The color changed event arguments.</param> protected virtual void OnColorChanged(ColorChangedEventArgs e) { if (null != ColorChanged) ColorChanged(this, e); }
上述示意代码定义了一个委托ColorChangedHandler,当颜色值发现改变时相应具体处理方法和一个事件ColorChangedHandler,其实事件是对委托的封装,犹如字段和属性的关系,具体委托和事件的介绍请参看这里和这里。
图6自定义事件
我们先介绍一下EventArgs这个的类型。其实这个类并没有太多的功能,它主要是作为一个基类让其他类去实现具体的功能和定义,当我们自定义事件参数时都必须继承于该类。
现在回到我们自定义事件参数ColorChangedEventArgs,其中包含初始化颜色值的方法和获取颜色值的属性。
/// <summary> /// The color changed event arguments. /// </summary> public class ColorChangedEventArgs : EventArgs { private readonly Color _color; /// <summary> /// Initializes a new instance of the <see cref="ColorChangedEventArgs"/> class. /// </summary> /// <param name="color">The color.</param> public ColorChangedEventArgs(Color color) { _color = color; } /// <summary> /// Gets the color. /// </summary> public Color Color { get { return _color; } } }
现在我们终于完成了拾色板的基本功能了,接着只需把拾色板控件添加到我们的应用程序中就OK了。
图6享元模式绘图程序界面
由于时间的关系我们已经把程序的界面设计好了,接下来让我们实现一系列的事件处理方法。
/// <summary> /// Handles the Click event of the btnDrawCircle control. /// </summary> /// <param name="sender">The source of the event.</param> /// <param name="e">The <see cref="System.EventArgs"/> /// instance containing the event data.</param> private void btnDrawCircle_Click(object sender, EventArgs e) { Graphics graphics = Graphics.FromImage(_drawArea); // Gets shape object with specified color in flyweight object. Shape shape = _factory[colorPanel1.Color]; shape.Draw(graphics, _position.GetPosition(this)); this.Invalidate(); } /// <summary> /// Handles the Click event of the btnClear control. /// </summary> /// <param name="sender">The source of the event.</param> /// <param name="e">The <see cref="System.EventArgs"/> /// instance containing the event data.</param> private void btnClear_Click(object sender, EventArgs e) { Graphics graphics = Graphics.FromImage(_drawArea); graphics.Clear(Color.SkyBlue); graphics.Dispose(); this.Invalidate(); }
上面我们定义了处理绘图点击方法和清除图形的方法,当用户选择颜色值时,我们的程序到享元工厂中获取该对象实例,这个对象可能是新建的,也可能是已经存在的。
图7绘图程序效果
本文通过给出享元模式的典型应用例子,来介绍了享元模式的具体应用,但享元模式在一般的开发中并不常用,而是常常应用于系统底层的开发,以便解决系统的性能问题。
适用性
Flyweight模式的有效性很大程度上取决于如何使用它以及在何处使用它。当以下情况都成立时使用Flyweight模式。
1) 一个应用程序使用了大量的对象。
2) 完全由于使用大量的对象,造成很大的存储开销。
3) 对象的大多数状态都可变为外部状态。
4) 如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象。
5) 应用程序不依赖对象标识。
优缺点
1)享元模式使得系统更加复杂。为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化。
2)享元模式将享元对象的状态外部化,而读取外部状态使得运行时间稍微变长。