C# 设计模式(五)原型模式(unity演示)


1、引言

  我们在软件开发的过程中,常常会用到new字段来创建对象。那么是不是只有这一种办法来创建对象呢?答案显然不是的。然而使用new来创建对象时,适用于任何时候呢?显然答案也是否定的,什么时候就不适用new呢?让我们来看看,当我们创建的一个实例的过程很昂贵或者很复杂,并且需要创建多个这样的类的实例时。如果这时候我们仍然用new操作符去创建这样的类的实例,会导致内存中多分配一个一样的类实例对象,这未免会增加创建类的复杂度和消耗更多的内存空间。那有人会说,采用简单工厂模式来创建这样的系统。伴随着产品类的不断增加,导致子类的数量不断增加,反而增加额系统复杂程度,所以在这里使用共产模式也是来封装类的创建过程也是不合适的。怎么办呢?这里就不得不提到原型模式了。

2、如何解决

  原型模式可以很好地解决这个问题,因为每个类实例都是相同的,当我们需要多个相同的类实例时,没必要每次都使用new运算符去创建相同的类实例对象,此时我们一般思路就是想——只创建一个类实例对象,如果后面需要更多这样的实例,可以通过对原来对象拷贝一份来完成创建,这样在内存中不需要创建多个相同的类实例,从而减少内存的消耗和达到类实例的复用。 然而这个思路正是原型模式的实现方式。下面就具体介绍下设计模式中的原型设计模式。

3、原型模式详细介绍

3.1、原型模式的定义

原型模式(Prototype Pattern)
  用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。简单来说就是从一个对象再创建另外一个可定制的对象,而且不需要知道任何的创建细节。在现实生活中,也有很多原型设计模式的例子,例如,细胞分裂的过程,一个细胞的有丝分裂产生两个相同的细胞;还有西游记中孙悟空变出后孙的本领和火影忍者中鸣人的隐分身忍术等

3.2、原型模式结构

  下面是原型模式的类图,我们一起来看看!
这里写图片描述
通过上图我们不难发现,在原型模式中只有两个角色:

  • 原型 (原型接口Prototype)角色:声明一个克隆自身的接口;
  • 具体原型(具体ConcreteComponent)角色:该类继承了原型类,用来实现一个克隆自身的操作。

3.3、类图实现

具体实现代码如下所示:

原型类:

/// <summary>
/// 抽象类原型
/// </summary>
public abstract class Prototype
{
    private string id;

    public string Id
    {
        get { return id; }
    }

    public Prototype(string id)
    {
        this.id = id;
    }

    /// <summary>
    /// 抽象类的关键就是这个clone()方法
    /// </summary>
    /// <returns></returns>
    public abstract Prototype Clone();
}

具体原型类

/// <summary>
/// 具体原型
/// </summary>
class ConcretePrototype1 : Prototype
{
    public ConcretePrototype1(string id) : base(id)
    {
    }

    /// <summary>
    /// 浅复制
    /// </summary>
    /// <returns></returns>
    public override Prototype Clone()
    {
        //创建当前对象的浅副本
        return (Prototype)this.MemberwiseClone();
    }
}

(Prototype)this.MemberwiseClone()创建当前对象的浅副本。方法是创建一个新对象,然后将当前对象的非静态字段复制到该新对象。

  • 如果字段是值类型的,则对该字段执行逐位复制。
  • 如果是引用类型的,则复制引用,但不复制引用的对象;

因此,原始对象及其副本引用同一对象MSDN

测试:

ConcretePrototype1 cp1 = new ConcretePrototype1("cp1...");
//克隆类ConcretePrototype1的对象cp1就能得到新的实例c1
ConcretePrototype1 c1 = (ConcretePrototype1)cp1.Clone();
Console.WriteLine("Cloned:{0}", c1.Id);

运行结果:

Cloned:cp1...

3.4、C#举例

  由于克隆类十分常用,以至于.Net在Syste命名空间中提供了ICloneable接口,其中就只有一个方法Clone(),我们在使用中只需实现这个接口就可以完成原型模式了。下面我们来举例说明:

3.4.1、情景设定
  • 假设一份简历,我们需要复制三分,简历上显示:姓名、性别、年龄,工作经历,工作经历包括:公司名字和时间区间。

下面是详细代码:

/// <summary>
/// 简历类
/// </summary>
public class Resume : ICloneable
{
    private string name;
    private string sex;
    private string age;
    private string timeArea;
    private string company;

    public Resume(string name)
    {
        this.name = name;
    }

    /// <summary>
    /// 设置个人信息
    /// </summary>
    /// <param name="sex"></param>
    /// <param name="age"></param>
    public void SetPersonalInfo(string sex, string age)
    {
        this.age = age;
        this.sex = sex;
    }

