6. 原型模式
Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
《设计模式:可复用面向对象软件的基础》
原型模式是创建型模式中一种比较强调产品个体特征的模式,它本身也服务于具体类型快速变化的环境,只不过它没有独立出一个工厂类来单独负责构造工作,而是通过产品类型自己的复制过程实现新实例的创建的。
原型模式的构造过程就是选择一个对象(被称为原型对象),通过调用它的“克隆”方法(或把它创给某个外部机制,并负责返回克隆结果)获得一个和它一样的对象,这个结果一般称为“克隆结果”或“副本”。
经典模式
在前面的工厂方法、抽象工厂模式等,都是借助其他对象来封装对象new()的过程。但是如果对象类型的变化相对太频繁,有时候“没完没了”地为它们创建各种工厂不值得。这时我们不妨换个思路,把new()的工作放到每个具体类型的内部,我们一般称之为“克隆”。
适用于:
- 如果我们需要的类型不是编译态就已经确定的,而是运行过程中动态选择的。
- 免去“没完没了”地创建工厂类型的工作,避免创建一个与产品类型层次平行的工厂类型层次。
- 如果我们需要某个状态的目标对象,或者类型中有限状态中的某一种。
— 类图在此 —
public interface IProtoType
{
IProtoType Clone();
string Name { get; set; }
}
public class ConcreteProtoType : IProtoType
{
public IProtoType Clone()
{
return (IProtoType)this.MemberwiseClone();
}
public string Name { get; set; }
}
抽象 IProtoType类型定义了所有具有“原型”特征的基本要求,那就是要有一个Clone()方法,而且产生的结果也是抽象的IProtoType自己。因为每个具体“原型”类型都实现了IProtoType,所以最终客户程序所依赖的仅仅是IProtoType,满足了“依赖倒置”原则。
[TestMethod]
public void Test_IProtoType()
{
IProtoType sample = new ConcreteProtoType();
sample.Name = "A";
IProtoType image = sample.Clone();
Assert.IsNotNull(image);
Assert.AreEqual("A", image.Name);
// 副本与样本是不同的实例
Assert.AreNotSame(sample, image);
image.Name = "B";
Assert.AreNotEqual(image.Name, sample.Name);
}
表面模仿还是深入模仿
上面通过Object.MemberwiseClone完成了克隆,但是这个方法创建的是浅表副本。它创建一个新对象,然后将当前对象的非静态字段复制到新对象。如果字段是值类型,这对该字段进行逐位复制,如果字段是引用类型,则复制引用而不是复制引用的对象,因此,样本对象和副本引用了同一个对象。
public class Indicator { }
public interface IProtoType2
{
IProtoType2 Clone();
Indicator Signal { get; set; }
}
public class ConcreteProtoType2 : IProtoType2
{
public IProtoType2 Clone()
{
return (IProtoType2)this.MemberwiseClone();
}
public Indicator Signal { get; set; }
}
Unit Test
[TestMethod]
public void Test_IProtoType()
{
IProtoType2 sample2 = new ConcreteProtoType2();
IProtoType2 image2 = sample2.Clone();
Assert.IsNotNull(image2);
// 副本和样本共享同一个引用成员
Assert.AreSame(sample2.Signal, image2.Signal);
}
深层复制
那么深表复制要怎么做呢?深表复制即针对那些引用类型另外开辟一些内存,把引用目标地址的内容逐个字节的复制一份。
听上去很简单,但实现起来并不容易。经常存在于对象内部的某个成员本身也可能是另一个引用对象的情况,而且很可能会多层嵌套下去。所以不难理解为什么.NET Framework 里带的Clone()方法基本上仅实现了浅表复制,因为它不知道客户程序在使用中如何嵌套。.NET定义了一个名为System.ICloneable的接口,它只是一个Clone()方法,等于告诉开发人员“你按照需要自己定义好了,至于复制的深度你自己拿捏”。
那怎么实现深表复制呢?有两种方法:
- 手工逐层完成。比如碰到引用类型,就自己在新对象里重新new()一个,但是要确保这个new()出来的的确是一个新的独立类型。
- 使用序列化实现。无论是二进制序列化还是XML序列化,编译器会帮助检查,而且会沿着引用和继承关系一查到底。
我一直推荐使用第二种方法,因为一般情况下,使用System.SeriailzableAttribute给目标类型贴个标签即可,二般情况,需要自己定制ISerailizable的序列化过程。
public class SerializationHelper
{
private static readonly IFormatter Formatter = new BinaryFormatter();
public byte[] SerializeObject(object graph)
{
if (graph == null)
throw new ArgumentNullException("graph");
using(MemoryStream stream = new MemoryStream())
{
Formatter.Serialize(stream, graph);
return stream.ToArray();
}
}
public string SerializeObjectToString(object graph)
{
return Convert.ToBase64String(SerializeObject(graph));
}
public T DeserializeObject<T>(byte[] buffer)
{
if (buffer == null || buffer.Length <= 0)
throw new ArgumentNullException("buffer");
using(MemoryStream stream = new MemoryStream(buffer))
{
return (T)Formatter.Deserialize(stream);
}
}
public T DeserializeObjectFromString<T>(string graph)
{
if (string.IsNullOrWhiteSpace(graph))
throw new ArgumentNullException("graph");
return DeserializeObject<T>(Convert.FromBase64String(graph));
}
}
[Serializable]
public class UserInfo
{
public string Name;
public IList<string> Education = new List<string>();
public UserInfo GetShallowCopy()
{
return (UserInfo)this.MemberwiseClone();
}
public UserInfo GetDeepCopy()
{
SerializationHelper helper = new SerializationHelper();
byte[] buffer = helper.SerializeObject(this);
return helper.DeserializeObject<UserInfo>(buffer);
}
}
Unit Test
[TestMethod]
public void Test_ShallowCopy()
{
UserInfo user1 = new UserInfo();
user1.Name = "Joe";
user1.Education.Add("A");
UserInfo user2 = user1.GetShallowCopy();
Assert.IsNotNull(user2);
Assert.AreEqual(user1.Name, user2.Name);
// 相同的引用实例
Assert.AreSame(user1.Education, user2.Education);
}
[TestMethod]
public void Test_DeepCopy()
{
UserInfo user1 = new UserInfo();
user1.Name = "Joe";
user1.Education.Add("A");
UserInfo user2 = user1.GetDeepCopy();
Assert.IsNotNull(user2);
Assert.AreEqual(user1.Name, user2.Name);
// 不同的引用实例
Assert.AreNotSame(user1.Education, user2.Education);
}
稍微保留点个性
以前总觉得做应用的时候谈“个性化”都是服务客户的,后来发现开发人员也是这样,总有个别开发人员会列举“无法辩驳”的要求,要求在总体序列化的过程中定义某几个类成员不会被序列化。
最省事的办法是请他们用另外一个属性,NonSerializedAttribute,把不需要一起处理的属性标记出来。
[Serializable]
public class User
{
public string Name;
[NonSerialized]
public string[] Parents;
public User Clone()
{
SerializationHelper helper = new SerializationHelper();
byte[] buffer = helper.SerializeObject(this);
return helper.DeserializeObject<User>(buffer);
}
}
Unit Test
[TestMethod]
public void Test_CustomCopy_Non()
{
User u1 = new User();
u1.Name = "U";
u1.Parents = new string[2];
Assert.IsNotNull(u1.Parents);
User u2 = u1.Clone();
Assert.IsNull(u2.Parents);
}
定制克隆过程
虽然上面的示例中,配合使用Serializabe和NonSerialized实现了总体序列化情况下有选择地剔除某些属性,但是颗粒度有点粗,个别情况下我们还需要更加定制化的操作,这时候可以选择配合使用System.Serialization.ISerializable 和ICloneable的方式。
[Serializable]
public class Customer : ISerializable
{
public string Name { get; set; }
public int Age { get; set; }
public IList<string> Education = new List<string>();
public Customer() { }
// 还原实例
protected Customer(SerializationInfo info, StreamingContext context)
{
this.Name = info.GetString("Name");
this.Education = (IList<string>)info.GetValue("Education", typeof(IList<string>));
}
// 定制序列化方法
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("Name", this.Name);
IList<string> education = new List<string>();
if (Education != null && Education.Count > 0)
{
int count = this.Education.Count < 3 ? Education.Count : 3;
for (int i = 0; i < count; i++)
{
education.Add(this.Education[i]);
}
}
info.AddValue("Education", education);
}
public Customer Clone()
{
SerializationHelper helper = new SerializationHelper();
byte[] buffer = helper.SerializeObject(this);
return helper.DeserializeObject<Customer>(buffer);
}
}
Unit Test
[TestMethod]
public void Test_CustomCopy_Custom()
{
Customer c = new Customer()
{
Name = "Joe",
Age = 20
};
c.Education.Add("A");
c.Education.Add("B");
c.Education.Add("C");
c.Education.Add("D");
Customer c2 = c.Clone();
Assert.IsNotNull(c2);
Assert.AreEqual(c.Name, c2.Name);
Assert.AreNotEqual(c.Age, c2.Age); // Age 没有序列化
Assert.AreEqual(3, c2.Education.Count); // Education只序列化了前三项
}
这个示例中,对于Customer类,仅需要序列化Name和Education的前三。此时还有一种方法就是修改数据源服务,在中间过程中吧不和规格的内容截留,那就不需要定制序列化过程,使用之前的标记方法即可。
重新定义的原型模式实现
我们有了深层复制的方法,那么我们要做的是重新实现克隆过程,并实现原型模式。
研究之前示例中的Clone()方法,可以发现它们几乎是一模一样的,那么我们就在IProtoType和ConcreteProtoType之间增加一个ProtoTypeBase的抽象类,把Clone()方法的实现提前到此抽象类中实现。
[Serializable]
public abstract class ProtoTypeBase: IProtoType
{
public virtual IProtoType Clone()
{
SerializationHelper helper = new SerializationHelper();
byte[] buffer = helper.SerializeObject(this);
return helper.DeserializeObject<IProtoType>(buffer);
}
public string Name { get; set; }
}
public class ConcreteProtoType : ProtoTypeBase { }
— 类图在此 —
这样做的好处就是以后那些类型用原型也懒得一遍遍编写Clone()的时候,直接继承ProtoTypeBase即可以了。不过这个方法也有一个不足之处,C#是单继承的,如果已经有了基类,那就不能用ProtoTypeBase了。
思考:抽象工厂模式还是原型模式?
在一个具体的语境范围内,例如:一个方法或一个业务实体类内部,我们需要创建型模式返回的一般都是“一组相关或具有依赖关系”的对象,这时候客户程序可以选择获得一个抽象工厂对象或是一组处理好的原型实例。
实现上,抽象工厂常常设计成使用原型类型或工厂方法完成某个抽象类型的创建工作,反过来也一样,有时候原型类型的Clone()方法调用的也是某个抽象工厂的特定创建方法。
到底使用哪种方式要根据是否需要一个汇总机制,以及这个汇总机制自身的稳定性。如果这个“一系列”对象变动很频繁,这时用抽象工厂反而给自己增加工作量,采用原型方法更好;反之,使用抽象工厂更简洁。