学习设计模式第八 - 原型模式
本文摘取自TerryLee(李会军)老师的设计模式系列文章,版权归TerryLee,仅供个人学习参考。转载请标明原作者TerryLee。部分示例代码来自DoFactory。
概述
在软件系统中,有时候面临的产品类是动态变化的,而且这个产品类具有一定的等级结构。这时如果用工厂模式,则与产品类等级结构平行的工厂方法类也要随着这种变化而变化,显然不大合适。那么如何封装这种动态的变化?从而使依赖于这些易变对象的客户程序不随着产品类变化?
意图
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
UML
图1. Prototype模式UML图
参与者
这个模式涉及的类或对象:
-
Prototype
-
定义一个用于复制自身的接口
-
ConcretePrototype
-
实现一个复制自身的操作
-
Client
-
通过要求原型复制自身对象来创建一个新对象
适用性
例如其它创建性模式,原型模式对客户端隐藏了对象的创建。然而,其返回一个新的对象并且通过由一个原型或示例对象拷贝值来进行初始化,而不是创建一个未初始化的对象。原型模式在商业应用的构建中不常见,其更多用于特定类型的程序,如,计算机图形,CAD,GIS及计算机游戏。
原型模式创建已存在示例对象的拷贝。在.NET中实现这个的最佳方式是使用作为原型的对象内置的ICloneable接口。ICloneable接口的Clone方法返回原始对象的一个拷贝。
当实现拷贝功能时,你需要清楚两种不同类型的拷贝:深拷贝和浅拷贝。浅拷贝很容易但仅是拷贝对象本身的数据(位于栈上的值类型数据或引用类型数据的引用),而不包含原型对象引用的数据(位于托管堆上)。深拷贝会复制原型对象包括其中引用类型引用的所有对象。潜拷贝很容易实现因为Object基类有一个MemberwiseClone方法,其返回对象的一份浅拷贝。深拷贝的拷贝策略会更复杂 – 一些对象不会真正被拷贝(如线程,数据库链接等对象),同时需要留神循环引用。
在下列情况下,应当使用Prototype模式:
-
当一个系统应该独立于它的产品创建,构成和表示时。
-
当要实例化的类是在运行时刻指定时,例如,通过动态装载。
-
为了避免创建一个与产品类层次平行的工厂类层次时。
-
当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。
DoFactory GoF代码
原型模式中,同一个类的新对象通过复制已存在的对象来创建。
// Prototype pattern // Structural example using System; namespace DoFactory.GangOfFour.Prototype.Structural { class MainApp { static void Main() { // Create two instances and clone each ConcretePrototype1 p1 = new ConcretePrototype1("I"); ConcretePrototype1 c1 = (ConcretePrototype1)p1.Clone(); Console.WriteLine("Cloned: {0}", c1.Id); ConcretePrototype2 p2 = new ConcretePrototype2("II"); ConcretePrototype2 c2 = (ConcretePrototype2)p2.Clone(); Console.WriteLine("Cloned: {0}", c2.Id); // Wait for user Console.ReadKey(); } } // "Prototype" abstract class Prototype { private string _id; // Constructor public Prototype(string id) { this._id = id; } // Gets id public string Id { get { return _id; } } public abstract Prototype Clone(); } // "ConcretePrototype1" class ConcretePrototype1 : Prototype { // Constructor public ConcretePrototype1(string id) : base(id) { } // Returns a shallow copy public override Prototype Clone() { return (Prototype)this.MemberwiseClone(); } } // "ConcretePrototype2" class ConcretePrototype2 : Prototype { // Constructor public ConcretePrototype2(string id) : base(id) { } // Returns a shallow copy public override Prototype Clone() { return (Prototype)this.MemberwiseClone(); } } }
这个例子中,演示了通过复制已存在的Color对象来创建新的Color对象。
例子中涉及到的类与原型模式中标准的类对应关系如下:
-
Prototype – ColorPrototype
-
ConcretePrototype – Color
-
Client – ColorManager
// Prototype pattern // Real World example using System; using System.Collections.Generic; namespace DoFactory.GangOfFour.Prototype.RealWorld { class MainApp { static void Main() { ColorManager colormanager = new ColorManager(); // Initialize with standard colors colormanager["red"] = new Color(255, 0, 0); colormanager["green"] = new Color(0, 255, 0); colormanager["blue"] = new Color(0, 0, 255); // User adds personalized colors colormanager["angry"] = new Color(255, 54, 0); colormanager["peace"] = new Color(128, 211, 128); colormanager["flame"] = new Color(211, 34, 20); // User clones selected colors Color color1 = colormanager["red"].Clone() as Color; Color color2 = colormanager["peace"].Clone() as Color; Color color3 = colormanager["flame"].Clone() as Color; // Wait for user Console.ReadKey(); } } // "Prototype" abstract class ColorPrototype { public abstract ColorPrototype Clone(); } // "ConcretePrototype" class Color : ColorPrototype { private int _red; private int _green; private int _blue; // Constructor public Color(int red, int green, int blue) { this._red = red; this._green = green; this._blue = blue; } // Create a shallow copy public override ColorPrototype Clone() { Console.WriteLine("Cloning color RGB: {0,3},{1,3},{2,3}", _red, _green, _blue); return this.MemberwiseClone() as ColorPrototype; } } // Prototype manager class ColorManager { private Dictionary<string, ColorPrototype> _colors = new Dictionary<string, ColorPrototype>(); // Indexer public ColorPrototype this[string key] { get { return _colors[key]; } set { _colors.Add(key, value); } } } }
.NET优化代码实现了与上述示例相同的功能,但是使用了更多.NET的特性。没有函数实现代码的抽象类被接口代替。自定义的Color类中表示RGB的三个属性的类型由int变为byte以节省内存。ColorManager中的字典类,Key的由string变为枚举类型增强类型安全。在Color类中使用了.NET3.0的自动属性与对象初始化器,这些极大的减少了代码的行数。
ICloneable是一个内置的.NET原型接口。ICloneable需要类被层次序列化,示例中Serializable特性正是用来完成这个工作(注意,如果类的成员中有事件,其必须被标记为NonSerialized)。替代方案,可以使用反射查询ICloneable类中每一个成员。提示:当通过序列化或反射实现大量对象的复制时,始终要注意对性能产生的负面影响。
// Prototype pattern // .NET Optimized example using System; using System.Collections.Generic; using System.IO; using System.Runtime.Serialization.Formatters.Binary; namespace DoFactory.GangOfFour.Prototype.NETOptimized { class MainApp { static void Main() { var colormanager = new ColorManager(); // Initialize with standard colors colormanager[ColorType.Red] = new Color { Red = 255, Blue = 0, Green = 0 }; colormanager[ColorType.Green] = new Color { Red = 0, Blue = 255, Green = 0 }; colormanager[ColorType.Blue] = new Color { Red = 0, Blue = 0, Green = 255 }; // User adds personalized colors colormanager[ColorType.Angry] = new Color { Red = 255, Blue = 54, Green = 0 }; colormanager[ColorType.Peace] = new Color { Red = 128, Blue = 211, Green = 128 }; colormanager[ColorType.Flame] = new Color { Red = 211, Blue = 34, Green = 20 }; // User uses selected colors var color1 = colormanager[ColorType.Red].Clone() as Color; var color2 = colormanager[ColorType.Peace].Clone() as Color; // Creates a "deep copy" var color3 = colormanager[ColorType.Flame].Clone(false) as Color; // Wait for user Console.ReadKey(); } } // "ConcretePrototype" [Serializable] class Color : ICloneable { // Gets or sets red value public byte Red { get; set; } // Gets or sets green value public byte Green { get; set; } // Gets or sets blue channel public byte Blue { get; set; } // Returns shallow or deep copy public object Clone(bool shallow) { return shallow ? Clone() : DeepCopy(); } // Creates a shallow copy public object Clone() { Console.WriteLine("Shallow copy of color RGB: {0,3},{1,3},{2,3}",Red, Green, Blue); return this.MemberwiseClone(); } // Creates a deep copy public object DeepCopy() { var stream = new MemoryStream(); var formatter = new BinaryFormatter(); formatter.Serialize(stream, this); stream.Seek(0, SeekOrigin.Begin); object copy = formatter.Deserialize(stream); stream.Close(); Console.WriteLine("*Deep* copy of color RGB: {0,3},{1,3},{2,3}", (copy as Color).Red,(copy as Color).Green,(copy as Color).Blue); return copy; } } // Type-safe prototype manager class ColorManager { private Dictionary<ColorType, Color> _colors = new Dictionary<ColorType, Color>(); // Gets or sets color public Color this[ColorType type] { get { return _colors[type]; } set { _colors.Add(type, value); } } } // Color type enumerations enum ColorType { Red, Green, Blue, Angry, Peace, Flame } }
原型模式解说
我们考虑这样一个场景,假定我们要开发一个调色板,用户单击调色板上任一个方块,将会返回一个对应的颜色的实例,下面我们看看如何通过原型模式来达到系统动态加载具体产品的目的。很自然,我们利用OO的思想,把每一种颜色作为一个对象,并为他们抽象出一个公用的父类,如下图:
图2. 调色板程序产品对象模型图
实现代码:
public abstract class Color { public abstract void Display(); } public class RedColor : Color { public override void Display() { Console.WriteLine("Red's RGB Values are:255,0,0"); } } public class GreenColor : Color { public override void Display() { Console.WriteLine("Green's RGB Values are:0,255,0"); } }
客户程序需要某一种颜色的时候,只需要创建对应的具体类的实例就可以了。但是这样我们并没有达到封装变化点的目的,也许你会说,可以使用工厂方法模式,为每一个具体子类定义一个与其等级平行的工厂类,那么好,看一下实现:
图3. 使用工厂方法模式实现的调色板程序模型图
实现代码:
public abstract class ColorFactory { public abstract Color Create(); } public class RedFactory : ColorFactory { public override Color Create() { return new RedColor(); } } public class GreenFactory : ColorFactory { public override Color Create() { return new GreenColor(); } }
实现了这一步之后,可以看到,客户程序只要调用工厂方法就可以了。似乎我们用工厂方法模式来解决是没有问题的。但是,我们考虑的仅仅是封装了new变化,而没有考虑颜色的数量是不断变化的,甚至可能是在程序运行的过程中动态增加和减少的,那么用这种方法实现,随着颜色数量的不断增加,子类的数量会迅速膨大,导致子类过多,显然用工厂方法模式有些不大合适。
进一步思考,这些Color子类仅仅在初始化的颜色对象类别上有所不同。添加一个ColorTool这样的类,来参数化的它的实例,而这些实例是由Color支持和创建的。我们让ColorTool通过克隆或者拷贝一个Color子类的实例来创建新的Color,这个实例就是一个原型。如下图所示:
图4. 使用原型模式实现的调色板程序模型图
实现代码:
abstract class ColorPrototype { public abstract ColorPrototype Clone(); } class ConcteteColorPrototype : ColorPrototype { private int _red, _green, _blue; public ConcteteColorPrototype(int red, int green, int blue) { this._red = red; this._green = green; this._blue = blue; } public override ColorPrototype Clone() { //实现浅拷贝 return (ColorPrototype)this.MemberwiseClone(); } public void Display(string colorname) { Console.WriteLine("{0}'s RGB Values are: {1},{2},{3}", colorname, _red, _green, _blue); } } class ColorManager { Hashtable colors = new Hashtable(); public ColorPrototype this[string name] { get { return (ColorPrototype)colors[name]; } set { colors.Add(name, value); } } }
现在我们分析一下,这样带来了什么好处?首先从子类的数目上大大减少了,不需要再为每一种具体的颜色产品而定一个类和与它等级平行的工厂方法类,而ColorTool则扮演了原型管理器的角色。再看一下为客户程序的实现:
class Clinet { public static void Main(string[] args) { ColorManager colormanager = new ColorManager(); //初始化颜色 colormanager["red"] = new ConcteteColorPrototype(255, 0, 0); colormanager["green"] = new ConcteteColorPrototype(0, 255, 0); colormanager["blue"] = new ConcteteColorPrototype(0, 0, 255); colormanager["angry"] = new ConcteteColorPrototype(255, 54, 0); colormanager["peace"] = new ConcteteColorPrototype(128, 211, 128); colormanager["flame"] = new ConcteteColorPrototype(211, 34, 20); //使用颜色 string colorName = "red"; ConcteteColorPrototype c1 = (ConcteteColorPrototype)colormanager[colorName].Clone(); c1.Display(colorName); colorName = "peace"; ConcteteColorPrototype c2 = (ConcteteColorPrototype)colormanager[colorName].Clone(); c2.Display(colorName); colorName = "flame"; ConcteteColorPrototype c3 = (ConcteteColorPrototype)colormanager[colorName].Clone(); c3.Display(colorName); Console.ReadLine(); } }
可以看到,客户程序通过注册原型实例就可以将一个具体产品类并入到系统中,在运行时刻,可以动态的建立和删除原型。最后还要注意一点,在上面的例子中,用的是浅表复制。如果想做深复制,需要通过序列化的方式来实现。经过了上面的分析之后,我们再来思考下面的问题:
-
为什么需要Prototype模式?
引入原型模式的本质在于利用已有的一个原型对象,快速的生成和原型对象一样的实例。你有一个A的实例a(A a = new A();),现在你想生成和car1一样的一个实例b,按照原型模式,应该是这样:A b = a.Clone();而不是重新再new一个A对象。通过上面这句话就可以得到一个和a一样的实例,确切的说,应该是它们的数据成员是一样的。Prototype模式同样是返回了一个A对象而没有使用new操作。
-
引入Prototype模式带来了什么好处?
可以看到,引入Prototype模式后我们不再需要一个与具体产品等级结构平行的工厂方法类,减少了类的构造,同时客户程序可以在运行时刻建立和删除原型。
-
Prototype模式满足了哪些面向对象的设计原则?
依赖倒置原则:上面的例子,原型管理器(ColorManager)仅仅依赖于抽象部分(ColorPrototype),而具体实现细节(ConcteteColorPrototype)则依赖与抽象部分(ColorPrototype),所以Prototype很好的满足了依赖倒置原则。
图5. 原型模式满足依赖倒置原则
通过序列化实现深拷贝
要实现深拷贝,可以通过序列化的方式。抽象类及具体类都必须标注为可序列化的[Serializable],上面的例子加上深拷贝之后的完整程序如下:
using System; using System.Collections; using System.IO; using System.Runtime.Serialization.Formatters.Binary; [Serializable] abstract class ColorPrototype { public abstract ColorPrototype Clone(bool Deep); } [Serializable] class ConcteteColorPrototype : ColorPrototype { private int _red, _green, _blue; public ConcteteColorPrototype(int red, int green, int blue) { this._red = red; this._green = green; this._blue = blue; } public override ColorPrototype Clone(bool Deep) { if (Deep) return CreateDeepCopy(); else return (ColorPrototype)this.MemberwiseClone(); } //实现深拷贝 public ColorPrototype CreateDeepCopy() { ColorPrototype colorPrototype; MemoryStream memoryStream = new MemoryStream(); BinaryFormatter formatter = new BinaryFormatter(); formatter.Serialize(memoryStream, this); memoryStream.Position = 0; colorPrototype = (ColorPrototype)formatter.Deserialize(memoryStream); return colorPrototype; } public ConcteteColorPrototype Create(int red, int green, int blue) { return new ConcteteColorPrototype(red, green, blue); } public void Display(string colorname) { Console.WriteLine("{0}'s RGB Values are: {1},{2},{3}",colorname, _red, _green, _blue); } } class ColorManager { Hashtable colors = new Hashtable(); public ColorPrototype this[string name] { get {return (ColorPrototype)colors[name];} set {colors.Add(name, value);} } } class Client { public static void Main(string[] args) { ColorManager colormanager = new ColorManager(); //初始化颜色 colormanager["red"] = new ConcteteColorPrototype(255, 0, 0); colormanager["green"] = new ConcteteColorPrototype(0, 255, 0); colormanager["blue"] = new ConcteteColorPrototype(0, 0, 255); colormanager["angry"] = new ConcteteColorPrototype(255, 54, 0); colormanager["peace"] = new ConcteteColorPrototype(128, 211, 128); colormanager["flame"] = new ConcteteColorPrototype(211, 34, 20); //使用颜色 string colorName = "red"; ConcteteColorPrototype c1 = (ConcteteColorPrototype)colormanager[colorName].Clone(false); c1.Display(colorName); colorName = "peace"; ConcteteColorPrototype c2 = (ConcteteColorPrototype)colormanager[colorName].Clone(true); c2.Display(colorName); colorName = "flame"; ConcteteColorPrototype c3 = (ConcteteColorPrototype)colormanager[colorName].Clone(true); c3.Display(colorName); Console.ReadLine(); } }
实现要点
-
使用原型管理器,体现在一个系统中原型数目不固定时,可以动态的创建和销毁,如上面的举的调色板的例子。
-
实现克隆操作,在.NET中可以使用Object类的MemberwiseClone()方法来实现对象的浅表拷贝或通过序列化的方式来实现深拷贝。
-
Prototype模式同样用于隔离类对象的使用者和具体类型(易变类)之间的耦合关系,它同样要求这些"易变类"拥有稳定的接口。
效果
-
它对客户隐藏了具体的产品类,因此减少了客户知道的名字的数目。
-
Prototype模式允许客户只通过注册原型实例就可以将一个具体产品类并入到系统中,客户可以在运行时刻建立和删除原型。
-
减少了子类构造,Prototype模式是克隆一个原型而不是请求工厂方法创建一个,所以它不需要一个与具体产品类平行的Creator类层次。
-
Prototype模式具有给一个应用软件动态加载新功能的能力。由于Prototype的独立性较高,可以很容易动态加载新功能而不影响老系统。
-
产品类不需要非得有任何事先确定的等级结构,因为Prototype模式适用于任何的等级结构。
-
Prototype模式的最主要缺点就是每一个类必须配备一个克隆方法。而且这个克隆方法需要对类的功能进行通盘考虑,这对全新的类来说不是很难,但对已有的类进行改造时,不一定是件容易的事。
缺点
有时候对象的复制很复杂。
.NET中的应用
.NET Framework内置对原型模式的支持,如在对象序列化场景。比如说你有一个已经被序列化到持久存储,如磁盘或数据库,的原型对象。有了这个序列化的表示作为原型,你可以使用它来创建一个原始对象的拷贝。
总结
Prototype模式同工厂模式,同样对客户隐藏了对象的创建工作。但是,与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的,达到了"隔离类对象的使用者和具体类型(易变类)之间的耦合关系"的目的。