其他分享
首页 > 其他分享> > 读书笔记-设计模式-可复用版-Prototype 原型模式

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

作者:互联网

在书中,首先讲到的第一个设计模式是创建型的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开始

标签:widget,设计模式,name,读书笔记,Clone,panel1,Prototype,public,Panel
来源: https://blog.csdn.net/wangchewen/article/details/120923639