5评论

读书笔记-设计模式-可复用版-Prototype 原型模式

Mitty 2019-02-21 4k浏览

在书中,首先讲到的第一个设计模式是创建型的Abstract Factory 抽象工厂,并且又提到了Abstract Factory通常可以使用Prototype进行替换,他们也可以一起使用,并且和Singleton以及Factory Method都有关系


既然是同是创建型的,一定是存在诸多关联的


那么在讲解Abstract Factory之前,有必要先了解下Prototype,Singleton,Factory Method三个设计模式,然后再去学习Abstract Factory会更容易理解一些


Prototype原型模式


是创建型的一种,提到Prototype原型模式,首先要想到的就是“克隆”(Clone),其次是浅拷贝(ShadowCopy)和深拷贝(DeepCopy),复用性,避免重复造轮子,节省构建时间


概念:


通过克隆(Clone)原型来创建新的对象


这里的原型指的是我们要克隆的实例(对象)(通常已经经过初始化,一系列计算之后)


所有的原型都有一个Clone操作,用于Clone自身,来创建新的对象。


这个Clone操作在Java和C#当中,都是以接口的形式存在


Java中是Cloneable接口,C#中是ICloneable接口


接口中只有一个方法:


object Clone();


我们只要去实现这个方法就可以了


在Java和C#之前的C++,更为底层的语言,则是通过拷贝构造函数实现,但通常也是定义纯虚函数Clone实现



下面是书中Prototype模式的结构图:



Client代表我如何使用Prototype模式

下面的p=prototype->Clone(),实例p调用Clone()接口,产生新的实例。


和Client平行的Prototype表示一个Clone接口,包含一个方法Clone(),就如上面提到的Java和C#那样


ConcretePrototype1和ConcreteProtype2 是实现了Prototype接口的两个具体类。


简要的代码如下:


public interface ICloneable{

object Clone();


public class Keyboard :ICloneable{

public override object Clone()

{......}


public class Mouse:ICloneable{

public override object Clone()

{....}


Client(具体使用代码示例):


Keyboard keyboard1 = new Keyboard();

Keyboard keyboard2 = keyboard1.Clone();


Mouse mouse1 = new Mouse();

Mouse mouse2 = mouse1.Clone();



原型接口中的Clone()是不带参数的,因为参数是不定的,由需求而定,并且带参数会破坏统一性,我们通常是Clone()后的对象,再进行指定字段的赋值操作。


比如:

Keyboard keyboard1 = new Keyboard();

Keyboard keyboard2 = keyboard1.Clone();

keyboard2.Initialize(xxx,xxx);


浅拷贝(ShadowCopy)深拷贝(DeepCopy)


浅拷贝和深拷贝只有在存在“引用”类型的时候,才有区别


值类型在赋值时,会产生一个类型的副本,这样互不影响,修改其中一个变量的值,并不会影响另外一个变量


引用类型则不同,引用类型其实由两部分组成,一个是引用部分,另一个是引用所指向的内存地址。


在引用类型赋值时,实际上是复制的引用本身,这样会导致两个引用对象指向了同一块内存地址,这样,如果其中一个修改或是释放,另一个引用也会受到影响,这在C++当中就叫野指针,会引起内存的泄露,但在Java,C#这些运行在“环境”(虚拟机和CLR)的语言,则不用担心内存泄露的问题,但会引起逻辑上的错误,这不是我们需要的结果


深拷贝也就是为了解决这个问题,引用类型在拷贝的时候,要在堆内存创建新的空间,并将值复制过去


所以,如果要克隆的对象,只包括值类型,那么使用浅拷贝和深拷贝是没有区别的,但如果存在引用类型,则就需要进行深拷贝的处理


string是引用类型,但他比较特殊,在堆内存中会有单独的区域用于存放字符串,一般叫字符串池,它具有值类型的特点,比如:


string a = "hello";

string b = a;

b = "hello world";

Debug.Log(a);


b指向了a,b修改了,并不会影响a,他会指向一个新的内存地址


浅拷贝(ShadowCopy)的例子:

因为克隆操作比较常用,所以Java和C#语言都提供了成员的逐一复制函数,C#中




Memberwise译为逐一复制,即浅克隆,引用类型会复制引用本身,并不会分配新的内存地址


但可以确定的是MemeberwiseClone会在堆内存中分配新的内存空间,然后进行逐一的复制,但如果复制的成员中包含了引用类型,并不会“智能”的为此再分配内存空间


public class Panel:ICloneable{

