65.9K
CodeProject 正在变化。 阅读更多。
Home

设计模式系列:创建型模式:原型模式

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.28/5 (12投票s)

2017年2月20日

CPOL

7分钟阅读

viewsIcon

13274

“用于复制或克隆资源和耗时对象的创建设计模式”

引言

原型模式通常在以下情况中使用

  1. 每次创建对象时,都需要初始化资源密集型数据,这会影响性能,例如:调用第三方API,获取JSON响应,将其转换为POCO对象,执行一些操作,然后将此数据加载到对象中。
  2. 假设同一类型的对象共享相同的资源和耗时数据,例如:在大量数据库访问中,所有同类型“Employee”对象(emp1emp2,...emp<n>)都需要相同的数据库结果集
  3. 克隆的对象在状态(属性值)变化方面明显小于原始对象,因此可以根据需要更改克隆对象的数据。

注意

原型模式虽然字面上创建了对象的内存,但对象池模式会重用已创建的对象。

原型模式

  1. 通过克隆现有原始对象来实例化对象。
  2. 这个复杂的原始对象被称为原型对象
  3. 要克隆的对象(Prototype)应提供一种方式来克隆复制)自身给其他对象。
  4. 这种方式是通过向客户端(任何调用代码都称为客户端,请勿将其与客户端/服务器约定相关联)公开Clone()方法来实现的。不强制使用与Clone()相同的名称,可以使用任何方便的名称。
  5. 客户端代码无需使用"new"关键字来创建新对象。它只需在Concrete Prototype Class对象上调用Clone()方法。此Clone()方法仅返回新创建的克隆对象(已加载所有数据)的引用变量。
  6. C#提供了IClonable接口作为抽象原型,可以在Concrete Prototype Class中实现。 IClonable公开了clone()方法,但问题是Clone方法的返回类型是object,因此需要进行类型转换。
  7. 克隆对象的状态(属性值)在克隆时与其Prototype对象相同。
  8. 如果Prototype类仅包含原始类型,则浅拷贝就足够了。
  9. 如果Prototype类内部包含指向其他对象的引用变量(例如,Employee对象内部包含Address对象),则需要深拷贝,这意味着也为引用对象分配了新内存。否则,Prototype对象和所有克隆对象将共享对所引用对象的同一内存。在我们的例子中,是Address。因此,任何一个对象对Address对象的更改都会影响所有其他对象,因为它们共享相同的内存地址。
  10. 每种语言都提供了一种克隆对象的方法。在C#中,这是通过MemberwiseClone()实现的。

参与者

  • Prototype:声明克隆自身的签名方法
  • ConcretePrototype:克隆对象自身的实现方法
  • Client:需要克隆对象并调用ConcretePrototype对象clone()方法的代码

背景

对象创建

通常,类对象是通过特殊关键字"new"创建的。

new”关键字执行三件事

  1. 在堆上创建内存。
  2. 通过构造函数(可选)将数据初始化到新创建的对象内存中。
  3. 返回指向对象的指针或引用。

例如

在这里,objEmp是引用变量,它保存着实际位于堆内存中的对象的地址或引用。这个引用变量objEmp位于栈内存中。

资源昂贵的对象

假设一个对象“objEmp”包含从多个资源收集的大量数据,并且经过了复杂的处理,导致性能受到严重影响。现在,其他对象也需要相同的数据,例如“objEmp1”,“objEmp2”...“objEmp<n>”。这些对象不应承受与“objEmp”对象相同的痛苦,因为所需数据已包含在源对象“objEmp”中。应该克隆"objEmp"。

实现原型模式

手动克隆

手动克隆所有字段、属性、引用类型。问题是这可能需要很长时间才能克隆所有引用对象,以及引用对象内部的引用对象等等。不建议使用此方法实现。

public class Person : ICloneable
{
   public string Name;
   public Person Spouse;
   public object Clone()
   {
       Person p = new Person();
       p.Name = this.Name;
       if (this.Spouse != null)
           p.Spouse = (Person)this.Spouse.Clone();
       return p;
   }
}

两种克隆类型

MemberwiseClone()

根据Microsoft文档,

"MemberwiseClone方法通过创建新对象来创建浅拷贝,然后将当前对象的非静态字段复制到新对象中。如果字段是值类型,则对字段执行逐位复制。如果字段是引用类型,则复制引用,但不复制被引用的对象;因此,原始对象及其克隆引用同一个对象。"