    /// <summary>
    /// 设置工作经历
    /// </summary>
    /// <param name="timeArea"></param>
    /// <param name="company"></param>
    public void SetWorkExperience(string timeArea, string company)
    {
        this.timeArea = timeArea;
        this.company = company;
    }

    /// <summary>
    /// 显示
    /// </summary>
    public void Display()
    {
        Console.WriteLine("{0},{1},{2}",name,sex,age);
        Console.WriteLine("{0},{1}",timeArea,company);
    }

    public object Clone()
    {
        return (Object)this.MemberwiseClone();
    }
}

测试:

Resume p1 = new Resume("大鸟");
p1.SetPersonalInfo("男", "28");
p1.SetWorkExperience("1998-2000", "XX公司");

Resume p2 = (Resume)p1.Clone();
p2.SetWorkExperience("1999-2002", "YY企业");

Resume p3 = (Resume)p1.Clone();
p3.SetPersonalInfo("男", "25");

p1.Display();
p2.Display();
p3.Display();

运行结果:

大鸟,男,28
1998-2000,XX公司
大鸟,男,28
1999-2002,YY企业
大鸟,男,25
1998-2000,XX公司
3.4.2、分析

  通过以上代码来创建对象时,不需要每次都new一次,这样大大的提高了性能。一般情况,在初始化的信息不变的情况下,克隆是最好的办法。这既隐藏了对象创建的细节,又对性能做了很大的提升。但是上面的是值类型的克隆,那么对于复杂的引用类型会不会奏效呢?我们把简历中的工作经历改成一个单独的类,代码修改如下:
工作经历类:

/// <summary>
/// 工作经历
/// </summary>
public class WorkExperience
{
    private string timeArea;
    private string company;

    public string TimeArea
    {
        get { return timeArea; }
        set { timeArea = value; }
    }

    public string Company
    {
        get { return company; }
        set { company = value; }
    }
}

简历类:

/// <summary>
/// 简历类
/// </summary>
public class Resume : ICloneable
{
    private string name;
    private string sex;
    private string age;

    private WorkExperience work;

    public Resume(string name)
    {
        this.name = name;
        work = new WorkExperience();
    }

    /// <summary>
    /// 设置个人信息
    /// </summary>
    /// <param name="sex"></param>
    /// <param name="age"></param>
    public void SetPersonalInfo(string sex, string age)
    {
        this.age = age;
        this.sex = sex;
    }

    /// <summary>
    /// 设置工作经历
    /// </summary>
    /// <param name="timeArea"></param>
    /// <param name="company"></param>
    public void SetWorkExperience(string timeArea, string company)
    {
        work.TimeArea = timeArea;
        work.Company = company;
    }

    /// <summary>
    /// 显示
    /// </summary>
    public void Display()
    {
        Console.WriteLine("{0},{1},{2}",name,sex,age);
        Console.WriteLine("{0},{1}",work.TimeArea,work.Company);
    }

    public object Clone()
    {
        return (Object)this.MemberwiseClone();
    }
}

测试:

Resume p1 = new Resume("大鸟");
 p1.SetPersonalInfo("男", "28");
 p1.SetWorkExperience("1998-2000", "XX公司");

 Resume p2 = (Resume)p1.Clone();
 p2.SetWorkExperience("1999-2002", "YY企业");

 Resume p3 = (Resume)p1.Clone();
 p3.SetPersonalInfo("男", "25");
 p3.SetWorkExperience("1998-2000", "ZZ公司");

 p1.Display();
 p2.Display();
 p3.Display();

运行结果:

大鸟,男,28
1998-2000,ZZ公司
大鸟,男,28
1998-2000,ZZ公司
大鸟,男,25
1998-2000,ZZ公司
3.4.3、再次分析

  这里你是否想到了刚才说的,复制的情况又两种,值类型和引用类型,引用是不同的,可以点击查看MSDN。这其中提到浅复制,上面第一个例子,可以复制成功是浅复制的原因就是都引用的是值类型。而这里的是引用类型,对引用的对象还是指向了原来的对象,即只引用了地址。所以造成了工作经历都是相同的,而且是最后一个。那么什么是深复制和浅复制呢?下文解释。

3.5、深复制与浅复制

  • 浅复制:被复制的对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用都仍然指向原来的对象。
  • 深复制:把引用对象的变量指向复制过的新对象,而不是原来的被引用的对象。如:我们刚才的例子,我们希望是不同的p1,p2,p3,复制时一变二,二变三。

3.6、深复制使用举例

还是刚才我们改为深复制:
工作经历类:

/// <summary>
/// 工作经历
/// </summary>
public class WorkExperience:ICloneable
{
    private string timeArea;
    private string company;

