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