原型模式(Prototype)
有的时候,我们需要一个实例时,并且,当创建一个实例的过程比较复杂或者说是昂贵时,
比如,创建实例的构造函数非常的复杂,在执行这个构造函数时会消耗较长的时间,
同时呢,这个构造函数中的一些信息又没有什么变化
(也就是说创建第一个实例时初始化信息是这样的,创建第二个实例时初始化信息还是还是这样的),
那么直接使用 new 来创建这样一个实例就显得太昂贵了,
最好的解决方法,并不是使用 new 来实例化一个对象,
而是使用克隆,也就是复制,克隆呢,就是通过复制现在已经有了的实例来创建新的实例,
这样有什么好处呢?
很明显,这样实现呢,客户端根本就不知道具体要实例化的是哪一个类,
它只知道是复制了,但具体的实例化情况,它却是一无所知的,这样便对客户端进行了隐藏,
同时,复制一个对象一般情况下会比创建一个对象性能更高(当然有时候也不一定,只是一般情况而已),
其实上面所提到的就是下面要介绍的一个设计模式--原型模式(Prototype),
原型模式(Prototype)
定义:用原型实例指定创建对象的种类,并且通过拷贝这些原型来创建新的实例。
换句话说,原型模式就是通过复制现在已经存在的对象来创建一个新的对象。
原型模式的结构类图如下:
从上面这副类图中,可以看出,在 AbstractPrototype 中只声明了一个方法,那就是克隆自身。
下面我们就来看一看原型模式的具体实现:
下面就直接把上面三个类的代码贴出来:
namespace Prototype
{
public abstract class AbstractPrototype
{
private string name;
public string Name
{
get { return name; }
set { name = value; }
}
/// <summary>
/// 在抽象类中声明一个克隆方法
/// </summary>
/// <returns></returns>
public abstract AbstractPrototype Clone();
}
}
namespace Prototype
{
public class ConcretePrototypeA : AbstractPrototype
{
public ConcretePrototypeA(string name)
{
this.Name = name;
}
public override AbstractPrototype Clone()
{
/* 摘自 MSDN:
MemberwiseClone 方法创建一个浅表副本
方法是创建一个新对象
然后将当前对象的非静态字段复制到该新对象
如果字段是值类型的
则对该字段执行逐位复制
如果字段是引用类型
则复制引用但不复制引用的对象
因此,原始对象及其复本引用同一对象。
例如,考虑一个名为 X 的对象,
该对象引用对象 A 和 B,对象 B 又引用对象 C
X 的浅表副本创建一个新对象 X2,该对象也引用对象 A 和 B
与此相对照,X 的深层副本创建一个新对象 X2,
该对象引用新对象 A2 和 B2,它们分别是 A 和 B 的副本
B2 又引用新对象 C2,C2 是 C 的副本
使用实现 ICloneable 接口的类执行对象的浅表或深层复制
*/
return (AbstractPrototype)this.MemberwiseClone();
}
}
}
namespace Prototype
{
public class ConcretePrototypeB : AbstractPrototype
{
public ConcretePrototypeB(string name)
{
this.Name = name;
}
public override AbstractPrototype Clone()
{
return (AbstractPrototype)this.MemberwiseClone();
}
}
}
下面就看看客户端代码得使用了
using System;
using Prototype;
namespace PrototypeTest
{
class Program
{
static void Main(string[] args)
{
//首先是要实例化一个原型对象
AbstractPrototype prototypeA = new ConcretePrototypeA("A");
//根据这个原型对象来克隆一个新的对象
AbstractPrototype prototypeB = prototypeA.Clone();
Console.WriteLine(prototypeA.Name);
Console.WriteLine(prototypeB.Name);
//再修改复制所得的对象
prototypeB.Name = "B";
Console.WriteLine("\n" + prototypeA.Name);
Console.WriteLine(prototypeB.Name);
Console.Read();
}
}
}
效果展示:
上面呢,便对原型模式进行了最简单的实现
原型模式在 . Net 中的实现
在 . Net 中的 System 命名空间下,有这样一个接口 ICloneable ,
在 ICloneable 中只公开了唯一的一个方法,也就是 Clone(),您可以通过实现这个接口来完成 . Net 中原型模式的使用,
和最上面的那个最原始的原型模式相比的话,便可以省去一个抽象类 AbstractPrototype 而使用这个 ICloneable 接口来代替,
所以类图便可以修改为如下:
这样的话,便可以将最上面的代码进行修改了
修改为如下(没有了 AbstractPrototype 这个抽象类)
using System;
namespace PrototypeDotNet
{
public class ConcreteProtoTypeA:ICloneable
{
private string name;
public string Name
{
get { return name; }
set { name = value; }
}
public ConcreteProtoTypeA(string name)
{
this.Name = name;
}
public Object Clone()
{
return (Object)this.MemberwiseClone();
}
}
}
using System;
namespace PrototypeDotNet
{
public class ConcreteProtoTypeB : ICloneable
{
private string name;
public string Name
{
get { return name; }
set { name = value; }
}
public ConcreteProtoTypeB(string name)
{
this.Name = name;
}
public Object Clone()
{
return (Object)this.MemberwiseClone();
}
}
}
using System;
using PrototypeDotNet;
namespace PrototypeDotNetTest
{
class Program
{
static void Main(string[] args)
{
//首先是要实例化一个原型对象
ConcreteProtoTypeA prototypeA = new ConcreteProtoTypeA("A");
//根据这个原型对象来克隆一个新的对象
ConcreteProtoTypeA prototypeB = (ConcreteProtoTypeA)prototypeA.Clone();
Console.WriteLine(prototypeA.Name);
Console.WriteLine(prototypeB.Name);
//再修改复制所得的对象
prototypeB.Name = "B";
Console.WriteLine("\n" + prototypeA.Name);
Console.WriteLine(prototypeB.Name);
Console.ReadKey();
}
}
}
效果展示:
上面呢,便是对原型模式在 . Net 中的实际应用做了一个介绍,
但是,上面的 Demo 中,我们演示的都是一个值类型变量的使用,
比如在整个的 Demo 中,我都是使用的 string name 这种类型
(string 类型虽然是引用类型,但是却在使用时和值类型一样使用,无需使用 new ,所以可以看做是一种值类型),
在最上面的 Demo 中,曾经在 MemberwiseClone()方法的前面摘抄了很长一段的 MSDN 对这个方法的说明,
从说明中可以知道,如果字段是值类型的,那么 MemberwiseClone()会将这个字段完整的复制一遍,
而如果这个字段是引用类型的话,那么,MemberwiseClone()便不会复制这个引用类型所引用的对象,
而只是会复制这个引用,也就是复制地址,
这样的话,便会使得原始的对象以及通过这个原始对象复制而来的克隆体所引用的对象是同一个对象,
有时候,这样做并不能解决我们实际当中的应用,比如,我建立一个员工类,而在员工类中引用了员工信息类,
这样的话,如果,我直接复制一个已经存在的员工(称为老员工)来创建另外一名新员工的话,
那么由于上面提到的问题,也就是 MemberwiseClone()针对引用类型来说,
它并不复制引用的对象,而只是简单的复制引用(也就是指向那个对象的地址)
这样就会出现,新员工的员工信息和老员工的员工信息指向同一块地址,也就是它们引用的是同一个对象,
那么,如果我修改了新员工的基本信息,那么老员工的基本信息就变了,
或者修改了老员工的信息,新员工的信息也会改变,很明显,这是不符合实际的,
所以我们需要解决这个问题,如果要解决上面出现的问题,
也就是让克隆体和原始对象在引用类型上面所引用的对象不是同一个对象,而各自引用各自的对象,
这里就必须提到两个概念了,即浅复制和深复制。
浅复制和深复制
浅复制呢又称为浅拷贝,深复制又称为深拷贝。
浅复制的话,就是通过一个原型实例(这里暂称为老对象)克隆所得到的对象(这里暂时称为新对象),
而这个新对象中所有的值类型变量都含有与老对象相同的值,
但是,新对象所有的对其他对象的引用却是和老对象指向同一个地方,
即对引用类型来说,老对象和新对象指向同一个引用对象。
深复制呢,和浅复制就一点不同,那就是,
新对象所有的对其他对象的引用都是指向了复制过的对象,而不再是和老对象指向同一个对象。
比如,上面提到的员工类中的员工信息类的引用,
如果使用深复制的话,便可以解决上面提出的问题,
使用深复制,可以先把老员工的员工信息类复制一遍(也就是创建了新的员工信息),
然后让新员工的员工信息引用指向这个复制而来的新的员工信息,
为了深刻一点说明这个浅复制和深复制,下面就分别看两个 Demo 吧,一个呢是关于浅复制的,一个关于深复制的,
先看浅复制的 Demo 下面贴出完整的代码
using System;
namespace PrototypeDotNet
{
public class ConcreteProtoTypeA : ICloneable
{
private string name;
private Test test;
public string Name
{
get { return name; }
set { name = value; }
}
public ConcreteProtoTypeA(string name, Test test)
{
this.Name = name;
this.test = test;
}
public Object Clone()
{
return (Object)this.MemberwiseClone();
}
public void SetTest(string test)
{
this.test.TestName = test;
}
public void DisplayTest()
{
Console.WriteLine(this.test.TestName);
}
}
}
namespace PrototypeDotNet
{
public class Test
{
private string testName;
public string TestName
{
get { return testName; }
set { testName = value; }
}
public Test(string name)
{
this.TestName = name;
}
}
}
using System;
using PrototypeDotNet;
namespace PrototypeDotNetTest
{
class Program
{
static void Main(string[] args)
{
Test test = new Test("BaoBeiMe");
//首先是要实例化一个原型对象
ConcreteProtoTypeA prototypeA = new ConcreteProtoTypeA("A", test);
//根据这个原型对象来克隆一个新的对象
ConcreteProtoTypeA prototypeB = (ConcreteProtoTypeA)prototypeA.Clone();
prototypeA.DisplayTest();
prototypeB.DisplayTest();
//修改老对象和新对象共同指向的同一个引用对象 Test
prototypeB.SetTest("BoyXiao");
Console.WriteLine("\n修改值后的结果:\n");
prototypeA.DisplayTest();
prototypeB.DisplayTest();
Console.ReadKey();
}
}
}
效果展示:
从上面这副截图结合上面 Demo 的代码部分就可以看出,老对象和新对象它们两个访问的是同一个对象,
在代码中,我通过新对象来对它们共同引用的对象进行了改变,然后输出结果,
从结果中便可以知道,老对象的引用的对象也改变了,这便充分证明了老对象和新对象引用的是同一个 Test 对象。
下面就该看看深复制的 Demo 了,下面也贴出 Demo 的完整代码
using System;
namespace PrototypeDotNet
{
public class ConcreteProtoTypeA : ICloneable
{
private string name;
private Test test;
public string Name
{
get { return name; }
set { name = value; }
}
public ConcreteProtoTypeA(string name, Test test)
{
this.Name = name;
this.test = test;
}
public Object Clone()
{
//先同过现有的 Test 对象来克隆一个新的 Test 对象
Test myTest = (Test)this.test.Clone();
//然后通过这个新的 Test 对象来实例化一个新的 ConcreteProtoTypeA 对象
return new ConcreteProtoTypeA(this.name, myTest);
}
public void SetTest(string test)
{
this.test.TestName = test;
}
public void DisplayTest()
{
Console.WriteLine(this.test.TestName);
}
}
}
using System;
namespace PrototypeDotNet
{
public class Test : ICloneable
{
private string testName;
public string TestName
{
get { return testName; }
set { testName = value; }
}
public Test(string name)
{
this.TestName = name;
}
/// <summary>
/// 实现一个克隆
/// </summary>
/// <returns></returns>
public Object Clone()
{
return (Object)this.MemberwiseClone();
}
}
}
using System;
using PrototypeDotNet;
namespace PrototypeDotNetTest
{
class Program
{
static void Main(string[] args)
{
Test test = new Test("BaoBeiMe");
//首先是要实例化一个原型对象
ConcreteProtoTypeA prototypeA = new ConcreteProtoTypeA("A", test);
//根据这个原型对象来克隆一个新的对象
ConcreteProtoTypeA prototypeB = (ConcreteProtoTypeA)prototypeA.Clone();
prototypeA.DisplayTest();
prototypeB.DisplayTest();
//修改新对象指向的引用对象 Test
prototypeB.SetTest("BoyXiao");
Console.WriteLine("\n修改值后的结果:\n");
prototypeA.DisplayTest();
prototypeB.DisplayTest();
Console.ReadKey();
}
}
}
效果展示:
从上面的结果中可以看出,我在客户端尽管修改了新对象所引用的对象的内容,
但是老对象所引用的对象的内容还是没有改变的,
所以,可以得出结论,新对象所引用的对象和老对象所引用的对象不再是同一个对象了。
需要提一下的是,在上面的这个 Demo 中,我在 ConcreteProtoTypeA 中的 Clone()方法中,
实际上,还是使用了 new 操作的,而不是真正意义上的使用 Clone(),
所以,这里并不能解决掉最最开始所说的使用 new 时的性能问题,
不过,这也并不是没有办法解决的,因为实现深复制的方式并不止上面这一种,
只不过是上面这一种比较简单而已,常用的呢,还可以通过序列化来实现深复制,
关于这一种方法,大家可以参考 TerryLee 的那篇关于原型模式的博文,里面有介绍也有 Demo 。
关于深复制和浅复制的一个用途呢,比较好的例子就是 DataSet 了
DataSet 提供了 Clone()和 Copy()方法,
Clone()方法呢只是复制 DataSet 的结构,而并不会复制数据,这就是原型模式的浅复制的一个应用了。
Copy()方法的话,是既复制 DataSet 中的结构,而且还会把全部的数据复制过去,很明显,这是原型模式的深复制的应用。