  public int depth;

  public int sortOrder;

  public string name;

  public object Clone()

  {

    return this.MemberwiseClone();

  }

  public override string ToString()

  {

    return "depth="+depth+",sortorder="+sortOrder+",name="+name;

  }

测试代码:

Panel panel1 = new Panel();

panel1.depth = 1;

panel1.sortOrder = 1;

panel1.name = "panel1";

Panel panel2 = panel1.Clone() as Panel;

panel2.name = "panel2";

Debug.Log(panel2.ToString());


通过克隆原型(panel1)创建新的对象panel2

panel2的修改并不会影响panel1


但如果在Panel中添加引用类型,问题就出现了:


public class Widget{

  public int id;

  public string name;

  public override string ToString()

  {

    return "id="+id+",name="+name;

  }


public class Panel:ICloneable{

  public int depth;

  public int sortOrder;

  public string name;

  public Widget widget;

  public object Clone()

  {

    return this.MemberwiseClone();

  }

  public override string ToString()

  {

    return "depth="+depth+",sortorder="+sortOrder+",name="+name+",widget="+widget.ToString();

  }

  public object Clone()

  {

    return this.MemberwiseClone();

  }

  public override string ToString()

  {

    return "depth="+depth+",sortorder="+sortOrder+",name="+name+",widget="+widget.ToString();

  }


测试代码:

Panel panel1 = new Panel();

panel1.depth = 1;

panel1.sortOrder = 1;

panel1.name = "panel1";

panel1.widget = new Widget();

panel1.widget.id = 1;

panel1.widget.name = "widget1";

Panel panel2 = panel1.Clone() as Panel;

panel2.name = "panel2";

panel2.widget.id = 2;

panel2.widget.name = "widget2";

Debug.Log(panel1.ToString());

panel2.widget.id.=2;

panel2.widget.name = "widget2";

会导致panel1中的widget变量也被修改了,这是浅拷贝问题所在,这里需要由深拷贝解决



深拷贝(DeepCopy)的例子:


继续沿用上面的例子,在上面提到过一句话:


但可以确定的是MemeberwiseClone会在堆内存中分配新的内存空间,然后进行逐一的复制,但如果复制的成员中包含了引用类型,并不会“智能”的为此再分配内存空间


所以只要需要让引用类型,自己再调用一次Clone即可


让Widget类实现Clone接口,并在Panel的Clone中做修改:


public class Widget:ICloneable{

  public int id;

  public string name;

  public object Clone()

  {

    return this.MemberwiseClone();

  }

  public override string ToString()

  {

    return "id="+id+",name="+name;

  }


public class Panel:ICloneable{

  public int depth;

  public int sortOrder;

  public string name;

  public Widget widget;

  public object Clone()

  {

    Panel newobj = this.MemberwiseClone() as Panel;

    newobj.widget = this.widget.Clone() as Widget;

    return newobj;

  }

  public override string ToString()

  {

    return "depth="+depth+",sortorder="+sortOrder+",name="+name+",widget="+widget.ToString();

  }


测试代码和上面是一样的


Panel中对Clone做了如下修改:

  public object Clone()

  {

    Panel newobj = this.MemberwiseClone() as Panel;

    newobj.widget = this.widget.Clone() as Widget;

