原型模式是一种创建型设计模式,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()方法不但复制结构 ,也复制数据,其实就是实现了原型模式的深拷贝。

源代码地址:https://github.com/houzhenhuang/DesignPattern