5. 设计模式-原型模式

定义


原型模式是用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。简单地说就是,首先创建一个实例,然后通过这个实例去拷贝(克隆)创建新的实例。

需求


我们还是通过一个简单需求开始说起,通常情况下,找工作时,需要准备多份简历,简历信息大致相同,但是可以根据不同的公司的岗位需求微调工作经历细节,以及薪资要求,例如有的公司要求电商经验优先,那么就可以把电商相关的工作细节多写一点,而有的要求管理经验,那么工作细节就需要更多的体现管理才能,薪资要求也会根据具体情况填写具体数值或者面议等。

我们先抛开原型模式不谈,我们可以考虑一下,前面讲到的几个创建型模式能否满足需求呢?

首先,我们需要多份简历,单例模式直接就可以Pass掉了,其次,由于简历信息比较复杂,起码也有几十个字段,并且根据不同情况,可能会发生部分修改,因此,三个工厂模式也不能满足需求。不过想到这里,我们想到建造者模式或许满足需求,因为它就是用来创建复杂对象的,不妨先用建造者模式试一下。

先定义简历:

public abstract class ResumeBase
{
    /// <summary>
    /// 姓名
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// 性别
    /// </summary>
    public string Gender { get; set; }

    /// <summary>
    /// 年龄
    /// </summary>
    public int Age { get; set; }

    /// <summary>
    /// 期望薪资
    /// </summary>
    public string ExpectedSalary { get; set; }

    public abstract void Display();
}

/// <summary>
/// 工作经历
/// </summary>
public class WorkExperence
{
    public string Company { get; set; }

    public string Detail { get; set; }

    public DateTime StartDate { get; set; }

    public DateTime EndDate { get; set; }

    public void Display()
    {
        Console.WriteLine("工作经历:");
        Console.WriteLine($"{this.Company}\t{this.StartDate.ToShortDateString()}-{EndDate.ToShortDateString()}");
        Console.WriteLine("工作详细:");
        Console.WriteLine(this.Detail);
    }
}

public class ItResume : ResumeBase
{
    /// <summary>
    /// 工作经历
    /// </summary>
    public WorkExperence WorkExperence { get; set; }

    public override void Display()
    {
        Console.WriteLine($"姓名:\t{this.Name}");
        Console.WriteLine($"性别:\t{this.Gender}");
        Console.WriteLine($"年龄:\t{this.Age}");
        Console.WriteLine($"期望薪资:\t{this.ExpectedSalary}");
        Console.WriteLine("--------------------------------");
        if (this.WorkExperence != null)
        {
            this.WorkExperence.Display();
        }

        Console.WriteLine("--------------------------------");
    }
}

再定义建造者:

public class BasicInfo
{
    /// <summary>
    /// 姓名
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// 性别
    /// </summary>
    public string Gender { get; set; }

    /// <summary>
    /// 年龄
    /// </summary>
    public int Age { get; set; }

    /// <summary>
    /// 期望薪资
    /// </summary>
    public string ExpectedSalary { get; set; }
}

public interface IResumeBuilder
{
    IResumeBuilder BuildBasicInfo(Action<BasicInfo> buildBasicInfoDelegate);
    IResumeBuilder BuildWorkExperence(Action<WorkExperence> buildWorkExperenceDelegate);
    ResumeBase Build();
}

public class ResumeBuilder : IResumeBuilder
{
    private readonly BasicInfo _basicInfo = new BasicInfo();
    private readonly WorkExperence _workExperence = new WorkExperence();

    public IResumeBuilder BuildBasicInfo(Action<BasicInfo> buildBasicInfoDelegate)
    {
        buildBasicInfoDelegate?.Invoke(_basicInfo);
        return this;
    }

    public IResumeBuilder BuildWorkExperence(Action<WorkExperence> buildWorkExperenceDelegate)
    {
        buildWorkExperenceDelegate?.Invoke(_workExperence);
        return this;
    }

    public ResumeBase Build()
    {
        ItResume resume = new ItResume()
        {
            Name = this._basicInfo.Name,
            Gender = this._basicInfo.Gender,
            Age = this._basicInfo.Age,
            ExpectedSalary = this._basicInfo.ExpectedSalary,
            WorkExperence = new WorkExperence
            {
                Company = this._workExperence.Company,
                Detail = this._workExperence.Detail,
                StartDate = this._workExperence.StartDate,
                EndDate = this._workExperence.EndDate
            }
        };
        return resume;
    }
}

其中,定义一个BasicInfo类是为了向外暴漏更少的参数,Build()方法每次调用都会产生一个全新的ItResume对象。

调用的地方也非常简单:

static void Main(string[] args)
{
    IResumeBuilder resumeBuilder = new ResumeBuilder()
        .BuildBasicInfo(resume =>
        {
            resume.Name = "张三";
            resume.Age = 18;
            resume.Gender = "男";
            resume.ExpectedSalary = "100W";
        })
        .BuildWorkExperence(work =>
        {
            work.Company = "A公司";
            work.Detail = "负责XX系统开发,精通YY。。。。。";
            work.StartDate = DateTime.Parse("2019-1-1");
            work.EndDate = DateTime.Parse("2020-1-1");
        });

    ResumeBase resume1 = resumeBuilder
        .Build();

    ResumeBase resume2 = resumeBuilder
        .BuildBasicInfo(resume =>
        {
            resume.ExpectedSalary = "面议";
        })
        .BuildWorkExperence(work =>
        {
            work.Detail = "电商经验丰富";
        })
        .Build();
    resume1.Display();
    resume2.Display();
}