    return newobj;

  }


newobj.widget = this.widget.Clone() as Widget;

单独的对widget进行Clone函数的调用


这样就可以解决引用类型指向同一地址带来的各种问题,但这只是一种解决方案,很难被实际应用,因为实际应用中的类,结构要复杂得很多,一个类中可能包含了多个引用类型,引用类型中也会有其它引用类型,并且也会存在循环引用的情况,而且也会针对不同的类进行Clone操作,工作量是巨大的,并且很不容易维护


所以,通过针对这种复杂类结构进行Clone操作,在C#中,可以通过序列化和反序列化实现(我们暂不考虑性能,因为涉及到反射,不建议大量频率的使用)


代码就简单很多了,需要做如下修改:


声明Widget和Panel类为可序列化

在类声明的上面添加特性:

[System.Serializable]


Widget不需要再继承ICloneable接口,实现Clone方法


在Panel的Clone方法修改如下:


public object Clone()

  {

    using (MemoryStream stream = new MemoryStream())

    {

      BinaryFormatter bf = new BinaryFormatter();

      bf.Serialize(stream, this);

      stream.Position = 0;

      return bf.Deserialize(stream) as Panel;

    }

  }


将原型序列化成字节流,保存在内存中,再从内存中,把字节流转换成对象


使用场景:

学习设计模式最重要的是了解他的使用场景,通过以上的解释,其实已经可以知道原型的具体作用


当我们需要创建多个对象的时候,对象和对象之间通常存在很多的相似性,比如敌人,可能只是某几个数值不同,其它都是相同的,如果对象的构建比较复杂:


构造函数要初始化的内容多

读取IO

进行一系列状态数值的计算


那么我们每一次构建都会有比较大的消耗


通过原型模式可以直接克隆一份,省去了上面的消耗,克隆出来的对象,我们再做针对具体的区别去设置


在游戏中,比如我当前的玩家一直在成长,中间经历了升级,强化等等状态数值的变化,我后来学到了一个新的技能,分身术,我需要产生我自身的多个副本,就需要使用克隆,我当前的角色就是一原型,克隆原型来产生一个或多个具有相同数值和状态的实例


因为实际对象的复杂度很高,手动进行赋值是不现实的


在Unity当中,动态的创建GameObject就应用到了Clone机制,比如下面这样:


GameObject obj = GameObject.Instantiate(Resources.Load("xxxxx")) as GameObject;

从本地加载Prefab到内存中,这个Prefab可以任何对象,比如敌人,角色等等


GameObject.Instantiate支持原型模式,我们可以通过克隆上面的obj,来创建新的实例

GameObject obj1 = GameObject.Instantiate(obj) as GameObject;


原型管理器:

在书中有提到过原型管理器,这通常是我们实际使用中,有克隆需求的类比较多,通过hashtable关联列表进行管理,方便我们快速 的查询,比如简单工厂中,也可以使用关联列表

实现,避免不断新增的switch case


Prototype在游戏中的应用,推荐游戏设计模式中的一篇文章

https://gpp.tkchu.me/prototype.html


里面关于Json保存敌人数据那里,说明很明白,归根结底,原型模式,也是提高了复用性,,避免每次都从0开始


参考资料:

1.设计模式-可复用 面向对象 软件基础

2.大话设计模式

3.http://gameprogrammingpatterns.com/contents.html


感谢您的阅读, 如文中有误,欢迎指正,共同提高 


欢迎关注我的技术分享的微信公众号,Paddtoning帕丁顿熊,期待和您的交流





本文作者

Mitty

欢迎关注我的技术分享微信公众号:PaddingtonCoder (Paddington帕丁顿熊,很喜欢这个名字,第一次出国就是英国,很意外的机会,了解了一点英国的历史,知道了帕丁顿熊,看了帕丁顿熊的电影,来到了伦敦的帕丁顿地铁站,随处可见肥肥的鸽子总是抢镜......很有趣儿)

腾讯游戏学院公众号