Prototype模式为创建型模式,翻译为原型模式。这种模式在生活中随处可见,很多产品设计一般都不会从头开始,都是从上一个版本直接不停的迭代,比如手机界早前的诺基亚“科技以换壳为本”,以及汽车工业界的更新,一般是过一年一个小改版基本就是“facelift”,然后才是大换代。

  在软件工程中亦是如此,在有些情况下,与其从头开始创建一个对象(比如工厂方法模式或者生成器模式做的那样),可以从之前预构造的对象或者直接拷贝原有对象,或者对原有对象简单修改来生成新的对象。

   这就产生了原型模式的概念,通过对某个对象的拷贝,定制化从而得到新的对象,原型模式的核心是拷贝,这也是容易出现问题的地方。

深拷贝与浅拷贝


    拷贝分为深拷贝(Deep copy)和浅拷贝(Shallow copy)之分,区分两者至关重要。下面来看例子,我们定义了Person类。

public class Person
{
    public string Name;
    public readonly Address Address;
    public Person(string name, Address add)
    {
        Name = name;
        Address = add;
    }
}

    其中Address的定义如下:

public class Address
{
    public readonly string StreetName;
    public int HouseNumber;
    public Address(string streetName, int houseNumber)
    {
        StreetName = streetName;
        HouseNumber = houseNumber;
    }
}

   现在假设,张三和老王是隔壁,在初始化完张三后,拷贝张三的信息,然后只需要修改姓名和门牌号,就能完成老王的初始化。

Person zs = new Person("zhangsan", new Address("Tie ling", 1));
Person lw = zs;
lw.Name = "laowang";
lw.Address.HouseNumber = 2;

   运行后,会发现修改lw的名称、门牌,zs的名称、门牌号也发生了变更。问题在于,zs和lw是引用的同一个对象,他们都是class,都是引用类型。修改lw会同时影响到zs的值。我们实际需要的是一个独立的zs的拷贝。这种引用类型的修改,在很多地方不注意非常容易出错,比如方法的参数是某个引用对象,在方法里的修改,同样会影响到外部该对象本身。

ICloneable接口存在的问题


    在.NET Framework中有个接口叫ICloneable接口,该接口只提供了一个Clone()方法。这个方法很晦涩,没有说明这个方法的应有实现,是Deep Copy还是Shallow Copy。ICloneable的典型的实现如下:

public class Person : ICloneable
{
    public string Name;
    public readonly Address Address;
    public Person(string name, Address add)
    {
        Name = name;
        Address = add;
    }

    public object Clone()
    {
        return (Person)MemberwiseClone();
    }
}

    Object.MemberwiseClone() 方法会创建当前对象的浅拷贝,在上面的例子中,如果只实现了Person的浅拷贝,是存在问题的,如下:

Person zs = new Person("zhangsan", new Address("Tie ling", 1));
Person lw = (Person)zs.Clone();
lw.Name = "laowang";
lw.Address.HouseNumber = 2;
Console.WriteLine("Hello World!");

   现在会发现,修改lw的值后,zs的Name虽然没有改变,但是其HouseNumber跟着发生了改变也为2,这是因为Address对象也是引用类型,他们指向的是同一个对象。我们需要的是整个对象的完整的深度拷贝,而不是这样的浅拷贝。

使用特别的接口表示深拷贝


    与用默认的意义不明,非泛型的IClonable接口的Clone()方法不同,一般定义一个明确的表示深拷贝的泛型接口IDeepCopyable

public interface IDeepCopyable<T>
{
       T DeepCopy();
}

  Person实现IDeepCopyable接口

public class Person : IDeepCopyable<Person>
{
    public string Name;
    public Address Address;
    public Person(string name, Address add)
    {
        Name = name;
        Address = add;
    }
    public Person DeepCopy()
    {
        return new Person(Name, Address.DeepCopy());
    }
}

   因为内部有Address引用类,所以Address也需要实现IDeepCopyable接口。

