原型模式,不只是clone那么简单
原型模式的意图经常被误解为复制对象,本来我觉得设计模式完全无必要(也不太敢)写任何文章,GoF书里写的清晰准确,还有无数例子,但我看到过无数文章把原型模式彻彻底底地变成了从已有对象方便地复制一个新对象,所以决定趟趟浑水也来白话一篇,说的不对的地方欢迎大家拍砖,猛拍,狂拍,往死里拍……
在我自己废话之前,还是先抄GoF,意图:
用原型实例指定创建对象的种类,并且通过拷贝这些原型对象创建新的对象。
什么意思呢,我觉得应该把这个短语里面的形容词部分去掉:指定种类,创建对象。这样就容易理解多了,这个模式的意图,不是要复制一个一模一样的对象,而是要创建一个对象,用一个原型指定它的种类,拷贝只是一种手法,不是目的。
这个动机就不抄了,其实是个蛮清楚的例子,不过乐谱编辑器这种东西貌似离我们程序员生活远了点。
这个适用性还是比较实用的,抄一下:
当一个系统应该独立于它的产品创建、构成和表示时,要使用Prototype模式;以及
• 当要实例化的类是在运行时刻指定时,例如,通过动态装载;或者
• 为了避免创建一个与产品类层次平行的工厂类层次时;或者
• 当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。
“当一个系统应该独立于它的产品创建、构成和表示时,要使用Prototype模式”这句话就是原型模式应用的最广泛的场景。所为的一个系统,其实可能就是一个程序员负责编写的一个类、一个模块、一个库或者多层架构中的一层。当你尝试编写一个完全符合DIP的系统时,你可能无法避免要创建符合特定接口的新对象,但因为DIP原则不允许你依赖具体类型,完全无法决定要创建哪一个具体类,这个时候,原型模式提供了一个很讨巧的办法:“我要创建一个跟它类型一样的对象!”
后面三条代表了三种场景:“当要实例化的类是在运行时刻指定时,例如,通过动态装载”,这个场景是动态指定创建对象的类型,你要创建的对象可能跟用户输入或者其它运行时的信息有关,如果你不想在每个创建对象的地方都写上一组if else或者switch那么原型就是一个不错的选择。
“为了避免创建一个与产品类层次平行的工厂类层次时”,这个就是原型模式替代抽象工厂,实际上,原型模式就是把对象本身当作了工厂。
“当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。”,这一句稍微有点复杂,其实它也是一个非常corner的场景。有些类,属于它的对象数目是有限的,比如斯诺克用台球虽然有不同颜色,但红、黄、绿、棕、蓝、粉和黑以及白几种颜色,你绝不可能创建一个紫球。这个时候,可以用原型模式来创建对象,我们首先手动实例化8种颜色的球各一个,之后的创建就全都用复制的方式来进行。
接下来的效果是从别人那里copy来的(原址http://blog.csdn.net/coolstar/archive/2001/08/16/5772.aspx):
P r o t o t y p e有许多和Abstract Factory(3 . 1)和B u i l d e r(3 . 2)一样的效果:它对客户隐藏了具体的产品类,因此减少了客户知道的名字的数目。此外,这些模式使客户无需改变即可使用与特定应用相关的类。
下面列出P r o t o t y p e模式的另外一些优点。
1 ) 运行时刻增加和删除产品P r o t o t y p e允许只通过客户注册原型实例就可以将一个新的具体产品类并入系统。它比其他创建型模式更为灵活,因为客户可以在运行时刻建立和删除原型。
2 ) 改变值以指定新对象高度动态的系统允许你通过对象复合定义新的行为—例如,通过为一个对象变量指定值—并且不定义新的类。你通过实例化已有类并且将这些实例注册为客户对象的原型,就可以有效定义新类别的对象。客户可以将职责代理给原型,从而表现出新的行为。
这种设计使得用户无需编程即可定义新“类”。实际上,克隆一个原型类似于实例化一个类。P r o t o t y p e模式可以极大的减少系统所需要的类的数目。在我们的音乐编辑器中,一个G r a p h i c To o l类可以创建无数种音乐对象。
3) 改变结构以指定新对象许多应用由部件和子部件来创建对象。例如电路设计编辑器就是由子电路来构造电路的。为方便起见,这样的应用通常允许你实例化复杂的、用户定义的结构,比方说,一次又一次的重复使用一个特定的子电路。
P r o t o t y p e模式也支持这一点。我们仅需将这个子电路作为一个原型增加到可用的电路元素选择板中。只要复合电路对象将C l o n e实现为一个深拷贝( deep copy),具有不同结构的电路就可以是原型了。
4 ) 减少子类的构造Factory Method(3 . 3)经常产生一个与产品类层次平行的C r e a t o r类层次。P r o t o t y p e模式使得你克隆一个原型而不是请求一个工厂方法去产生一个新的对象。因此你根本不需要C r e a t o r类层次。这一优点主要适用于像C + +这样不将类作为一级类对象的语言。像S m a l l t a l k和Objective C这样的语言从中获益较少,因为你总是可以用一个类对象作为生成者。在这些语言中,类对象已经起到原型一样的作用了。
5) 用类动态配置应用一些运行时刻环境允许你动态将类装载到应用中。在像C + +这样的语言中,P r o t o t y p e模式是利用这种功能的关键。一个希望创建动态载入类的实例的应用不能静态引用类的构造器。而应该由运行环境在
载入时自动创建每个类的实例,并用原型管理器来注册这个实例(参见实现一节)。这样应用就可以向原型管理器请求新装载的类的实例,这些类原本并没有和程序相连接。E T + +应用框架[ W G M 8 8 ]有一个运行系统就是使用这一方案的。
P r o t o t y p e的主要缺陷是每一个P r o t o t y p e的子类都必须实现C l o n e操作,这可能很困难。例如,当所考虑的类已经存在时就难以新增C l o n e操作。当内部包括一些不支持拷贝或有循环引用的对象时,实现克隆可能也会很困难的。
这个实在太长,我就懒得解释了。当然我也觉得GoF这里讲的很清楚。
外一:《大话设计模式》中的原型模式讲得对吗?
我认为《大话设计模式》在这里偏离了原型模式的原意,下面一段代码摘自《大话设计模式》,使用原型模式之前代码
Resume a=new Resume("大鸟"); a.setpersonalinfo("男","29"); a.setworkexperience("1998-2000","xx公司"); //注意:Resume b=a;这其实是在传引用,而不是传值,这样做如同在b纸张上写着简历在a处一样,没有实际的内容的 Resume b=new Resume("大鸟"); a.setpersonalinfo("男","29"); a.setworkexperience("1998-2000","xx公司"); Resume c=new Resume("大鸟"); a.setpersonalinfo("男","29"); a.setworkexperience("1998-2000","xx公司"); a.display(); b.display(); c.display(); |
使用原型模式之后代码
Resume a=new Resume("大鸟"); a.setpersonalinfo("男","29"); a.setworkexperience("1998-2000","xx公司"); //注意:Resume b=a;这其实是在传引用,而不是传值,这样做如同在b纸张上写着简历在a处一样,没有实际的内容的 Resume b=(Resume)a.clone(); b.SetWorkExperience("1998-2000","yy公司"); Resume c=(Resume)a.Clone(); c.SetPersonalInfo("男","24"); a.display(); b.display(); c.display(); |
书中给出的描述是:“很好,这其实就是当年我手写简历时代的代码。三份简历需要三次实例化,你觉得这样客户端代码是不是很麻烦,如果要二十份,你就需要二十次实例化。”
“而且如果我写错了一个字,比如98年改成99年,那要改二十次。”
这个例子是原型模式吗?显然不是,它不符合上面任何一个适用性的场景。我们来逐句分析下文中提到的问题:
三份简历需要三次实例化,你觉得这样客户端代码是不是很麻烦
三次实例化对于客户端代码来说算是麻烦么?我觉得写三个new跟写一个new二个clone没什么本质区别
如果要二十份,你就需要二十次实例化。
clone解决了这个问题了吗?没有,只是把二十次实例化变成了一次实例化和十九次clone
如果写错了一个字,比如98年改成99年,那要改二十次。
其实这不是原型模式想要解决的问题,大概循环是个更好的主意……
任何时刻都不要忘记原型模式的意图:指定种类,创建对象。在这个例子中变量b和c的类型是Resume,这是一个具体类型,b所引用的对象的种类已经没有什么可以指定的余地了。所以判断一段代码是否在使用原型模式的一个重要标志就是复制之后引用它的目标变量类型是否是抽象类型。
外二:ICloneable是.net Framework为我们写好的原型接口吗?
很多人认为ICloneable是.net中为我们写好的原型接口,在原型模式的UML中,Prototype便是ICloneable,是这样吗?我的看法是,ICloneable与原型模式并无半点关系。若说有联系,就是Prototype可以顺便实现ICloneable。
原因来自上面的论述,Prototype应该是一个怎样的接口呢?假如你的代码是重构到原型模式的,Prototype这个类应该是在重构之前就已经存在的,但是它没有clone方法。当然它肯定有一个跟自己使命相关的名字,比如GoF例子里的Graphic,Prototype模式只是给它添加了一个clone方法,这样我们就可以从一个对象构造相同类型的另一个对象。
捎带一提,设计模式中的所有模式,都是隐含在代码中的一个结构,如果你的类系统中,某个类的命名跟使用的模式UML里面的代称完全一样,那一定是犯了为模式而模式的错误。
That’s all. Thanks.