C#设计模式学习笔记:(5)原型模式
一、引言
很多人说原型设计模式会节省机器内存,他们说是拷贝出来的对象是原型的复制,不会使用内存。我认为这是不对的,因为拷贝出来的每一个对象都是实际存在的,每个对象都有自己独立的内存地址且会被GC回收。如果就浅拷贝来说,可能会公用一些字段(引用类型),但深拷贝是不会的。所以说原型设计模式会提高内存使用率是不一定的,具体还要看当时的设计,如果拷贝出来的对象缓存了,每次使用的是缓存的拷贝对象,那就另当别论,再说该模式本身解决的不是内存使用率的问题。
附:浅复制与深复制的区别
浅复制一个对象:
1)如果这个对象(如int age=18 )是值类型,则得到的对象是一个全新的值类型对象(新的内存地址);
2)如果这个对象是引用类型(如class Person):
I、这个对象中的值类型(如person.Age=18)是一个全新的值类型对象(新的内存地址);
II、这个对象中的引用类型是公用的(同一内存地址),当原始引用类型的值变化时,新生成对象的引用类型的值也会跟着变化。
深复制一个对象:
无论之前这个对象是值类型还是引用类型,得到的新对象都是一个全新的对象(新的内存地址)。
在软件系统中,当创建一个类的实例的过程很昂贵或很复杂,并且我们需要创建多个这样的类的实例时,如果用new操作符去创建时,会增加创建的复杂度与客户代码的耦合度。如果采用工厂方法模式来创建这样的实例对象的话,随着产品类的不断增加,导致子类的数量不断增多,也导致了相应工厂类的增加,系统复杂程度随之增加,所以此时使用工厂方法模式来封装类的创建过程并不合适。
由于每个类的实例都是相同的(这个相同指的是类型相同,但是每个实例的状态参数会有不同,如果状态数值也相同就没意义了),有一个这样的对象就可以了。当我们需要多个相同的类实例时,可以通过对原来对象拷贝一份来完成创建,这个思路正是原型模式的实现方式。
二、原型模式介绍
原型模式:英文名称--Prototype Pattern;分类--创建型。
2.1、动机(Motivate)
在软件系统中,经常面临着“某些结构复杂的对象”的创建工作,由于需求的变化,这些对象经常面临着剧烈的变化,但是它们却拥有比较稳定一致的接口。如何应对这种变化?如何向“客户程序(使用这些对象的程序)”隔离出“这些易变对象”,从而使得“依赖这些易变对象的客户程序”不随着需求改变而改变?
2.2、意图(Intent)
使用原型实例指定创建对象的种类,然后通过拷贝这些原型来创建新的对象。--《设计模式》Gof
2.3、结构图(Structure)
2.4、模式的组成
从上图可以看出,在原型模式的结构图有以下角色:
1)原型类(Prototype):原型类,声明一个Clone自身的接口。
2)具体原型类(ConcretePrototype):实现一个Clone自身的操作。
在原型模式中,Prototype通常提供一个包含Clone方法的接口,具体的原型ConcretePrototype使用Clone方法完成对象的创建。
2.5、原型模式的具体实现
《大话西游之大圣娶亲》这部电影,里面有这样一个场景:牛魔王使用无敌牛虱大战至尊宝,至尊宝的应对之策就是--从脑后拔下一撮猴毛,吹了口仙气,无数猴子猴孙现身来大战牛魔王的无敌牛虱。至尊宝的猴子猴孙就是该原型模式的最好体现,至尊宝创建自己的一个副本,不用还要重新孕育五百年,然后出世、再学艺,最后再来和老牛大战,假如这样的话,估计黄花菜都凉了。至尊宝有3根救命猴毛,轻轻一吹,想要多少个自己就有多少个,方便、快捷。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
class Program { /// <summary> /// 抽象原型,定义了原型本身所具有特征和动作,该类型就是至尊宝。 /// </summary> public abstract class Prototype { //战斗--保护师傅 public abstract void Fight(); //化缘--不要饿着师傅 public abstract void BegAlms(); //吹口仙气--变一个自己出来 public abstract Prototype Clone(); } /// <summary> /// 具体原型,例如:行者孙A,他只负责与从天界宠物下界的妖怪战斗和化缘斋饭食。 /// </summary> public sealed class MonkeyKingPrototype : Prototype { //战斗--保护师傅 public override void Fight() { Console.WriteLine("七十二变,集万千武艺于一身。"); } //化缘--不要饿着师傅 public override void BegAlms() { Console.WriteLine("阿弥陀佛!施主,请施舍点饭食。"); } //吹口仙气--变一个自己出来 public override Prototype Clone() { return (MonkeyKingPrototype)MemberwiseClone(); } } /// <summary> /// 具体原型,例如:孙行者B,他只负责与自然界修炼成妖的妖怪战斗和化缘水果。 /// </summary> public sealed class NewskyPrototype : Prototype { //战斗--保护师傅 public override void Fight() { Console.WriteLine("七十二变,集万千武艺于一身。"); } //化缘--不要饿着师傅 public override void BegAlms() { Console.WriteLine("阿弥陀佛!施主,请施舍点水果。"); } //吹口仙气--变一个自己出来 public override Prototype Clone() { return (NewskyPrototype)MemberwiseClone(); } } static void Main(string[] args) { #region 原型模式 Prototype monkeyKing = new MonkeyKingPrototype(); Prototype monkeyKing1 = monkeyKing.Clone(); Prototype monkeyKing2 = monkeyKing.Clone(); Prototype newsky = new NewskyPrototype(); Prototype newsky1 = newsky.Clone(); Prototype newsky2 = newsky.Clone(); //孙行者A打妖怪 monkeyKing1.Fight(); //孙行者B去化缘 newsky2.BegAlms(); Console.Read(); #endregion } }
运行结果如下:
三、原型模式的实现要点
Prototype模式同样用于隔离类对象的使用者和具体类型(易变类)之间的耦合关系,它同样要求这些“易变类”拥有“稳定的接口”。
Prototype模式对于“如何创建易变类的实体对象”(创建型模式除了Singleton模式以外,都是用于解决创建易变类的实体对象的问题的)采用“原型克隆”的方法来做,它使得我们可以非常灵活地动态创建“拥有某些稳定接口”的新对象——所需工作仅仅是注册一个新类的对象(即原型),然后在任何需要的地方不断地Clone。
Prototype模式中的Clone方法可以利用.NET中的Object类的MemberwiseClone()方法或者序列化来实现深拷贝。
3.1、原型模式的优点
1)原型模式向客户隐藏了创建新实例的复杂性。
2)原型模式允许动态增加或较少产品类。
3)原型模式简化了实例的创建结构,工厂方法模式需要有一个与产品类等级结构相同的等级结构,而原型模式不需要这样。
4)产品类不需要事先确定产品的等级结构,因为原型模式适用于任何的等级结构。
3.2、原型模式的缺点
1)每个类必须配备一个克隆方法。
2)配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。
3.3、原型模式的使用场景
1)资源优化场景
类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。
2)性能和安全要求的场景
通过new产生一个对象需要非常繁琐的数据准备或访问权限时,则可以使用原型模式。
3)一个对象多个修改者的场景
一个对象需要提供给其它对象访问而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过clone的方法创建一个对象,然后由工厂方法提供给调用者。
四、.NET中原型模式的实现
在.NET中,微软已经为我们提供了原型模式的接口实现,该接口就是ICloneable。其实这个接口就是抽象原型,提供克隆方法,相当于与上面代码中Prototype抽象类,其中的Clone()方法实现原型模式。如果想自定义的类具有克隆的功能,首先需要在类定义时实现ICloneable接口的Clone方法。
namespace System { [ComVisible(true)] public interface ICloneable { object Clone(); } }
其实在.NET中实现了ICloneable接口的类有很多,如下图所示(只截取了部分,可以用ILSpy反编译工具进行查看):
五、总结
到本篇为止,所有的创建型设计模式就写完了。学习设计模式应该是一个循序渐进的过程,当我们写代码的时候不要一上来就用什么设计模式,而是通过重构来使用设计模式。
下面总结一下创建型的设计模式:
单例模式解决的是实体对象个数的问题。除了单例模式之外,其它的创建型模式解决的都是new所带来的耦合关系。工厂方法模式、抽象工厂模式、建造者模式都需要一个额外的工厂类来负责实例化“易变对象”,而原型模式则是通过原型(一个特殊的工厂类把工厂和实体对象耦合在一起了)来克隆“易变对象”。如果遇到“易变类”,起初的设计通常从工厂方法模式开始,当遇到更多的复杂变化时,再考虑重构为其他三种工厂模式(抽象工厂模式、建造者模式、原型模式)。
一般来说,如果可以使用工厂方法模式,那么一定可以使用原型模式,但是原型模式的使用情况一般是在类比较容易克隆的条件之上。如果是每个类的实现都比较简单,只需要实现MemberwiseClone而没有引用类型的深拷贝,那么就更加适合了。