public class Address : IDeepCopyable<Address>
{
    public readonly string StreetName;
    public int HouseNumber;
    public Address(string streetName, int houseNumber)
    {
        StreetName = streetName;
        HouseNumber = houseNumber;
    }

    public Address DeepCopy()
    {
        return new Address(StreetName, HouseNumber);
    }
}

    现在通过实现深拷贝获得的对象,修改也不会影响到原有对象了。

Person zs = new Person("zhangsan", new Address("Tie ling", 1));
Person lw = zs.DeepCopy();
lw.Name = "laowang";
lw.Address.HouseNumber = 2;

   相互修改都不会影响了。自定义的IDeepCopyable相较于ICloneable有两个好处

  • 一是明确该接口是深拷贝,用起来意图明确
  • 二是是泛型接口,不需要额外转换

对象的深拷贝


   .NET 中的对象分为两类:值类型和引用类型。对于值类型,比如int,double等基本类型,以及一些结构体struct如DateTime,Guid,Decimal等,直接通过赋值语句=就可以完成深拷贝,比如:

var dt = new DateTime(2016, 1, 1);
var dt2 = dt; // deep copy!

    String类型很特殊,虽然是引用类型,但是其行为表现看起来跟值类型很像, 直接通过赋值就能实现深拷贝。

    一些其它的类型,比如数组,字典,需要通过其提供的方法来实现深拷贝。比如,对于数组可以使用Array.Copy(),对于Dictionary<,>,可以通过其提供的默认构造函数来进行深拷贝,比如:

var d = new Dictionary<string, int>
{
    ["foo"] = 1,
    ["bar"] = 2
};
var d2 = new Dictionary<string, int>(d);
d2["foo"] = 55;
Console.WriteLine(d["foo"]); // prints 1

   但即便如此,也要非常小心,对于Dictinary里面的复杂类型,特别是里面包含了引用类型的,比如Dictionary<string,Address>。

var d = new Dictionary<string, Address>
{
      ["sherlock"] = new Address {HouseNumber = 221, StreetName ="Baker St"}
};
var d2 = new Dictionary<string, Address>(d);
d2["sherlock"].HouseNumber = 222;
Console.WriteLine(d["sherlock"].HouseNumber); // prints "222"

   引用类型的Address依然引用的是同一个对象。所以为保证每个对象都可以深拷贝,可以对对象分别调用IDeepCopy方法。

var d2 = d.ToDictionary(x => x.Key, x => x.Value.DeepCopy());

   除此之外LINQ语句的一些操作,比如 ToArray()/ToList()/ToDictionary(),也非常有用。

   下面是一些通用方法,来实现深拷贝,比较复杂,首先是对任意类型进行深拷贝。

public static object DeepCopy(object srcobj)
{
    if (srcobj == null)
    {
        return null;
    }

    Type srcObjType = srcobj.GetType();

    // Is simple value type, directly assign
    if (srcObjType.IsValueType)
    {
        return srcobj;
    }
    // Is array
    if (srcObjType.IsArray)
    {
        return DeepCopyArray(srcobj as Array);
    }
    // is List or map
    else if (srcObjType.IsGenericType)
    {
        return DeepCopyGenericType(srcobj);
    }
    // is cloneable
    else if (srcobj is ICloneable)
    {
        // Log informations
        return (srcobj as ICloneable).Clone();
    }
    else
    {
        // Try to do deep copy, create a new copied instance
        object deepCopiedObj = System.Activator.CreateInstance(srcObjType);

        // Find out all fields or properties, do deep copy
        BindingFlags bflags = BindingFlags.DeclaredOnly | BindingFlags.Public
        | BindingFlags.NonPublic | BindingFlags.Instance;
        MemberInfo[] memberCollection = srcObjType.GetMembers(bflags);

        foreach (MemberInfo member in memberCollection)
        {
            if (member.MemberType == MemberTypes.Field)
            {
                FieldInfo field = (FieldInfo)member;
                object fieldValue = field.GetValue(srcobj);
                field.SetValue(deepCopiedObj, DeepCopy(fieldValue));
            }
            else if (member.MemberType == MemberTypes.Property)
            {
                PropertyInfo property = (PropertyInfo)member;
                MethodInfo info = property.GetSetMethod(false);
                if (info != null)
                {
                    object propertyValue = property.GetValue(srcobj, null);
                    property.SetValue(deepCopiedObj, DeepCopy(propertyValue), null);
                }
            }
        }

        return deepCopiedObj;
    }
}

   其中对于数组的深拷贝,Array提供的Copy方法也是浅拷贝,需要遍历逐个深拷贝:

