原型(Prototype)模式用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
简单举个例子,游戏里面有动物类型,定义如下:
public class AnimalPrototype
{
public string name { get; set;}
public int hp{get;set;}
public AnimalPrototype(string name_)
{
name = name_;
}
public override string ToString()
{
return string.Format ("[Animal: name={0}, hp={1}]", name, hp);
}
}
创建时:
AnimalPrototype wolverine = new AnimalPrototype ("wolverine");
wolverine.hp = 100;
这里只有两个属性,name和hp,实际情况可能有十几个或者更多的属性(例如怒气值,是否愤怒,是否遭遇过等等)。我们不希望在创建狼獾(wolverine)时反复设置这些属性,或者有时候我们需要创建一个临时的拷贝了所有属性的wolverine(例如预览伤害时,我们需要生成一个临时的拷贝对象,用于计算预览值),这样的情况下,原型模式是一种不错的选择。
稍微修改一下代码,使用.Net为我们提供的ICloneable接口(这个接口经常见到啊!原来还是一种设计模式!突然感觉高贵冷艳了起来。):
public class AnimalPrototype : ICloneable
{
public string name { get; set;}
public int hp{get;set;}
public AnimalPrototype(string name_)
{
name = name_;
}
public override string ToString()
{
return string.Format ("[Animal: name={0}, hp={1}]", name, hp);
}
public virtual object Clone()
{
return (object)this.MemberwiseClone ();
}
}
使用时:
AnimalPrototype wolverine2 = (AnimalPrototype)wolverine.Clone ();
wolverine2.hp = 80;
这样我们就可以进行各种克隆了,生成茫茫多的wolverine。
然而,这并不能满足我们的需求。例如,我们定义了一种会使用技能的动物:
public class AnimalSkill : ICloneable
{
public string skillName { get; set;}
public int skillAttack { get; set;}
public object Clone()
{
return (object)this.MemberwiseClone ();
}
public override string ToString ()
{
return string.Format ("[AnimalSkill: skillName={0}, skillAttack={1}]", skillName, skillAttack);
}
}
public class SkillAnimalPrototype : AnimalPrototype
{
private AnimalSkill _skill;
public AnimalSkill skill { get{ return _skill;}}
public SkillAnimalPrototype(string name_):base(name_)
{
_skill = new AnimalSkill ();
}
public void SetSkillInfo(string name_, int attack_)
{
_skill.skillName = name_;
_skill.skillAttack = attack_;
}
public override object Clone()
{
return (object)this.MemberwiseClone ();
}
public override string ToString()
{
return base.ToString () + skill.ToString ();
}
}
当我们调用时:
SkillAnimalPrototype phoenix = new SkillAnimalPrototype ("phoenix");
phoenix.SetSkillInfo ("Revive", 0);
SkillAnimalPrototype phoenix2 = (SkillAnimalPrototype)phoenix.Clone ();
phoenix2.name = "phoenix2";
phoenix2.SetSkillInfo ("Fire", 10);
Console.WriteLine (phoenix);
Console.WriteLine (phoenix2);
会得到结果:
[Animal: name=phoenix, hp=0][AnimalSkill: skillName=Fire, skillAttack=10]
[Animal: name=phoenix2, hp=0][AnimalSkill: skillName=Fire, skillAttack=10]
我们看到AnimalSkill都显示的是第二次设置的信息。是因为MemberwiseClone只是浅表复制,复制值类型没有问题,但是复制引用类型的时候,只是复制了引用(对string做了特殊处理),所以两个AnimalSkill指向的是同一个对象。
所以我们需要这样修改SkillAnimalPrototype的Clone方法:
public override object Clone()
{
SkillAnimalPrototype ret = (SkillAnimalPrototype)base.Clone();
ret._skill = (AnimalSkill)_skill.Clone ();
return ret;
}
这样我们就会得到新的结果:
[Animal: name=phoenix, hp=0][AnimalSkill: skillName=Revive, skillAttack=0]
[Animal: name=phoenix2, hp=0][AnimalSkill: skillName=Fire, skillAttack=10]
这样就是深表复制。
深表复制比较复杂,实际情况中要格外注意。而且,并不是所有的引用类型的变量都需要被深表复制,例如我们有一个AnimalBasicData的类型:
public class AnimalBasicData
{
public int mapHp{ get; private set;}
public void LoadFromData(string file)
{
//TODO:
}
}
通过LoadFromData加载配置文件的数据,而我们希望所有的wolverine都用同一份AnimalBasicData,所有的phoenix都用另一份AnimalBasicData,Clone的时候就不需要也不该对AnimalBasicData进行深表复制。
最后说两句:
原型模式好处在于对于相同(或者大部分相似)的对象,我们创建它们的时候,并不需要考虑每一个对象的每一个细节。并且可以创建一个对象的副本,在有限的范围内修改这个副本不会影响原对象。
坏处就在于,我们需要实现复杂的Clone方法,需要避免多余的深表复制(浪费内存),尤其要注意循环引用的情况(例如A引用B,B再引用A,这样会造成死循环)。