浅拷贝

当进行浅拷贝以从源对象创建克隆对象时,只为值类型创建克隆对象的新内存。如果源对象内部包含引用变量,则原始对象和克隆对象将共享同一引用变量对象的内存。对引用对象状态值的任何更改都会影响原始对象和克隆对象。

通常,这种对象的浅拷贝是使用MemberwiseClone()完成的。

 

深拷贝

当执行深拷贝来创建克隆对象时,除了原始类型之外,还会为子引用变量创建单独的内存。为了实现这一点,再次,应该实现MemberwiseClone()关键字并将其暴露在所有引用类中,以创建子引用对象,然后将其分配给克隆对象,以便为子引用变量也创建新的单独内存。这是实现带有引用类型成员的对象深拷贝的一种方法。还有其他方法,如序列化,使用第三方NewtonSoft JsonConvert,反射等。

 

使用MemberwiseClone()的示例

public interface IEmployee
{
    IEmployee ShallowClone();
    IEmployee DeepClone();
    string GetDetails();
}
public interface IProject
{
    IProject ShallowClone();
}
public class ProjectDetail : IProject
{
    public string ProjectName { get; set; }
    public int Size { get; set; }
    public IProject ShallowClone()
    {
        return (IProject)MemberwiseClone();
    }
}
public class Developer : IEmployee
{
    public string Name { get; set; }
    public ProjectDetail project { get; set; }
    public IEmployee ShallowClone()
    {
        return (IEmployee)MemberwiseClone();
    }
    public IEmployee DeepClone()
    {
        Developer dev = (Developer)this.ShallowClone();
        dev.project = (ProjectDetail)project.ShallowClone();
        return dev;
    }
    public string GetDetails()
    {
        return string.Format("{0}->Project Name:{1}->Project Size:{2}",
            Name,
            project.ProjectName,
            project.Size);
    }

}
class PrototypeClient
{
    static void Main(string[] args)
    {
        Console.WriteLine("Shallow Copy Example:");
        Developer dev1 = new Developer()
        {
            Name = "Joe",
            project = new ProjectDetail()
            {
                ProjectName = "E-Commerce",
                Size = 10
            }
        };
        Developer devCopy1 = (Developer)dev1.ShallowClone();
        devCopy1.Name = "Sam";
        devCopy1.project.ProjectName = "Mobile App";
        devCopy1.project.Size = 8;
        Console.WriteLine(dev1.GetDetails());
        Console.WriteLine(devCopy1.GetDetails());

        Console.WriteLine("\nDeep Copy Example:");
        Developer dev2 = new Developer()
        {
            Name = "Joe",
            project = new ProjectDetail()
            {
                ProjectName = "E-Commerce",
                Size = 10
            }
        };
        Developer devCopy2 = (Developer)dev2.DeepClone();
        devCopy2.Name = "Sam";
        devCopy2.project.ProjectName = "Mobile App";
        devCopy2.project.Size = 8;
        Console.WriteLine(dev2.GetDetails());
        Console.WriteLine(devCopy2.GetDetails());
        Console.ReadKey();
    }
}

输出

解释

在此程序中

  • IEmployeePrototype
  • DeveloperConcretePrototype
  • PrototypeClient是使用Prototype创建对象的Client

在上面的程序中,浅拷贝和深拷贝都得到了实现。Developer类将ProjectDetail对象作为引用变量。这被称为对象组合(has-a关系)。原始对象dev1包含name="joe",并且它还有一个引用变量“project”指向ProjectDetail对象。字段的值在声明dev1对象时进行赋值。从dev1对象(原始对象)进行浅拷贝,并将其分配给新对象devCopy1devCopy1的所有值都已故意更改。由于dev1devCopy1内部的引用对象共享相同的内存位置,因此引用对象的最后一个修改值被设置为原始对象和克隆对象devCopy1。在深拷贝模式下,会创建一个"new"引用对象(子对象)来为克隆对象的引用子对象"project"分配单独的内存。

使用序列化进行深拷贝

序列化:将对象转换为二进制流

反序列化:从二进制流转换回原始对象

无需担心每个引用子成员的克隆。