private static Array DeepCopyArray(Array srcArray)
{
    if (srcArray.Length <= 0)
    {
        return null;
    }
    // Create new array instance based on source array
    Array arrayCopied = Array.CreateInstance(srcArray.GetValue(0).GetType(), srcArray.Length);
    // deep copy each object in array
    for (int i = 0; i < srcArray.Length; i++)
    {
        object o = DeepCopy(srcArray.GetValue(i));
        arrayCopied.SetValue(o, i);
    }
    return arrayCopied;
}

   对于List以及Dictionary类型,也是需要逐个拷贝:

private static object DeepCopyGenericType(object srcGeneric)
{
    try
    {
        // Is List 
        IList srcList = srcGeneric as IList;
        if (srcList.Count <= 0)
        {
            return null;
        }

        // Create new List<object> instance
        IList dstList = Activator.CreateInstance(srcList.GetType()) as IList;
        // deep copy each object in List
        foreach (object o in srcList)
        {
            dstList.Add(DeepCopy(o));
        }

        return dstList;
    }
    catch (Exception)
    {
        try
        {
            IDictionary srcDictionary = srcGeneric as IDictionary;
            if (srcDictionary.Count <= 0)
            {
                return null;
            }

            // Create new map instance
            IDictionary dstDictionary = Activator.CreateInstance(srcDictionary.GetType()) as IDictionary;
            // deep copy each object in map
            foreach (object o in srcDictionary.Keys)
            {
                dstDictionary[o] = srcDictionary[o];
            }
            return dstDictionary;
        }
        catch (Exception)
        {
            return null;
        }
    }
}

通过构造函数来实现拷贝


    最简单方式是“拷贝构造函数” 就是该构造函数就是用来生成一个本对象的新对象,通过提供该构造函数,参数类型为对象本身来复制一个新的对象,比如:

public Address(Address other)
{
    StreetAddress = other.StreetAddress;
    City = other.City;
    Country = other.Country;
}

public Person(Person other)
{
    Name = other.Name;
    Address = new Address(other.Address); // uses a copy
}

    用起来也很方便:

var zs = new Person("zhangsan",new Address("tieling", 1));
var lw = new Person(zs); // copy constructor!
lw.Name = "lao wang";
lw.Address.HouseNumber = 321; // zhangsan is still at 123

   需要注意,这里的Name是string,是不可变的,所以可以直接拷贝,如果Name是string数组,则需要使用Array.Copy来执行拷贝了,虽然仍然是浅拷贝。

   “拷贝构造函数”很有用,但是用户不是那么容易发现存在这样拷贝对象的方式,相反明确实现IDeepCopyable接口,提供DeepCopy则更加明确易用。这个方法有个问题是,他要求对象的所有子类型都必须实现拷贝构造函数,如果任何一个子象或者子对象的子对象没有正确实现,那就有麻烦。另外,对于一些已经存在的类型,要其实现拷贝构造函数,就需要修改之前的代码,这显然不符合OCP原则,再者,有些对象我们可能没有权限进行修改。所以这种拷贝构造函数的方式要求比较多,实现起来限制条件比较多。

通过序列化实现深拷贝


   需要庆幸的是,在C#中,无论是值类型,还是引用类型,或者是集合类型等,在将其序列化到文件或者内存中时,他们都是逐步递归序列化的,这不需要修改待序列化对象的代码就能实现。

  因为只要可以序列化到内存或者文件,就能够保留所有信息地反序列化回来,比如使用二进制序列化,将对象序列化到内存,并反序列化回来。