    public string TimeArea
    {
        get { return timeArea; }
        set { timeArea = value; }
    }

    public string Company
    {
        get { return company; }
        set { company = value; }
    }

    public object Clone()
    {
        return (object)this.MemberwiseClone();
    }
}

简历类:

/// <summary>
/// 简历类
/// </summary>
public class Resume : ICloneable
{
    private string name;
    private string sex;
    private string age;

    private WorkExperience work;

    private Resume(WorkExperience work)
    {
        this.work = (WorkExperience)work.Clone();
    }

    /// <summary>
    /// 为Clone()方法调用的私有构造函数,以便于克隆“工作经历”的数据
    /// </summary>
    /// <param name="name"></param>
    public Resume(string name)
    {
        this.name = name;
        work = new WorkExperience();
    }

    /// <summary>
    /// 设置个人信息
    /// </summary>
    /// <param name="sex"></param>
    /// <param name="age"></param>
    public void SetPersonalInfo(string sex, string age)
    {
        this.age = age;
        this.sex = sex;
    }

    /// <summary>
    /// 设置工作经历
    /// </summary>
    /// <param name="timeArea"></param>
    /// <param name="company"></param>
    public void SetWorkExperience(string timeArea, string company)
    {
        work.TimeArea = timeArea;
        work.Company = company;
    }

    /// <summary>
    /// 显示
    /// </summary>
    public void Display()
    {
        Console.WriteLine("{0},{1},{2}",name,sex,age);
        Console.WriteLine("{0},{1}",work.TimeArea,work.Company);
    }

    /// <summary>
    /// 
    /// </summary>
    /// <returns>最终返回深复制的对象</returns>
    public object Clone()
    {
        //调用私有的构造函数,克隆工作经历,然后再重新给新对象的字段赋值
        Resume obj = new Resume(this.work);
        obj.name = this.name;
        obj.sex = this.sex;
        obj.age = this.age;

        return obj;
    }
}

测试,我们继续用上面的,运行结果如下:

大鸟,男,28
1998-2000,XX公司
大鸟,男,28
1999-2002,YY企业
大鸟,男,25
1998-2000,ZZ公司

  这次修改是不是达到了预期呢?深复制复制了是新的对象,与原来的对象没有共享关系。

3.7、原型模式的要点

  • 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
  • 原型模式是一种比较简单的模式,也非常容易理解,实现一个接口,重写一个方法即完成了原型模式。在实际应用中,原型模式很少单独出现。经常与其他模式混用,他的原型类Prototype也常用抽象类来替代。
  • 使用原型模式拷贝对象时,需注意浅拷贝与深拷贝的区别。
  • 原型模式可以结合JSON等数据交换格式,为数据模型构建原型。

4、原型模式的优缺点

  • 优点:

    1. 原型模式向客户隐藏了创建新实例的复杂性
    2. 原型模式允许动态增加或较少产品类。
    3. 原型模式简化了实例的创建结构,工厂方法模式需要有一个与产品类等级结构相同的等级结构,而原型模式不需要这样。
    4. 产品类不需要事先确定产品的等级结构,因为原型模式适用于任何的等级结构
  • 缺点:

    1. 每个类必须配备一个克隆方法
    2. 配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。

5、原型模式的适用场景

原型模式适用于:

  • 产生对象过程比较复杂,初始化需要许多资源时。
  • 希望框架原型和产生对象分开时。
  • 同一个对象可能会供其他调用者同时调用访问时。

6、应用举例(unity演示)

  也是写几个类,这里就不在演示了!

7、总结

  最美好的时光总是短暂的,原型模式的介绍就结束了,原型模式用一个原型对象来指明所要创建的对象类型,然后用复制这个原型对象的方法来创建出更多的同类型对象。它与工厂方法模式的实现非常相似,其中原型模式中的Clone方法就类似工厂方法模式中的工厂方法,只是工厂方法模式的工厂方法是通过new运算符重新创建一个新的对象(相当于原型模式的深拷贝实现),而原型模式是通过调用MemberwiseClone方法来对原来对象进行拷贝,也就是复制,同时在原型模式优点中也介绍了与工厂方法的区别。好了一句话,深复制创建的对象与原对象没有任何共享,一个对象的改变不会影响到另外一个对象;而浅复制是共享的,一个改变了,另外一个对象的成员的值会随之改变。


The End
  好了,今天的分享就到这里,如有不足之处,还望大家及时指正,随时欢迎探讨交流!!!


喜欢的朋友们,请帮顶、点赞、评论!您的肯定是我写作的不竭动力!

相关阅读
C# 23种设计模式(unity演示)

猜你喜欢

转载自blog.csdn.net/lxt610/article/details/81108211