示例

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
namespace CreationPattern
{
    public class Utility
    {
        public static T DeepClone<T>(T sourceObj)
        {
            if (!typeof(T).IsSerializable)
            {
                throw new ArgumentException("Class must be marked [Serializable]");
            }
            if (Object.ReferenceEquals(sourceObj, null))
            {
                return default(T);
            }
            using (var ms = new MemoryStream())
            {
                var formatter = new BinaryFormatter();
                formatter.Serialize(ms, sourceObj);
                ms.Position = 0;
                return (T)formatter.Deserialize(ms);
            }
        }
    }
    [Serializable]
    public class ProjectDetail
    {
        public string ProjectName { get; set; }
        public int Size { get; set; }
    }
    [Serializable]
    public class Developer
    {
        public string Name { get; set; }
        public ProjectDetail project { get; set; }
        public Developer DeepClone()
        {
            return (Developer)Utility.DeepClone<Developer>(this);
        }
        public string GetDetails()
        {
            return string.Format("{0}->Project Name:{1}->Project Size:{2}",
                Name,
                project.ProjectName,
                project.Size);
        }
    }
    class PrototypeClient
    {
        static void Main(string[] args)
        {

            Console.WriteLine("\nDeep Copy Example:");
            Developer dev2 = new Developer()
            {
                Name = "Joe",
                project = new ProjectDetail()
                {
                    ProjectName = "E-Commerce",
                    Size = 10
                }
            };
            Developer devCopy2 = (Developer)dev2.DeepClone();
            devCopy2.Name = "Sam";
            devCopy2.project.ProjectName = "Mobile App";
            devCopy2.project.Size = 8;
            Console.WriteLine(dev2.GetDetails());
            Console.WriteLine(devCopy2.GetDetails());
            Console.ReadKey();
        }
    }
}

输出

解释

流是字节序列。MemoryStream是字节序列,将在内存中处理。如果序列化的对象需要存储在文件中并反序列化回原始对象,那么FileStream可以是选择。

源对象使用BinaryFormatter对象的Serialize()方法进行序列化,并以MemoryStream对象的形式存储。BinaryFormatter使用其Deserialize()方法反序列化MemoryStream对象并返回新对象。

使用NewtonSoft JSON序列化进行深拷贝

在项目中导入using Newtonsoft.Json。如果还没有,请使用NuGet。

using Newtonsoft.Json;
using System;
namespace CreationPattern
{
    public class Utility
    {
        public static T DeepClone<T>(T sourceObj)
        {
            if (Object.ReferenceEquals(sourceObj, null))
            {
                return default(T);
            }
            return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(sourceObj));
        }
    }
    public class ProjectDetail
    {
        public string ProjectName { get; set; }
        public int Size { get; set; }
    }
    public class Developer
    {
        public string Name { get; set; }
        public ProjectDetail project { get; set; }
        public Developer DeepClone()
        {
            return (Developer)Utility.DeepClone<Developer>(this);
        }
        public string GetDetails()
        {
            return string.Format("{0}->Project Name:{1}->Project Size:{2}",
                Name,
                project.ProjectName,
                project.Size);
        }
    }
    class PrototypeClient
    {
        static void Main(string[] args)
        {

            Console.WriteLine("\nDeep Copy Example:");
            Developer dev2 = new Developer()
            {
                Name = "Joe",
                project = new ProjectDetail()
                {
                    ProjectName = "E-Commerce",
                    Size = 10
                }
            };
            Developer devCopy2 = (Developer)dev2.DeepClone();
            devCopy2.Name = "Sam";
            devCopy2.project.ProjectName = "Mobile App";
            devCopy2.project.Size = 8;
            Console.WriteLine(dev2.GetDetails());
            Console.WriteLine(devCopy2.GetDetails());
            Console.ReadKey();
        }
    }
}

输出

解释

(Developer)Utility.DeepClone<Developer>(this)

此代码调用泛型Utility类静态方法。该方法使用JsonConvert.Serialize()将对象序列化为JSON字符串,并使用JsonConvert.Deserialize()将JSON字符串反序列化为新对象并返回给调用代码。

给所有读者的笔记

亲爱的程序员们,这是我的第一篇文章。我的目标是用简单的语言让大家清楚地理解原型模式,同时不牺牲深入的技术细节。请分享您的反馈。许多类似的设计模式即将到来。感谢阅读!

© . All rights reserved.