設計模式-原型模式 (Prototype Pattern)

  1. 類別圖
  2. 使用情境
  3. 繼承 ICloneable 的缺點
  4. 泛型深淺複製 Sample

類型: Creational
目的: 透過原型物件來複製出新物件

類別圖

使用情境

當原物件的建立過程繁瑣或是結構複雜,需要快速複製出新物件時,可使用原型模式來達到目的

以C#來說會先建立一個抽象類別,繼承 ICloneable 介面

//
// 摘要:
//     Supports cloning, which creates a new instance of a class with the same value
//     as an existing instance.
[ComVisible(true)]
public interface ICloneable
{
    //
    // 摘要:
    //     Creates a new object that is a copy of the current instance.
    //
    // 傳回:
    //     A new object that is a copy of this instance.
    object Clone();
}

再由繼承該抽象類別的子類別,分別實作其 Clone 方法

範例

using Newtonsoft.Json;
using System;
using System.Collections.Generic;

namespace DesignPattern.Prototype
{
    /// <summary>
    /// 定義:
    /// 使用原型實例指定創建對象的種類,然後通過拷貝這些原型來創建新的對象
    ///
    /// 角色:
    /// 1. Prototype:定義一個抽象方法 Clone [PrototypeBase]
    /// 2. ConcretePrototype: 實現Clone方法 [ConcretePrototype1]
    ///
    /// 缺點:
    /// 1.當有大量子類別需要Clone時,實現Clone的子類別也要各寫一個
    /// 2.Clone有分淺複製和深複製,.Net Framework提供的ICloneable 只有一個Clone方法
    ///   無法明確定義出該Clone是淺複製還是深複製
    /// </summary>
    public class Prototype
    {
        public void Main()
        {
            ConcretePrototype1 obj = new ConcretePrototype1() { Id = 1, IdList = new List<int>() { 1, 2, 3 } };
            ConcretePrototype1 copyObj = (ConcretePrototype1)obj.Clone();

            obj.Id = 10;
            obj.IdList.Clear();
            Console.WriteLine($"obj={JsonConvert.SerializeObject(obj)}");
            Console.WriteLine($"copyObj={JsonConvert.SerializeObject(copyObj)}");
        }
    }

    /// <summary>
    /// (Prototype)
    /// </summary>
    public abstract class PrototypeBase : ICloneable
    {
        // Methods
        public abstract object Clone();
    }

    /// <summary>
    /// (ConcretePrototype)
    /// </summary>
    public class ConcretePrototype1 : PrototypeBase
    {
        public int Id { get; set; }

        public List<int> IdList { get; set; }

        public override object Clone()
        {
            return this.MemberwiseClone();
        }
    }
}

可以觀察到 .Net 原生的 物件複製方法 MemberwiseClone 為淺複製,再複製參考型別的 Property 時並不會建立新的 Reference
進而導致修改參考型別的 Property 會連帶影響到被複製的物件

繼承 ICloneable 的缺點

在 MSDN 上有說明該 MemberwiseClone 為淺複製 Object.MemberwiseClone
而繼承自 ICloneable 的 Clone 方法,從外面直接引用時 並不知道這是深複製還是淺複製

也有人討論過該問題,但似乎沒有個結論 討論連結
我自己覺得這確實是會造成困擾,所以直接建立深複製與淺複製的方法,以免混淆

泛型深淺複製 Sample

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;

namespace DesignPattern.Generic_Prototype
{
    /// <summary>
    /// Prototype泛型改良版
    ///
    /// 以泛型解決Prototype的缺點
    /// 並把Clone定義分為深複製與淺複製以免誤用
    /// </summary>
    public class Generic_Prototype
    {
        public void Main()
        {
            TestClass a = new TestClass() { Id = 1, IdList = new List<int>() { 1, 2, 3 } };
            TestClass shallowCopyObj = PrototypeHelper.ShallowCopy(a);
            TestClass deepCopyObj = PrototypeHelper.DeepCopy(a);

            a.Id = 10;
            a.IdList.Clear();
            Console.WriteLine($"a={JsonConvert.SerializeObject(a)}");
            Console.WriteLine($"shallowCopyObj={JsonConvert.SerializeObject(shallowCopyObj)}");
            Console.WriteLine($"deepCopyObj={JsonConvert.SerializeObject(deepCopyObj)}");
        }
    }

    [Serializable]
    internal class TestClass
    {
        public int Id { get; set; }
        public List<int> IdList { get; set; }
    }

    public static class PrototypeHelper
    {
        // 淺複製
        public static T ShallowCopy<T>(this T targetObject)
        {
            if ((object)targetObject is null)
                throw new ArgumentNullException();

            return (T)targetObject.GetType().GetMethod("MemberwiseClone", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(targetObject, null);
        }

        // 深複製
        public static T DeepCopy<T>(this T targetObject)
        {
            using (var memory = new System.IO.MemoryStream())
            {
                System.Runtime.Serialization.IFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
                formatter.Serialize(memory, targetObject);
                memory.Seek(0, SeekOrigin.Begin);
                return (T)formatter.Deserialize(memory);
            }
        }
    }
}

轉載請註明來源,若有任何錯誤或表達不清楚的地方,歡迎在下方評論區留言,也可以來信至 leozheng0621@gmail.com
如果文章對您有幫助,歡迎斗內(donate),請我喝杯咖啡

斗內💰

×

歡迎斗內

github