重构-改善既有代码的设计:重新组织数据的16种方法(六)
1.Self Encapsulate Field 自封裝字段
間接訪問類的屬性:你直接訪問一個字段,但與字段之間的耦合關系逐漸變得笨拙。為這個字段建立取值/設值函數,并且只以這些函數來訪問字段。
? ? ? ?間接訪問變量的好處是,子類可以通過覆寫一個函數而改變獲取數據的途徑;它還支持更靈活的數據管理方式,例如延遲初始化。
???????如果你想訪問超類中的一個字段,卻又想子類中將對這個變量的訪問改為一個計算后的值,這就是使用Self Encapsulate Field (自封裝字段)的時候。“字段自我封裝”只是第一步。完成自我封裝后,你可以在子類中根據自己的需要隨意覆寫取值/設值函數。
2.?Replace Data Value with Object 對象取代數據值
聚合改為組合:你有一個數據項,需要與其他數據和行為一起使用才有意義。將數據項變成對象。
3.Change value to Reference 將值對象改為引用對象
組合改為重數性關聯(另一個類的一個對象與一個或多個該類對象有關系?Customer類對象可以包含多個order類的對象。):從一個類中衍生出許多彼此相等的實例,希望將它們替換為一個對象。將這個值對象變成引用對象。(Customer是一個實值對象,就算多份訂單屬于一個客戶每個Order對象還是擁有各自的Customer對象。重構的結果是所有的訂單對象共享一個客戶名稱。也就是,一個客戶對象只對應一個客戶名稱。)
? ? 可以將對象分成兩類:reference object(引用對象)和value object(實值對象)。
有一個實值對象,在其中保存了少量的不可修改的數據。你需要給這個對象加入一些可修改的數據,并確保對任何一個對象的修改都能影響到所有引用此對象的地方。這是后就需要將value object(實值對象)變成一個reference object(引用對象)。
? ? ? ? 在許多系統中,都可以對對象做一個有用的分類:引用對象和值對象。要在引用對象和值對象之間做選擇有時并不容易。有時候,你會從一個簡單的值對象開始,在其中保存少量不可修改的數據。而后,你可能會希望給這個對象加入一些可修改數據,并確保對任何一個對象的修改都能影響到所引用此對象的地方。這時候你就需要將這個對象變成引用對象。
4.?Change Reference to Value 將引用對象改為值對象
你有一個引用對象,很小且不可變,而且不易管理。 將它變成一個值對象。
?
? ? ? ?要在引用對象和值對象之間做選擇,有時并不容易。做出選擇后,你常會需要一條回頭路,
???????如果引用對象開始變得難以使用,也許就應該將它改為值對象。引用對象必須被某種方式控制,你總是必須向其控制者請求適當的引用對象。它們可能造成內存區域之間錯綜復雜的關聯。在分布式和并發系統中,不可變的值對象特別有用,因為你無需考慮它們的同步問題。
???????值對象有一個非常重要的特征:它們應該是不可變的。無論何時,只有你調用同一對象的同一個查詢函數,都應該得到同樣結果。如果保證了這一點,就可以放心地以多個對象表示同一個事物。如果值對象是可變的,你就必須確保某個對象的修改會自動更新其他“代表相同事物”的對象。與其如此還不如把它變成引用對象。
???????澄清下“不可變”(immutable)的意思:如果你以money類表示“錢’的概念,其中有”幣種“和”金額“2條信息。那么Money對象通常是一個不可變的值對象。這并非意味你的薪資不能改變,而是意味:如果要改變你的薪資,就需要使用另一個Money對象來取代現有的Money對象,而不是在現有的Money對象上修改。你和Money對象之間的關系可以改變,但Money對象自身不能改變。
5.Replace Array with Object 以對象取代數組
你有一個數組,其中的元素各自代表不同的東西。以對象替換數組,對于數組中的每個元素,以一個字段來表示。
數組時一種常見的用以組織數據的結構。不過,它們應該只用于“以某種順序容納一組相似對象”。有時候你會發現,一個數組容納了多種不同對象,這會給用戶帶來麻煩,因為他們很難記住像“數組的第一個元素是人名”這樣的約定。對象就不同了,你可以運用字段名和函數名來傳達這樣的信息,因此你無需死記它,也無需依賴注釋。而且如果使用對象,你還可以將信息封裝起來。并使用 Move Method (搬移函數)為它加上相關行為。
6.?Duplicate Observed data 復制被監視數據
你有一些領域數據置身于GUI控件中,而領域函數需要訪問這些數據。將該數據復制到一個領域對象中。建立一個Observer模式,用以同步領域對象和GUI對象內的重復數據。
一個分層良好的系統,應該將處理用戶界面和處理業務邏輯的代碼分開。之所以這樣做,原因有以下幾點:1)你可能需要使用不同的用戶界面來表現相同的業務邏輯,如果同時承擔2種責任,用戶界面會變得過分復雜;2)與GUI隔離后,領域對象的維護和演化都會更容易,你甚至可以讓不同的開發者負責不同部分的開發。
???????盡管可以輕松地將“行為”劃分到不同部位,“數據”卻往往不能如此。同一項數據可能既需要內嵌于GUI控件,也需要保存于領域模型里。自從MVC模式出現后,用戶界面框架都使用多層系統來提供某種機制,使你不但可以提供這類數據,并保持它們同步。
???????如果你遇到的代碼是以2層方式開發,業務邏輯被內嵌于用戶界面之中,你就有必要將行為分離出來。其中的主要工作就是函數的分解和搬移。但數據就不同了;你不能僅僅只是移動數據,必須將它復制到新的對象中,并提供相應的同步機制。
?
7.Change Unidirection Association to Bidirectional 將單向關聯改為雙向關聯
兩個類都需要使用對方特性,但其間只有一條單向連接。添加一個反向指針,并使修改函數能夠同時更新2條連接。
開發初期,你可能會在2個類之間建立1條單向連接,使其中一個類可以使用另一個類。隨著時間推移,你可能發現被引用類需要得到其引用者以便進行某些處理。也就是說它需要一個反向指針。但指針是一種單向連接,你不可能反向操作它。通常你可以繞道而行,雖然會耗費一些計算時間,成本還算合理,然后你可以在被引用類中建立一個函數專門負責此行為。但是,有時候想繞過這個問題并不容易,此時就需要建立雙向引用關系,或稱為反向指針。如果使用不當,反向指針和很容易造成混亂;但只要你習慣了這種手法,它們其實并不太復雜。
???????“反向指針”手法有點棘手,所以在你能夠自如運用之前,應該有相應的測試。通常不用花心思去測試訪問函數,因為普通訪問函數的風險沒有高到需要測試的地步,但本重構要求測試訪問函數,所以它是極少數需要添加測試的重構手法之一。
???????本重構運用反向指針實現雙向關聯,其他技術需要其他重構手法。
8.Change Bidirectional ?Association to Unidirection將雙向關聯改為單向關聯
兩個類之間有雙向關聯,但其中一個類如今不再需要另一個類的特性。去除不必要的關聯。雙向關聯很有用,但你必須為它付出代價,那就是維護雙向連接、確保對象被正確創建和刪除而增加的復雜度。而且,由于很多程序員并不習慣使用雙向關聯,它往往成為錯誤之源。
???????大量的雙向連接也很容易造成“僵尸對象”:某個對象本來應該死亡了,卻仍然保留在系統中,因為對它的引用沒有完全清除。
???????此外,雙向關聯也迫使2個類之間有了依賴:對其中任一個類的任何修改,都可能引發另一個類的變化。如果這2個類位于不同的程序集,這種依賴就是程序集之間的相依。過多的跨程序集依賴會造就緊耦合的系統,使得任何一點小小改動就可能造成許多無法預知的后果。
???????只有在真正需要雙向關聯的時候,才該使用它。如果發現雙向關聯不再有存在價值,就應該去掉不必要的一條關聯。
9.Replace Magic Number with Symbolic Constant字面常量取代魔法數
你有一個字面數值,帶有特別含義。創建一個常量,根據其意義為它命名,并將上述的字面數值替換為這個常量。? ? ? ? 在計算科學中,魔法數是歷史悠久的不良現象之一。所謂魔法數是指擁有特殊意義,卻又不能明確表現出這種意義的數字。如果你需要在不同的地點引用同一個邏輯數,魔法數會讓你煩惱不已,因為一旦這些數發生變化,你就必須在程序中找到所有魔法數,并將它們全部修改一遍。就算你不需要修改,要準確指出每個魔法數的用途,也會讓你頗費腦筋。
???????許多語言都允許聲明常量。常量不會造成任何性能開銷,卻可以大大提高代碼的可讀性。
???????進行本項重構之前,你應該先尋找其他替代方案。你應該觀察魔法數任何被使用,而后你往往會發現一種更好的使用方式。如果這個魔法數是個類型碼,請考慮使用 Replace Type Code with Class (以類取代類型碼);如果這個魔法數代表一個數組的長度,請在遍歷數組時,改用數組.length。
10.Encapsulate Field 封裝字段
你的類中存在一個public字段。將它聲明為private,并且提供相應的訪問函數。面向對象的首要原則之一就是封裝,或者稱為“數據隱藏”。按此原則,你絕不應該將數據聲明為public,否則其他對象就有可能訪問甚至修改這項數據,而擁有該數據的對象卻毫無察覺。于是,數據和行為就被分開了。
???????數據聲明為public被看做是一種不好的做法,因為這樣會降低程序的模塊化程度。數據和使用該數據的行為如果集中在一起,一旦情況發生變化,代碼的修改就會比較簡單,因為需要修改的代碼都集中于同一塊地方,而不是星羅棋布地散落在整個程序中。
?????? Encapsulated Field?(封裝字段)是封裝過程的第一步,通過這項重構手法,你可以將數據隱藏起來,并通過相應的訪問函數。但它畢竟只是第一步。如果一個類除了訪問函數外不能提供其他行為,它終究只是一個啞巴類。這樣的;類并不能享受對象技術帶來的好處。實施Encapsulated Field (封裝字段)之后,嘗試尋找用到新建訪問函數的代碼,看看是否可以通過簡單的 Move Method(搬移函數)將它們移到新對象去。
11.Encapsulate Coolection 封裝集合
有一個函數返回一個集合。讓這個函數返回該集合的一個只讀副本,并在這個類中提供添加/移除集合元素的函數。我們常常會在一個類中使用集合來保存一組實例。這樣的類通常也會提供針對該集合的取值/設值函數。
???????但是,集合的處理方式應該和其他種類的數據略有不同。取值函數不該返回集合自身,因為這會讓用戶得以修改集合內容而集合擁有者卻一無所悉。也會對用戶暴露過多對象內部數據結構信息。如果一個取值函數確實需要返回多個值,它應該避免用戶直接操作對象內所保存的集合。并隱藏對象內與用戶無關的數據結構。
???????另外,不應該為這整個集合提供設值函數,但應該提供用以為集合添加/移除元素的函數。這樣,集合擁有者就可以控制集合元素的添加和移除。
???????如果你做到以上幾點,集合就被很好地封裝起來,這便可以降低集合擁有者和用戶之間的耦合度。
12.Replace Record with Data Class 以數據類取代記錄
? ? ? ?你需要面對傳統編程環境中的記錄結構。為該記錄創建一個“啞”數據對象。 ? ? ? ?記錄型結構是許多編程環境的共同性質。有一些理由使它們被帶進面向對象程序之中:你可能面對的是一個遺留程序,也可能需要通過一個傳統API來與記錄結構交流,或是處理從數據庫讀出的記錄。這些時候你就有必要創建一個接口類,用以處理這些外來數據。最簡單的做法就是先建立一個看起來類似外部記錄的類,以便日后將某些字段和函數搬移到這個類中。一個不太常見但非常令人注目的情況是:數組中的每個位置上的元素都有特定含義,這種情況下應該使用 Replace Array with Object (以對象取代數組)。13.?Replace Type Code with Class 以類來取代類型碼
類之中有一個數值類型碼,但它并不影響類的行為。以一個新的類替換該數值類型碼。在以C為基礎的編程語言中,類型碼或枚舉值很常見。如果帶著一個有意義的符號名,類型碼的可讀性還不錯。問題在于,符號名終究只是個別名,編譯器看見的、進行類型檢驗的,還是背后那個數值。任何接受類型碼作為參數的函數,所期望的實際上是一個數值,無法強制使用符號名。這會大大降低代碼的可讀性,從而成為bug之源。
???????如果把那樣的數值換成一個類,編譯器就可以對這個類進行類型檢驗。只要為這個類提供工廠函數,你就可以始終保證只有合法的實例才會被常見出來,而且他們都會被傳遞給正確的宿主對象。
???????但是,在使用Replace Type Code with Class (以類取代類型碼)之前,你應該先考慮類型碼的其他替換方式。只有當類型碼是純粹數據時(也就是類型碼不會在switch語句中引起行為變化時),你才能以類來取代它。更重要的是:任何switch語句都應該運用Replace Conditional with Polymorphism (以多態取代條件表達式)去掉。為了進行那樣的重構,你首先必須運用 Replace Type Code with Subclass (以子類取代類型碼)或Replace Type Code with State/Strategy (以狀態策略取代類型碼),把類型碼處理掉。
???????即使一個類型碼不會因其數值的不同而引起行為上的差異,宿主類中的某些行為還是可能更適合放置于類型碼類中,因此你還應該留意是否有必要使用Move Method(搬移函數)將一兩個函數搬過去。
14.Replace Type Code with Subclasses 以子類來取代類型碼
??你有一個不可變的類型碼,它會影響類的行為。以子類取代這個類型碼。如果你面對的類型碼不會影響宿主類的行為,可以使用Replace Type Code with Class (以類取代類型碼)來處理它們。但如果類型碼會影響宿主類的行為,那么最后的辦法就是借助多態來處理變化行為。
???????一般來說,這種情況的標志就是像switch這樣的條件表達式。這種條件表達式可能有2種表現形式:switch語句或者if –then-else結構。不論哪種形式,它們都是檢查類型碼值,并根據不同的值執行不同的動作。這種情況下,你應該以 Replace Conditional with Polymorphism (以多態取代條件表達式)進行重構。但為了那個順利進行那樣的重構,首先應該將類型碼替換為可擁有多態行為的繼承體系。這樣一個繼承體系應該以類型碼宿主類為基類,并針對每一種類型碼建立一個子類。
???????但是以下2種情況你不能那么做1)類型碼值在對象創建之后發生了變化;2)由于某種原因,類型碼宿主類已經有了子類。如果你恰好面臨這2種情況之一,就需要運用 Replace Type Code with StateStrategy (以狀態策略取代類型碼)。
?????? Replace Type Code with Subclass?(以子類取代類型碼)的主要作用其實是搭建一個舞臺,讓Replace Conditional with Polymorphism (以多態取代條件表達式)得以一展身手。如果宿主類中并沒有出現條件表達式,那么Replace Type Code with Class (以類取代類型碼)更合適,風險也較低。
???????使用Replace Type Code with Subclass (以子類取代類型碼)的另一個原因是,宿主類中出現了“只與具備特定類型碼之對象相關”的特性。完成本項重構后,你可以使用 push down Method (函數下移)和push down field (字段下移)將這些特性推到合適的子類中去,以彰顯它們只與特定情況相關這一事實。
?????? Replace Type Code with Subclass?(以子類取代類型碼)的好處在于:它把“對不同行為的了解”從類用戶那轉移到了類自身。如果需要再加入新的行為變化,只需要添加一個子類就行了。如果沒有多態機制,就必須找到所有條件表達式,并逐一修改它們。因此,如果為了還有可能加入新行為,這項重構將特別有價值。
15.Replace Type Code with State/Strategy 以狀態/策略取代類型碼
你有一個類型碼,它會影響類的行為,但你無法提供繼承手法消除它。以狀態對象取代類型碼。本項重構和Replace Type Code with Subclass (以子類取代類型碼)類似,但如果“類型碼在對象生命期中發生變化”或“其他原因使得宿主類不能被繼承”,你也可以使用本重構。本重構使用State模式和Strategy模式。
?????? State模式和Strategy模式非常相似,因此無論你選擇其中哪一個,重構過程都是一樣的。“選擇哪個模式”并非問題的關鍵所在,你只需要選擇更合適特定情境的模式就行了。如果你打算在完成本重構后再以Replace Conditional with Polymorphism (以多態取代條件表達式)簡化一個算法,那么選擇Strategy模式較合適;如果你打算搬移狀態相關的數據,而且你把新建對象視為一種變遷狀態,就應該選擇State模式。
?
16.Replace Subclass with Fieldls 以字段取代子類
你的各個子類的唯一差別只在“返回常量數據”的函數身上。修改這些函數,使它們返回超類中的某個(新增)只讀,然后銷毀子類。? ? ? ?建立子類的目的,是為了增加新特性或變化其行為。有一種變化行為被稱為“常量函數”,它們會返回一個硬編碼的值。這東西有其用途:你可以讓不同的子類中的同一個訪問函數返回不同的值。你可以在超類中將訪問函數聲明為抽象函數,并在不同子類中讓它返回不同的值。
???????盡管常量函數有其用途,但若與子類中只有常量函數,實在沒有足夠的存在價值。你可以在超類中設計一個與常量函數返回值相應的字段,從而完全除去這樣的子類。如此一來就可以避免因繼承而帶來的額外復雜性。
總結
以上是生活随笔為你收集整理的重构-改善既有代码的设计:重新组织数据的16种方法(六)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 重构-改善既有代码的设计:重构原则(二)
- 下一篇: 重构-改善既有代码的设计:编写代码22宗