public static T DeepCopy<T>(this T self)
{
    using (var stream = new MemoryStream())
    {
        BinaryFormatter formatter = new BinaryFormatter();
        formatter.Serialize(stream, self);
        stream.Seek(0, SeekOrigin.Begin);
        object copy = formatter.Deserialize(stream);
        return (T)copy;
    }
}

    这种方法很常见,用法也非常简单。

var foo = new Foo { Stuff = 42, Whatever = new Bar { Baz ="abc"} };
var foo2 = foo.DeepCopy();
foo2.Whatever.Baz = "xyz";

    这种使用二进制序列化反序列化实现深拷贝的方式的唯一问题是,待二进制序列化的对象,必须标记为[Serializable]表示可二进制序列化,否则强行序列化会抛出异常,所以我们在定义对象的时候,需要考虑其是否支持二进制序列化。

   如果要使用序列化方法来实现深拷贝,还有一种方式是采用XML序列化,它不要求对象是否有[Serializable]标记。

public static T DeepCopyXML<T>(this T self)
{
    using (var stream = new MemoryStream())
    {
        XmlSerializer formatter = new XmlSerializer(typeof(T));
        formatter.Serialize(stream, self);
        stream.Seek(0, SeekOrigin.Begin);
        return (T)formatter.Deserialize(stream);
    }
}

原型工厂


    假设,某公司办公地点有两个,总部以及分公司,未来的办公地点也不确定,可能还有更多。

static Person main = new Person(null,new Address("123 East Dr", "London", 0));
static Person aux = new Person(null,new Address("123B East Dr", "London", 0));

    现在定义了两个静态的办公地点,人名是空着的,当有新人来时,拷贝这些对象,然后设置名称即可,不能把这个两个静态对象放在Employ对象里,因为将来可能会新增办公地点,这是个变化点,也不符合SRP单一职责原则,所以将其移到EmployFactory中,在EmployFactory工厂中,存储这些静态设置,然后提供一些方法来方便创建新的对象:

public class EmployeeFactory
{
    private static Person main = new Person(null, new Address("123 East Dr", "London", 0));
    private static Person aux = new Person(null, new Address("123B East Dr", "London", 0));
    public static Person NewMainOfficeEmployee(string name, int suite) => NewEmployee(main, name, suite);
    public static Person NewAuxOfficeEmployee(string name, int suite) => NewEmployee(aux, name, suite);
    private static Person NewEmployee(Person proto, string name, int suite)
    {
        var copy = proto.DeepCopy();
        copy.Name = name;
        copy.Address.Suite = suite;
        return copy;
    }
}

    用起来如下:

var john = EmployeeFactory.NewMainOfficeEmployee("John Doe", 100);
var jane = EmployeeFactory.NewAuxOfficeEmployee("Jane Doe", 123);

总结


    原型模式的最主要原理就是对象的深拷贝,相比每一次都需要从头开始初始化一个对象,原型模式可以使得我们能够在现有的对象基础上,通过拷贝,或者简单的修改,就能得到一个跟原对象无关的全新对象。有两种方式能够实现原型模式:

  • 一是通过代码,逐个复制待拷贝对象的每个属性以及子对象实现深拷贝。可以通过在构造函数中提供本类型,来实现拷贝构造器。或者定义一个比较易懂的方法,比如DeepCopy,来返回一个跟本对象一样的全新对象。
  • 二是通过序列化,通过支持对象的二进制或者XML的序列化和反序列化,将对象先序列化到内存中,然后反序列化回来得到新对象,就能实现深拷贝,这种方式会消耗额外的计算资源,需要考虑序列化和反序列化的使用频率。它的优点是,不需要修改原对象,没有侵入性。并且该方法更加安全,不会忘记少了某一个字对象的深度拷贝,或者错误的实现深度拷贝某个对象。

    需要注意,深拷贝都是针对引用类型,对于值类型来说,不存在这个问题。如果想要拷贝一个struct结构体,直接使用赋值语句即可。另外对于string类型,本身就是不可变的,也可以直接通过赋值语句=即可实现深拷贝。

 

参考