关于设计模式:5-设计模式原型模式

8次阅读

共计 6333 个字符,预计需要花费 16 分钟才能阅读完成。

定义


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

需要


咱们还是通过一个简略需要开始说起,通常状况下,找工作时,须要筹备多份简历,简历信息大致相同,然而能够依据不同的公司的岗位需要微调工作经验细节,以及薪资要求,例如有的公司要求电商教训优先,那么就能够把电商相干的工作细节多写一点,而有的要求治理教训,那么工作细节就须要更多的体现治理能力,薪资要求也会依据具体情况填写具体数值或者面议等。

咱们先抛开原型模式不谈,咱们能够考虑一下,后面讲到的几个创立型模式是否满足需要呢?

首先,咱们须要多份简历,单例模式间接就能够 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 类图


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

改良


当然,这种写法还有很大的优化空间,例如,如果对象属性比拟多,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 接口的应用。

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

正文完
 0