2021年6月28日 星期一

[設計模式] 常用的 Clone 也是一種設計模式 !? Prototype Pattern 原型模式 - 深複製 與 淺複製

因為Prototype Pattern實在太常用了,所以微軟直接幫你把它定義好,就是ICloneable,

只要實作它就代表你一定要做出clone功能,那就等於是實現了Prototype Pattern。

Prototype Pattern的目的:實現clone功能,複製出參數相同的物件,不須再對物件做設定

先來看看實現 Prototype Pattern 的架構圖:

Resume物件內除了有自己的變數儲存資訊,

也會有WorkExperence物件來儲存資訊。

搭配程式碼看會比較了解架構。


淺複製 ( Shallow Copy )

Clone大家都再用,但有些特殊情況需要特別注意。

如果複製的是"數值型別",那都不會出現什麼問題,

完整範例可以參考 Chapter_9.1 中宣告為string的資料(PersonalInfo)被複製後的前後變化,

[其實string不完全是數值型別,而是它擁有數值型別特性的特殊參考類型,所以行為和數值型別類似,請參考余大文章。重點即是string的值被修改後,就會重新配置記憶體。]

這個範例使用的是淺複製,也就是Clone()內呼叫的是 MemberwiseClone()


能夠注意到內部被複製的"實質型別"(string)怎麼修改都不會發生問題。

以下提供使用者程式碼與結果做比較, b 和 c 物件都是由 a clone()而來,

請觀察紅框內的設定與結果:

SetPersonalInfo()內設定的是String型別的欄位。



可以觀察到,即使 c 物件重新設定了年齡,對於其他被複製的物件也不會有所影響,

數值型別的clone是複製值給新物件,所以各自獨立。

那麼如果想複製的是"參考型別"呢?

也就是被new出來的物件,此例為WorkExperience物件,

(特別注意!如果沒有new,該物件一樣會被生成,

但該物件內所有參考型別都會被當成實質型別來對待,也就是各自獨立。)

clone則會複製該物件的"參考",也就是說不管複製幾個出來,

新物件全都指向同一個物件,因為參考都一樣阿!


直接來看淺複製對於"使用到的物件(參考型別)"會有什麼影響,

請參考 Chapter_9.1 中對於Clone之後的三個WorkExperence物件變化。

以下提供使用者程式碼與結果做比較,請觀察黃框內的設定與結果:

SetWorkExperience()內設定的是WorkExperience型別的物件。

可以發現最後一個物件 c 設定了WorkExperience物件後,

a 和 b 的WorkExperience物件內的資訊也一併被修改成 c 的資訊了,

因為 a b c 三個所存取的WorkExperience物件都是同一個。

那麼要如何讓 a b c 三者擁有的WorkExperience物件都是各自獨立的呢?

那就要進行深複製了。


深複製 ( Deep Copy )

來看看實現 Prototype Pattern 淺複製與深複製的架構圖:

其實就差在把你想深複製的類別也實作ICloneable而以,

但一樣需要對複製出來的參考物件做初值設定,因為它並不會幫你把值也複製過去,

來看看怎麼做:

先讓WorkExperence實作ICloneable介面。

並在Resume類別內的Clone()加入

"WorkExperence物件的複製"與

"賦值給被複製的參考物件"的片段。

透過新增的建構式來複製被參考的物件。(呼叫WorkExperence內的Clone()來進行物件的複製)

接著以相同的使用者程式碼來看看這一次的改變。

使用者程式碼:

a b c 三個物件都各自設定了WorkExperence參考物件,

但結果卻是各自獨立的,達成了深複製的效果。

這樣的方式有一些缺點:

  • 但總會遇到你要使用的類別不是你寫的吧?不是你寫的類別,沒辦法修改,又該如何幫它實作IClonable呢?
  • 如果WorkExperence類別內的欄位有增減,那麼在使用者端的賦值片段又得去做增減,容易造成疏漏。

因此我認為大話作者的方式只能應用再比較少的情境(類別都是自己寫的),

網路搜尋其他方式,有序列化反射兩種做法,這邊先參考 余小章 大神的文章,

利用序列化的方式進行深複製,那我就依照原本的範例來做修改吧。

(余大內使用的是BinaryFormatter,而官方文件提到 "二進位序列化可能帶來危害。 如需詳細資訊,請參閱BinaryFormatter 安全性指南",請自行參考)

使用序列化進行深複製

首先先將要序列化的類別,也就是會被複製到的相關資料類別(Resume與WorkExperence),

加上序列化屬性標籤,告知編譯器這兩個類別會被序列化,

且WorkExperence也不需要時做ICloneable介面。

並在Resume中的Clone() 改成序列化複製。

結果可以發現一樣達到深複製的效果。


如果我參考的物件內又有包含參考的物件呢?

透過序列話也沒有問題,請參考 Chapter_9.4  ,

我把WorkExperence內的company由string型別改為Company類別,

因此使用者複製Resume時會複製第一層的Resume,

再來是Resume所參考的第二層Company物件,

即使是多層的物件參考,透過序列化的複製一樣能達成。


疑?怎麼沒有實作ICloneable介面?

不需要繼承ICloneable介面的好處是什麼?

如果繼承了ICloneable介面,代表後續其他的子類別也必須要實作該介面,

如果有其他方法可以取代ICloneable,代表我們就不必被強制繼承該介面實作Clone方法。 

但實作介面也是有好處的,當別人接手你的code時,一目了然這個類別具有什麼功能。


適用情境

目前在自動化設備想不到使用情境(代表我自己不常使用到),

可能會這樣複製大多用在資料的使用,

例如某個時間點的狀態儲存,在往後的流程中如果想要回復到當初儲存的狀態,

就能夠把這個儲存狀態作為恢復的依據。


沒有留言:

張貼留言

社會新鮮人如何投資?

我的觀點是,在 沒有很多 本錢 的情況下, 別寄望每個月幾千元放到股票或者最近很夯的高股息ETF就能讓你致富, 先投資自己,讓自己的本業收入提高吧。