这样好像就已经满足需求了,我们只需要少量修改就可以创建多份简历。但是呢,这种情况,每次创建一批简历之前,我们都必须先有一个Builder,否则无法完成简历的创建,而我们实际期望的是直接通过一份旧的简历就可以复制得到一份新简历,在这种期望下,并没有所谓的Builder存在。
但是通过观察我们不难发现,旧简历其实已经具备了生产新简历的所有参数,唯一缺少的就是Build()方法,因此,既然不能使用Builder,我们直接将Builder中的Build()方法Copy到Resume中不就可以了吗?于是就有了如下改造,将Build()方法完整的CopyResumeBaseItResume中,仅仅将方法名改成了Clone()

public abstract class ResumeBase
{
    ...

    public abstract ResumeBase Clone();
}

public class ItResume : ResumeBase
{
    ...

    public override ResumeBase Clone()
    {
        ItResume resume = new ItResume()
        {
            Name = this.Name,
            Gender = this.Gender,
            Age = this.Age,
            ExpectedSalary = this.ExpectedSalary,
            WorkExperence = new WorkExperence
            {
                Company = this.WorkExperence.Company,
                Detail = this.WorkExperence.Detail,
                StartDate = this.WorkExperence.StartDate,
                EndDate = this.WorkExperence.EndDate
            }
        };
        return resume;
    }
}

调用的地方就可以直接通过resume.Clone()方法创建新的简历了!
完美!其实这就是我们的原型模式了,仅仅是对建造者模式进行了一点点的改造,就有了神奇的效果!

UML类图


我们再来看一下原型模式的类图:
image

改进


当然,这种写法还有很大的优化空间,例如,如果对象属性比较多,Clone()方法的维护就会变得非常麻烦,因此,我们可以使用Object.MemberwiseClone()来简化调用,如下所示:

public override ResumeBase Clone()
{
    ItResume itResume = this.MemberwiseClone() as ItResume;
    itResume.WorkExperence = this.WorkExperence.Clone();
    return itResume;
}

这样就简化很多了,但是又引入了新的问题,MemberwiseClone()是浅拷贝的,因此要完成深拷贝,就必须所有引用类型的属性都实现Clone()功能,如WorkExperence,否则,在后续调用时可能出现由于数据共享而产生的未知错误,这可能是灾难性的,因为很难排查出错误出在哪里,因此,我们更建议使用序列化和反序列化的方式来实现深拷贝,如下所示:

[Serializable]
public sealed class ItResume : ResumeBase
{
    ...

    public override ResumeBase Clone()
    {
        using (MemoryStream stream = new MemoryStream())
        {
            BinaryFormatter bf = new BinaryFormatter();
            bf.Serialize(stream, this);
            stream.Position = 0;
            return bf.Deserialize(stream) as ResumeBase;
        }
    }
}

这里需要注意的是,所涉及的所有引用类型的属性(字符串除外),都需要打上Serializable标记,否则会抛出异常(抛出异常比MemberwiseClone()的什么也不发生要好的多),注意,这里的ItResume最好标记为sealed,原因后续解释。

使用场景


  • 当需要重复创建一个包含大量公共属性,而只需要修改少量属性的对象时;
  • 当需要重复创建一个初始化需要消耗大量资源的对象时。

优点


  • 创建大量重复的对象,同时保证性能

浅拷贝与深拷贝


上面提到了浅拷贝和深拷贝,这里简单解释一下。

浅拷贝

  1. 对于基本类型的成员变量,浅拷贝会直接进行值传递。
  2. 对于引用类型的成员变量,比如数组、对象等,浅拷贝会进行引用传递。因此,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。
  3. Object.MemberwiseClone()是浅拷贝。

深拷贝

  1. 对于一个对象无论其成员变量是什么类型,都从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象;
  2. 对对象先序列化,再反序列化是深拷贝。

浅拷贝和深拷贝是相对的,如果一个对象内部只有基本数据类型,那么浅拷贝和深拷贝是等价的。

避免使用ICloneable接口


ICloneable接口只有一个Clone()成员方法,我们通常会用它充当Prototype基类来实现原型模式,但我这里要说的是尽量避免使用ICloneable,原因在 《Effective C#:50 Specific Ways to Improve Your C#》 一书中的原则27 有给出,基本思想如下:

  1. 由于只有一个Clone方法,因此调用者无法区分到底是深拷贝还是浅拷贝,会给调用者造成极大的困扰;
  2. 如果基类继承了ICloneable接口,并且非Sealed类型,那么它的所有派生类都需要实现Clone方法。否则,用派生类对象调用Clone方法,返回的对象将会是基类Clone方法创建的对象,这就给派生类带来了沉重的负担,因此在非密封类中应该避免实现 ICloneable 接口,但这个不是ICloneable特有的缺陷,任何一种方式实现原型模式都存在该问题,因此建议将原型模式的实现类设置为密封类。
  3. Clone方法返回值是object,是非类型安全的;

ICloneable被很多人认为是一个糟糕的设计,其他理由如下:

  1. ICloneable除了标识可被克隆之外,无论作为参数还是返回值都没有任何意义;
  2. .Net Framework在升级支持泛型至今,都没有添加一个与之对应的ICloneable<T>泛型接口;
  3. 很多框架中为了向下兼容,虽然实现了ICloneable接口,但是内部只提供了一个抛出异常的私有实现,例如SqlConnection

鉴于上述诸多缺点,在实现原型模式时,ICloneable接口能不用就不要用了,自己定义一个更有意义的方法或许会更好。

总结


原型模式通常用在对象创建复杂或者创建过程需要消耗大量资源的场景。但由于其实现过程中会存在诸多问题,如果处理不当很容易对使用者造成困扰,因此,应尽量使用序列化反序列化的方式实现,尽量将其标记为sealed,另外,尽量避免对ICloneable接口的使用。

源码链接
更多内容,欢迎关注公众号:
image

阅读 176

推荐阅读