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






3.28/5 (12投票s)
“用于复制或克隆资源和耗时对象的创建设计模式”
引言
原型模式通常在以下情况中使用
- 每次创建对象时,都需要初始化资源密集型数据,这会影响性能,例如:调用第三方API,获取JSON响应,将其转换为POCO对象,执行一些操作,然后将此数据加载到对象中。
- 假设同一类型的对象共享相同的资源和耗时数据,例如:在大量数据库访问中,所有同类型“
Employee
”对象(emp1
,emp2
,...emp<n>
)都需要相同的数据库结果集
。 - 克隆的对象在状态(属性值)变化方面明显小于原始对象,因此可以根据需要更改克隆对象的数据。
注意
原型模式虽然字面上创建了对象的内存,但对象池模式会重用已创建的对象。
原型模式
- 通过克隆现有原始对象来实例化对象。
- 这个复杂的原始对象被称为原型对象。
- 要克隆的对象(
Prototype
)应提供一种方式来克隆(复制
)自身给其他对象。 - 这种方式是通过向客户端(任何调用代码都称为客户端,请勿将其与客户端/服务器约定相关联)公开
Clone()
方法来实现的。不强制使用与Clone()
相同的名称,可以使用任何方便的名称。 - 客户端代码无需使用"
new
"关键字来创建新对象。它只需在Concrete Prototype Class
对象上调用Clone()
方法。此Clone()
方法仅返回新创建的克隆对象(已加载所有数据)的引用变量。 - C#提供了
IClonable
接口作为抽象原型,可以在Concrete Prototype Class
中实现。IClonable
公开了clone()方法,但问题是Clone方法的返回类型是object
,因此需要进行类型转换。 - 克隆对象的状态(属性值)在克隆时与其
Prototype
对象相同。 - 如果
Prototype
类仅包含原始类型,则浅拷贝就足够了。 - 如果
Prototype
类内部包含指向其他对象的引用变量(例如,Employee
对象内部包含Address
对象),则需要深拷贝,这意味着也为引用对象分配了新内存。否则,Prototype
对象和所有克隆对象将共享对所引用对象的同一内存。在我们的例子中,是Address
。因此,任何一个对象对Address
对象的更改都会影响所有其他对象,因为它们共享相同的内存地址。 - 每种语言都提供了一种克隆对象的方法。在C#中,这是通过
MemberwiseClone()
实现的。
参与者
Prototype
:声明克隆自身的签名方法ConcretePrototype
:克隆对象自身的实现方法Client
:需要克隆对象并调用ConcretePrototype
对象clone()
方法的代码
背景
对象创建
通常,类对象是通过特殊关键字"new
"创建的。
“new
”关键字执行三件事
- 在堆上创建内存。
- 通过构造函数(可选)将数据初始化到新创建的对象内存中。
- 返回指向对象的指针或引用。
例如
在这里,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();
}
}
输出
解释
在此程序中
IEmployee
是Prototype
Developer
是ConcretePrototype
PrototypeClient
是使用Prototype
创建对象的Client
在上面的程序中,浅拷贝和深拷贝都得到了实现。Developer类将ProjectDetail
对象作为引用变量。这被称为对象组合(has-a关系)。原始对象dev1
包含name="joe"
,并且它还有一个引用变量“project
”指向ProjectDetail
对象。字段的值在声明dev1
对象时进行赋值。从dev1
对象(原始对象)进行浅拷贝,并将其分配给新对象devCopy1
。devCopy1
的所有值都已故意更改。由于dev1
和devCopy1
内部的引用对象共享相同的内存位置,因此引用对象的最后一个修改值被设置为原始对象和克隆对象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字符串反序列化为新对象并返回给调用代码。
给所有读者的笔记
亲爱的程序员们,这是我的第一篇文章。我的目标是用简单的语言让大家清楚地理解原型模式,同时不牺牲深入的技术细节。请分享您的反馈。许多类似的设计模式即将到来。感谢阅读!