原型模式是一种创建型设计模式,Prototype模式允许一个对象再创建另外一个可定制的对象,根本无需知道任何如何创建的细节。工作原理是:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它们自己来实施创建。
接下来我们就以简历为例,当我们要面试时,通常会通过打印机将简历一份份打印出来。
简历代码初步实现
简历类:
namespace PrototypePattern { /// <summary> /// 简历 /// </summary> public class Resume { public string Name { get; set; } public string Sex { get; set; } public int Age { get; set; } public string TimeArea { get; set; } public string Company { get; set; } public Resume(string name) { this.Name = name; } //设置个人信息 public void SetPersonalInfo(string sex, int age) { this.Sex = sex; this.Age = age; } //设置工作经历 public void SetWorkExperience(string timeArea, string company) { this.TimeArea = timeArea; this.Company = company; } //显示 public void Show() { Console.WriteLine($"姓名:{this.Name} 性别:{this.Sex} 年龄:{this.Age}"); Console.WriteLine($"工作经历:{this.TimeArea} {this.Company}"); } } }
客户端调用代码:
static void Main(string[] args) { try { Resume a = new Resume("tom"); a.SetPersonalInfo("男", 21); a.SetWorkExperience("1998-2000", "XXX公司"); Resume b = new Resume("tom"); b.SetPersonalInfo("男", 21); b.SetWorkExperience("1998-2000", "XXX公司"); Resume c = new Resume("tom"); c.SetPersonalInfo("男", 21); c.SetWorkExperience("1998-2000", "XXX公司"); a.Show(); b.Show(); c.Show(); } catch (Exception ex) { Console.WriteLine(ex.Message); } Console.ReadKey(); }
结果如下:
没毛病,出现了3张一模一模的简历。在这里我们需要3张简历,所以我们实例化了3次。但我觉得这样的客户端很麻烦,如果需要20份,200份呢,我们难道需要实例化20次、200次?
如果简历中写错了一个字,把1998错写成了1999,这时我们就需要修改20,200次。。。
Oh My God,这实在是太糟糕了。
这时有人会说,我们可以这样写:
Resume a = new Resume("tom"); a.SetPersonalInfo("男", 21); a.SetWorkExperience("1998-2000", "XXX公司"); Resume b = a; Resume c = a; a.Show(); b.Show(); c.Show();
这样确实可以实现和上面一样的效果,但这只是“传递引用”,并不是传值,这样就相当于在b纸张和c纸张上写着简历在a纸张上一样。
现在我想让这个简历其它的全部一样,就工作经历不同,怎么办?上面的操作是“传递引用”,当我们修改一个简历的工作经历时候,其它的简历也会跟随着改变。
原型模式
原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
原型模式其实就是从一个对象再创建一个可定制的对象,而且不需要知道任何的细节。
原型类:
namespace PrototypePattern.PrototypePattern { abstract class Prototype { public int Id { get; set; } public Prototype(int id) { this.Id = id; } public abstract Prototype Clone();//这个抽象类的关键就是这个Clone方法 } }
具体原型类:
namespace PrototypePattern.PrototypePattern { /// <summary> /// 具体原型 /// </summary> class ConcretePrototype1 : Prototype { public ConcretePrototype1(int id) : base(id) { } /// <summary> /// 创建当前对象的浅表副本。 /// 方法是创建一个新对象,然后将当前对象的非静态字段复制到该新对象。 /// 如果字段是值类型的,则对该字段进行逐位复制。 /// 如果字段是引用类型的,则复制引用,但不复制引用的对象,因此,原始对象及其副本引用同一个对象 /// </summary> /// <returns></returns> public override Prototype Clone() { return (Prototype)this.MemberwiseClone(); } } }
客户端代码:
ConcretePrototype1 p1 = new ConcretePrototype1(1); ConcretePrototype1 c1 = (ConcretePrototype1)p1.Clone(); Console.WriteLine($"p1.Id={p1.Id},c1.Id={c1.Id}"); c1.Id = 2; Console.WriteLine($"p1.Id={p1.Id},c1.Id={c1.Id}");
结果如下:
对于.net而言,原型抽象类Prototype类是用不类的,因为克隆实在是太常用了,所以.net在System命名空间中提供了ICloneable接口,其中就只有唯一的一个方法Clone(),这样我们就可以只需要实现这个接口就可以完成原型模式了。
简历的原型实现
简历类:
namespace PrototypePattern { /// <summary> /// 简历 /// </summary> public class Resume : ICloneable { public string Name { get; set; } public string Sex { get; set; } public int Age { get; set; } public string TimeArea { get; set; } public string Company { get; set; } public Resume(string name) { this.Name = name; } //设置个人信息 public void SetPersonalInfo(string sex, int age) { this.Sex = sex; this.Age = age; } //设置工作经历 public void SetWorkExperience(string timeArea, string company) { this.TimeArea = timeArea; this.Company = company; } //显示 public void Show() { Console.WriteLine($"姓名:{this.Name} 性别:{this.Sex} 年龄:{this.Age}"); Console.WriteLine($"工作经历:{this.TimeArea} {this.Company}"); } public object Clone() { return (Resume)this.MemberwiseClone(); } } }
客户端调用代码:
Resume a = new Resume("tom"); a.SetPersonalInfo("男", 21); a.SetWorkExperience("1998-2000", "XXX公司"); Resume b = (Resume)a.Clone(); b.SetWorkExperience("2000-2002", "YYY公司"); Resume c = (Resume)a.Clone(); c.SetWorkExperience("2002-2004", "ZZZ公司"); a.Show(); b.Show(); c.Show();
结果如下:
这样实现好多了,而且当你想要改其中某一份简历的时候,直接修改就好了,并不会影响到其它的简历。
一般在初始化信息不发生变化的情况下,克隆是最好的方法。这既隐藏了对象创建的细节,又提高了性能,何乐而不为呢?
浅拷贝与深拷贝
当你自以为完美的时候,然后结果却往往令你大失所望。
哈哈,没错,现在简历对象的所有属性都是值类型或者重写过运算符的引用类型。那么当你的简历类中含有“引用类型”时,那么当你克隆简历时,这个引用类型会不会也被克隆过来呢?
我们简历类中有一个“设置工作经历”的方法,在实际开发中,都会有一个工作经历类,现在我们添加一个“工作经历”类, 当中有“时间区间”和“公司名称”属性,类直接调用这个对象即可。
工作经历类:
namespace PrototypePattern { public class WorkExperience { public string WorkDate { get; set; } public string Company { get; set; } } }
简历类:
namespace PrototypePattern { /// <summary> /// 简历 /// </summary> public class Resume : ICloneable { public string Name { get; set; } public string Sex { get; set; } public int Age { get; set; } public WorkExperience WorkExperience { get; set; } public Resume(string name) { this.Name = name; this.WorkExperience = new WorkExperience(); } //设置个人信息 public void SetPersonalInfo(string sex, int age) { this.Sex = sex; this.Age = age; } //设置工作经历 public void SetWorkExperience(string timeArea, string company) { WorkExperience.WorkDate = timeArea; WorkExperience.Company = company; } //显示 public void Show() { Console.WriteLine($"姓名:{this.Name} 性别:{this.Sex} 年龄:{this.Age}"); Console.WriteLine($"工作经历:{this.WorkExperience.WorkDate} {this.WorkExperience.Company}"); } public object Clone() { return (Resume)this.MemberwiseClone(); } } }
客户端代码调用:
Resume a = new Resume("tom"); a.SetPersonalInfo("男", 21); a.SetWorkExperience("1998-2000", "XXX公司"); Resume b = (Resume)a.Clone(); b.SetWorkExperience("2000-2002", "YYY公司"); Resume c = (Resume)a.Clone(); c.SetWorkExperience("2002-2004", "ZZZ公司"); a.Show(); b.Show(); c.Show();
如果如下:
由于MemberwiseClone()方法是浅表拷贝,对于值类型没啥问题,但对于引用类型,就只复制了引用,它们所指向的任是一个对象。那么怎么实现拷贝时,要将简历类当中的引用类型的对象拷贝一份,而不是拷贝引用呢?
简历的深复制实现
工作经历类:
namespace PrototypePattern { public class WorkExperience:ICloneable { public string WorkDate { get; set; } public string Company { get; set; } public object Clone() { return (WorkExperience)this.MemberwiseClone(); } } }
简历类:
namespace PrototypePattern { /// <summary> /// 简历 /// </summary> public class Resume : ICloneable { public string Name { get; set; } public string Sex { get; set; } public int Age { get; set; } public WorkExperience WorkExperience { get; set; } public Resume(string name) { this.Name = name; this.WorkExperience = new WorkExperience(); } public Resume(WorkExperience work) { this.WorkExperience = (WorkExperience)work.Clone(); } //设置个人信息 public void SetPersonalInfo(string sex, int age) { this.Sex = sex; this.Age = age; } //设置工作经历 public void SetWorkExperience(string timeArea, string company) { WorkExperience.WorkDate = timeArea; WorkExperience.Company = company; } //显示 public void Show() { Console.WriteLine($"姓名:{this.Name} 性别:{this.Sex} 年龄:{this.Age}"); Console.WriteLine($"工作经历:{this.WorkExperience.WorkDate} {this.WorkExperience.Company}"); } public object Clone() { Resume obj = new Resume(this.WorkExperience); obj.Name = this.Name; obj.Sex = this.Sex; obj.Age = this.Age; return obj; } } }
客户端调用:
Resume a = new Resume("tom"); a.SetPersonalInfo("男", 21); a.SetWorkExperience("1998-2000", "XXX公司"); Resume b = (Resume)a.Clone(); b.SetWorkExperience("2000-2002", "YYY公司"); Resume c = (Resume)a.Clone(); c.SetWorkExperience("2002-2004", "ZZZ公司"); a.Show(); b.Show(); c.Show();
结果如下:
注意:使用实现ICloneable接口来实现原型模式的深拷贝,如果类与类之前的关系比较简单的话还好,如果很复杂的话,通常会让你很头痛,这个可以推荐一个简单的方法。就是使用二进制序列化将要拷贝的对象(简历)先序列化,然后再反序列化成对象,这个简历当中的引用类型的属性就能实现深拷贝了。
由于在一些特定的场合,会经常涉及到深拷贝或浅拷贝,比如说,数据集对象DataSet,它就有Clone()方法和Copy()方法,Clone()方法用来复制DataSet的结构,但不复制DataSet的数据 ,实现了原型模式的浅拷贝。Copy()方法不但复制结构 ,也复制数据,其实就是实现了原型模式的深拷贝。