--- title: 第 30 章-設計模式 tags: Kent Beck的測試驅動開發:案例導向的逐步解決之道讀書會 --- # 第 30 章-設計模式 我們處理的問題大部分是來自於使用的工具(內部),而不是需求(外部)。因此就算需求不同,還是可以找到某些常見的共同問題的解決方案。這就是模式。 透過物件組織常見、可預測的設計方式,解決內部產生的子問題。設計模式的成功證明了物件導向設計的開發人員所看到的通用性。 這本書的例子有使用到這些設計: ### 命令(P181) 把被呼叫的方法用物件呈現,而非訊息溝通(這裡指的是呼叫方法傳遞的參數)。建立一個物件表示要做的事情,並在建立的同時把執行所需的訊息都準備好,接著就可以用共同的介面直接呼叫了!(像是本書提到的 run 或是 execute) [[Day 9] 初探設計模式 - 命令模式 ( Command Pattern )](https://ithelp.ithome.com.tw/articles/10204425) ### 數值物件(P182) 當建立後狀態就不會再被改變的物件。在 Ch3 有使用到,關於這個物件所有的操作都會回傳一個新的物件,避免別名問題(不同名稱但是指同個物件),導致原本的物件在各種操作後不經意地被改變。 ### 空物件(P183) 表示一個物件的基本型態。建立另外一個例外的物件做特殊情況的操作,例如書中舉的例子: ```javascript= const setReadOnly = () => { const guard = System.getSecurityManager(); if (guard) guard.canWrite(path); return System.setReadOnly(this); } ``` 在每一次取得 SecurityManager 物件的時候都要另外判斷 guard,每取一次就要再判斷一次,為了這種特殊情況可以再另外建立一個類別 LaxSecurity,讓在 guard 為 null 時呼叫 canWrite 不會出錯: ```javascript= class LaxSecurity { canWrite(path) {} } ``` 之後把判斷改寫在 getSecurityManager 裡面: ```javascript= static getSecurityManager() { return security ? security : new LaxSecurity(); } ``` 如此一來外面使用 getSecurityManage 後就不用再判斷 guard 了: ```javascript= const setReadOnly = () => { const guard = System.getSecurityManager(); guard.canWrite(path); return System.setReadOnly(this); } ``` ### 範本方法(P185) 在父類別定義不變的流程,透過繼承實作具體的內容。在程式開發領域有幾種經典流程: * 輸入、處理、輸出 * 送出訊息、接收方回應 * 讀取命令、回傳結果 或是像之前實作的 TestCase 就有 setUp、runTest、tearDown 這樣子的流程。 範本方法通常會在撰寫完實作內容後才設計,當兩個子類別有某個不同的實作內容時,將同樣的部分拉到父類別,並消除子類別中的重複,就是範本方法了。而不是一開始就先設計好「我的步驟就是要這樣子」,避免將來使用 inline 條件式去處理不同的地方,再提取到子類別。 現實的例子:React 的生命週期(class component) ### 插入式物件(P186) 將兩種以上的變化封裝在實作同個抽象的不同物件,提供一致的抽象操作。一般會用條件判斷的方式表達變化,但如果你在某個地方使用條件判斷,這個條件判斷可能也會出現在其他地方。 以書上的例子來說,他現在在做的介面會根據滑鼠所點的地方,來決定接下來執行的行為要做什麼(滑動或放開滑鼠): ```javascript= class SelectionTool { constructor() { this.selected; } mouseDown() { const selected = findFingure(); if (selected) this.selected = selected; } mouseMove() { if (this.selected) { move(selected); } else { moveSelectionRectangle(); } } mouseUp() { if (!this.selected) return selectAll(); } } ``` 面對這種狀況可以用插入式物件,建立有相同介面的兩種物件,在 mouseDown 的時候將特定操作的物件插入到 mode 中,之後就消除重複的散落的判斷式,直接執行 mode: ```javascript= class SelectionTool { constructor() { this.mode; } mouseDown() { const selected = findFingure(); if (selected) { this.mode = SingleSelection(selected); } else { this.mode = MultipleSelection(); } } mouseMove() { this.mode.move(); } mouseUp() { this.mode.selectAll(); } } ``` ### 插入式選擇器(P188) 動態呼叫不同實體的不同方法,避免建立不必要的子類別。如果每個不同實作的子類別都只有一個方法的實作不同,那多建立子類別實在是大材小用,這時候就撰寫一個包含 switch 方法和各種實作的類別,直接呼叫就行了: ```javascript= class Report { constructor() { this.printMessage; } report(printMessage) { this.printMessage = printMessage; } print() { switch(this.printMessage) { case 'printHTML': this.printHTML(); break; case 'printXML': this.printXML(); break; } } printHTML() {} printXML() {} } ``` 接著只要有新的 print 方法就在 Report 裡建立該方法的實作,然後在 print 的 switch 中加入對應的方法名稱就好。另外也可以用 reflection 的方式動態呼叫方法,就省去寫 switch 了: ```javascript= class Report { constructor() { this.printMessage; } report(printMessage) { this.printMessage = printMessage; } print() { this[this.printMessage](); } printHTML() {} printXML() {} } ``` 現實的例子:Git 的 config ### 工廠方法(P189) 透過呼叫方法建立物件的實體,而不是建構式以增加建立物件的靈活性。在 Ch8 中有使用這個方法將建立 Frane 和 Dollar 物件移到 Money 中。 ### 冒名頂替(P190) 在已存在的協議中,引入新的實作物件來引入變化。去實作另外一個物件擁有的所有介面,並把原本的物件改成新的物件。剛剛提到的空物件,和待會會講到的遞迴組合都是冒名頂替的例子。 ### 遞迴組合(P192) 使用一個物件來表示一群物件行為的組合。以 Transaction 和 Account 為例子,Account 裡面的 balance 會根據 Transaction 的 value 算出有多少餘額: ```javascript= class Transaction { constructor(value) { this.value = value; } } class Account { constructor() { this.transactions = []; } balance() { return this.trnasactions.reduce( (result, transaction) => result + transaction.value, 0, ); } } ``` 那如果要變成計算所有 Account 的餘額呢?就不是只以 Transaction 為單位,也有可能是 Account,這時候可以讓 Transaction 和 Account 都用相同的介面來取得 balance: ```javascript= class Transaction { constructor(value) { this.value = value; } balance() { return this.value; } } ``` Account 裡面用來 reduce 的名稱也改成 holdings: ```javascript= class Account { constructor() { this.holdings = []; } balance() { return this.holdings.reduce( (result, holding) => result + transaction.balance(), 0, ); } } ``` 這麼一來不論是 hodings 裡面是 Transaction 或 Account 都沒差,而 Account 也可以成為一個容納很多 Account 的物件。 ### 收集參數(P194) 透過傳遞一個參數物件收集不同物件之間呼叫方法的結果,最後再用它來做事。在 Ch21 用來記錄執行了幾個測試和失敗幾個的計數,就是利用這個收集參數的模式。 ### 單例模式(P195) 如果你的語言沒有全域變數的概念,那就不要使用。