原型模式概述
原型模式是一种特殊的创建型模式,它通过复制一个已有对象来获取更多相同或相似的对象。原型模式可以提高系统同类型对象的创建效率,简化创建过程。
举个栗子:你写了一份简历,需要"复制"多份出来以便投给不同的企业,难道真的需要"手写"多份出来嘛,那不是浪费纸张浪费笔墨吗,后面我们会用代码来实现这个小栗子。
原型模式定义
使用原型实例指定待创建对象的类型,并通过复制这个原型来创建新的对象。
原型模式原理
将一个原型对象传给要发动创建的对象(即客户端对象),这个要发动创建的对象通过请求原型对象复制自己来实现新对象的创建过程。这种创建新对象的过程也称为”克隆对象“,创建新对象的工厂就是原型类自身,工厂方法由负责复制原型对象的克隆方法来实现。
Tips:通过克隆对象创建的是全新的对象,它们在内存中拥有新的地址。通常对所克隆产生的新对象的创建过程。这种创建对象的过程也称之为"克隆对象",创建新对象的工厂就是原型类自身,工厂方法由复制原型对象的克隆方法来实现。
原型模式的结构
- Prototype(抽象原型类):它是声明克隆方法的接口,是所有具体原型类的公共父类,它可以是抽象类也可以是接口,甚至可以是具体实现类。
- ConcertePrototype(具体原型类):它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
- Client:可以复制实现了原型接口的任何对象
原型模式的实现
原型模式的深拷贝与浅拷贝
首先我们先来看一段小样例:
简历类:
public class Resume : ICloneable
{
public string Name { get; set; }
public int Age { get; set; }
public string Sex { get; set; }
private WorkExperince workExperince;
public Resume()
{
workExperince = new WorkExperince();
}
public void SetWorkExperince(string workDate, string workCompany)
{
workExperince.WorkDate = workDate;
workExperince.Company = workCompany;
}
public void Display()
{
Console.WriteLine($"个人信息:\r\n{Name}\t{Sex}\t{Age}");
Console.WriteLine($"工作经历:\r\n{workExperince.WorkDate}\t{workExperince.Company}\t");
}
public object Clone()
{
return MemberwiseClone();
}
}
工作经历类:(简历类与工作经历的类关系是1-->n)
public class WorkExperince
{
public string WorkDate { get; set; }
public string Company { get; set; }
}
假设我们现在需要3份简历,客户端实例化3个:
static void Main(string[] args)
{
Resume resume1 = new Resume() { Name = "张三", Age = 22, Sex = "女" };
resume1.SetWorkExperince("2019-2020", "MicroSoft");
Resume resume2 = (Resume)resume1.Clone();
resume1.SetWorkExperince("2019-2020", "Tencent");
Resume resume3 = (Resume)resume1.Clone();
resume1.SetWorkExperince("2019-2020", "AliBaba");
resume1.Display();
resume2.Display();
resume3.Display();
Console.WriteLine("Press Any Key to end!");
Console.ReadKey();
}
输出结果:
发现什么问题了吗?对,就是我们明明设置了3份不同的工作经历,最后工作经历却是一样的并且是最后一次设定的被"覆盖"了。这是典型的浅表复制,因为工作经历是一个引用类型,对于应引用类型的克隆,就只是复制了对象的引用,也就是内存指向的地址。
浅复制:被复制对象的所有变量都含有与原来相同的值,而所有的对其他对象的引用都仍然指向原来的对象。
那这显然是没有达到我们"复制简历"的要求呢?有浅克隆自然也有深克隆,那我们来试试深克隆,改造一下代码。
改造过程:
在简历类中新增一个私有构造函数
private Resume(WorkExperince work)
{
//提供Clone方法调用的私有构造函数,以便克隆"工作经历"的数据
workExperince = (WorkExperince)work.Clone();
}
工作经历类也继承"ICloneable
"并实现
public class WorkExperince : ICloneable
{
public string WorkDate { get; set; }
public string Company { get; set; }
public object Clone()
{
return MemberwiseClone();
}
}
修改简历类中Clone方法的实现
Tips:调用私有构造函数,让"工作经历"克隆完成,然后再给这个简历的相关属性赋值,最终返回一个深克隆的简历对象
public object Clone()
{
//return MemberwiseClone();
Resume resume = new Resume(workExperince);
resume.Name = Name;
resume.Age = Age;
resume.Sex = Sex;
return resume;
}
输出结果:
OK。目标达成!
通过克隆实现
可以注意到,我们刚才的Clone方法是通过继承ICloneable
来实现的。我们看下这个接口实现
通过观察程序集进去发现ICloneable接口仅仅只有Clone()一个方法。另外可以看到返回的是一个object类型,F12进去
原型模式优缺点
优点
- 可以克隆对象, 而无需与它们所属的具体类相耦合。
- 可以克隆预生成原型, 避免反复运行初始化代码。
- 可以更方便地生成复杂对象。
- 可以用继承以外的方式来处理复杂对象的不同配置。
缺点
- 克隆包含循环引用的复杂对象可能会非常麻烦。
原型模式使用场景
使用场景
- 如果你需要复制一些对象,同时又希望代码独立于这些对象所属的具体类, 可以使用原型模式。
- 果子类的区别仅在于其对象的初始化方式, 那么你可以使用该模式来减少子类的数量。 别人创建这些子类的目的可能是为了创建特定类型的对象。
实现方式
-
创建原型接口, 并在其中声明
克隆
方法。 如果你已有类层次结构, 则只需在其所有类中添加该方法即可。 -
原型类必须另行定义一个以该类对象为参数的构造函数。 构造函数必须复制参数对象中的所有成员变量值到新建实体中。 如果你需要修改子类, 则必须调用父类构造函数, 让父类复制其私有成员变量值。
如果编程语言不支持方法重载, 那么你可能需要定义一个特殊方法来复制对象数据。 在构造函数中进行此类处理比较方便, 因为它在调用
new
运算符后会马上返回结果对象。 -
克隆方法通常只有一行代码: 使用
new
运算符调用原型版本的构造函数。 注意, 每个类都必须显式重写克隆方法并使用自身类名调用new
运算符。 否则, 克隆方法可能会生成父类的对象。 -
你还可以创建一个中心化原型注册表, 用于存储常用原型。
你可以新建一个工厂类来实现注册表, 或者在原型基类中添加一个获取原型的静态方法。 该方法必须能够根据客户端代码设定的条件进行搜索。 搜索条件可以是简单的字符串, 或者是一组复杂的搜索参数。 找到合适的原型后, 注册表应对原型进行克隆, 并将复制生成的对象返回给客户端。
最后还要将对子类构造函数的直接调用替换为对原型注册表工厂方法的调用。
源代码示例
源代码仓库地址:https://github.com/luchong0813/DesignModel