# 物件導向軟體工程 - 設計模型
# 參考資料
文章內容多數皆來自 ianjustin39 的文章
與一些個人整理與補充
原文連結
# SOLID Priciple
OOAD 設計原則
- SRP (Single Responsibility Principle) 單一責任原則
- A class as the method they do simalar thing (Highly relative)
- Help for Aggregation
- If seperate the will increase the number of class/interface
- OCP (Open Close Principle) 開閉原則
- Extend or Remove a class easily
- Open for extension close for modification
- LSP (Liskov Substitution Principle) 里式替換原則
- Parent Class has create the concrete method but the child class override the method
- Restrict to use inheritance change to use same interface
- ISP (Interface Segregation Principle) 介面隔離原則
- Similar with previous
- Let concrete class’s method be a abstract method (interface) then inheritance then
- Interface should be segregate with other even if they are similar service
- DIP (Dependence Inversion Principle) 依賴反轉原則
- Parent Class don,t depend on the Sub Class
- Should be rely on higher level abstract class or interface
- Depend an abstract not a concrete
- Also can create instance in a class can reduce coupli
# White/Black Box Test
Reference Link
# RunTime VS ComplieTime
Inheritance Class let Compile time up
Overloading when be executed Run Time Up
# Design Pattern
Design Pattern 大致分成三種模式:
Design Pattern
- Creational Patterns(創建型模式)
- Structural Patterns(結構型模式)
- Behavioral Patterns(行為型模式)
# W2 Strategy/Composite Pattern
# Composite 組合模式
Structural Design Pattern
使用情境: 假設我們有兩個類別 A 與 B,且 A 裡面可以有很多 A 或 B,若要得知所有 object 得總某個的值得總和,就必須從最外層的 A 實例化到最深層的 A 或 B。
使用效果: 宣告一組物件,這些物件 (A 與 B) 會有共同的 Interface,這個 Interface 提供達成上述行為 (計算值的總和) 的方法,若給一個 A 會計算所有 A 包含的 (A B 物件的值總和),給一個 B 會回傳 B 的值,這麼一來行為就會變得很好處理,只有透過他們的公同 Interface (一視同仁) 接著調用其方法物件本身就會將請求向下傳遞到各個節點中,做子結點自己該做的事情!就好比分配一個任務到主管身上,而主管就會去組織他的團隊,向下布達本次任務,再由每個人各司其職,來完成團隊任務!
常用於: 處理樹狀結構,若要新增新的物件到程式碼中,也可以不用修改主程式就能夠新增樹狀結構的新結點了。
Class Diagram:
Skeleton Code:
1 | import java.util.ArrayList; |
特色: 適合處理複雜樹狀結構,透過共同的 Interface 完成
新增新 Object 到程式碼,不需要修改主程式就能建立樹狀結構中的新節點
Better to use Abstract class
參與者:
- Component
- 宣告 Composite 內含物件的 interface
- 替所有 class 共有的 behavior 實作合適的 default behavior
- 宣告 store/access leaf node 的 interface
- (Optional) 宣告存取 parent node 的 interface
- Leaf
- Composite Structure 的終端 object (Leaf 不會有 child node)
- 定義基本物件行為
- 當 Component 宣告 方法但 leaf 無法適用 將方法空實作或 throw Exception
- Composite
- 定義 child node behavior
- store child node
- 實作 Component 中與 child node related interface
- Client
- 透過 Component interface 操作 Composite Object
以上四個角色的合作方式:
Client 透過 Component Interface/Abstract 與 Composite Structure 溝通,如果操作對象是 leaf 會直接操作,如果是 Composite 會將訊息傳給 child node 處理 (在傳遞的前後可能會偷偷的做額外的事情)
優點:
- 定義包含基本物件與複合物件的類別階層。基本物件可以拼出更複雜的物件。後者可以在拚出更更複雜的物件,如此不斷遞迴下去。任何能夠處理基本物件的 Client 程式,也都能處理複合物件。
- 簡化客戶程式碼。Client 能以一致性的角度處理複合結構與個別物件。他通常不會知道 (也不該在意) 所面對的是 Leaf 還是 Composite 如此一來可以簡化 Client 程式,不須而外處理複合結構。
- 更容易添加新的 Component 類型物件,新增的 Composite Or Leaf 的 child class 會自動融入既有的 Structure 也不須修改 Client 程式
缺點:
- 可能使程式設計過於一般化。太容易增加新的 Component class 代表難以對 Composite 所譨包含的 Component 類別進行管控。有時我們會希望對此加以限制,但在 Composite 結構下,就不能仰賴型別系統幫我們進行設限,需要自己檢驗。
# Strategy (Policy) 策略模式
Behavioral Patterns
使用情境:
假設我們一個網站一開始只有一個功能,但隨者使用者需求的增加功能也越來越多,程式也會需要更新,造成一個服務需要很多不同的演算法在每次新增一個演算法的同時該 class 也會增長一倍,若不幸需要更改演算法不論調整輕重,都有很大的機率在該 class 產生 run time error
使用效果:
Strategy Pattern 會把不同的演算法 Algorithm 提取出來把他們單獨成為一個 class,接者會需要 Context class 且套用 Strategy Interface 的 Property 去呼叫該策略的演算法,這樣一來不需要更該 Context class 或其他策略 class 彼此也不會互相影響,有解耦合的效果。
Class Diagram:
Skeleton Code:
1 | class Context { |
特色: 同一個中間物件比如 UI 或其他單一 Client Side Interface 可以處理很多功能的時候很好用,每個小修改都能透過同一介面去調用不同演算法 (行為類似但不相同)
請不要將此 Pattern 與其他 Pattern 結合,如果需要請用其他 Pattern
Strategy 可以 Context set Strategy 也可以反過來也可以互相 Set
參與者:
- Strategy(Compositor)
- 制定所有 algorithm 的共同 Interface。Context 透過這個 Interface 呼叫 ConcreteStrategy (實作演算法)
- ConcreteStrategy(SimpleCompositor)
- 根據 Strategy Interface 實作演算法
- Context(Composition)
- 用 ConcreteStrategy 物件設置 Config
- 持有一個指向 Strategy 物件的 Reference
- 可能會定義 Interface 讓 Strategy 藉以存取自身資料
以上三位角色的合作方式;
- Strategy 與 Context 會協議出該採用的演算法版本。Context 可能會給與一些 Strategy 一些演算法會用到的 Info,也可能索性把自己當成參數傳遞給 Strategy 操作,讓 Strategy 自己來索取想要的資料。
- Context 會將客戶碼送來的 Message 傳遞給 Strategy 物件。客戶碼通常會先建立一個 ConcreteStrategy 物件來餵給 Context,之後就直接和 Context 打交道。通常會有一大堆的 ConcreteStrategy 讓 Client 挑選,
優點:
- 一整族相關的演算法。Strategy 類別定義了一整組的演算法及行為讓 Context 選用,繼承機制可將各演算法的共同功能抽取出來。
- 子類別繼承之外的另一種選擇。我們都知道:繼承機制也能提供多種版本的演算法及行為–只要訂自 Context 的 Child Class 就可以延伸出不同的行為。可是若把演算法實作細節混在 Context 裡,Context 會變得難以理解、維護、擴充,也無法動態改變引用的演算法,更會平白弄出一堆只在演算法行為方面有別的相關類別。如果改將演算法封裝在另一個 Strategy 類別上,就可在不影養 Context 的情況下單獨改變影用的演算法,教易切換、理解、擴充。
- 去除條件判斷式。Strategy Pattern 和條件判斷式只能都能讓我們挑選中意的行為。如果所有可能的選項全都塞在同一個 Class 裡面,就很難不寫條件判斷式:若改將選項封裝在各個 Strategy child class 裡面,就不必寫成雜亂的條件判斷式。
- 可選擇的實作。Strategy 可針對同一行為提供多種不同的實作。讓客戶碼根據監空間考量則依採用。
缺點:
- 客戶碼必須對各種不同的 Strategy 有所認知。這個 Pattern 有個淺在的缺點:客戶碼要先懂得各種 Strategy 之間的異同,才有可能挑選最合用的一種:但如此一來,客戶碼就得對實作細節有某種程度的認知。因此,若這種下意識認知不合理,就不該使用這個 Pattern
- Strategy 跟 Context 之間的通訊負擔。不管 ConcreteStrategy class 所實作的演算法式簡單還是複雜他們共用同一個 Strategy Interface。因此有些東西對 ConcreteStrategy 來說可能稍嫌多餘,簡單的 ConcreteStrategy 甚至根本不會用上半分!這意味者:Context 送給他的初始化參數有時根本沒有用。如果你很在意這一點,不妨考慮將 Strategy 和 Context 做更緊密的結合。
- 物件數目增加。Strategy 會增加應用程式的物件數量。如果把 Strategy 做成不含狀態的物件,就可供 Context 共用;至於其他狀態資訊,則由 Context 所維護,在必要時才當參數交給 Strategy 物件的操作。可共用的 Strategy 不能含有狀態的資訊。
- Strategy pattern 容易去切換 Strategy 但 Algorithm 不一定適合所有 Context
補充:
我們不能確定任何 Context 的 Child class 能套用所有的 Strategy (當你使用糟糕的設計時)
# W3 CH16 Decorator, Abstract Factory Patterns, Bridge (裝飾子、抽象工廠,橋接)
# Decorator 裝飾子模式
Structural Pattern
使用情境: 現在想像一下,你正在開發一個通知的功能,初始的版本非常簡單,就一個 Constructor 和一個簡單的送出方法,隨著時間慢慢發展,你的通知功能也許不只是用簡訊通知,可能會用電子郵件或是其他第三方軟體通知,盡可能滿足用戶的需求。
後來又有個需求,想要一次不只用一種方式通知,也許是用簡訊加電子郵件,或是用電子郵件加第三方軟體通知,還有可能是一次使用三種不同方式去通知用戶,然而你會為了種種的特殊需求去建立新的子類別來解決問題,但一次一次的增加,便會造成程式碼快速的成長,你需要的是一種方法可以建構這種通知類別的結構,使程式碼膨脹的速度趨緩。
使用效果: Decorator 能夠允許在不用改變結構的情況下,對現有的物件添加新的功能,這個模式會創建一個裝飾類別,它將會保持現有類別的結構並增添新的附加功能。
也就是說,我們能夠用最簡單的簡訊通知作為基本功能,其他的電子郵件或是第三方軟體的通知一並做成裝飾者,如果想要有不同組合的通知,只要使用裝飾者就可以將簡訊通知的功能額外再添加另一種通知方式囉!
Class Diagram:
Skeleton Code:
1 | interface Component { |
特色: Concrete Decorator 和 Concrete Component 都是屬於 Component 型,也就是說,不管是單純的簡訊通知,或是是電子郵件加上第三方軟體通知,對用戶來說都是一種通知而已,不須去耦合如此多類型的通知類別,只要將被包裝的物件給包裝者引用,那麼結果就會是一種不一樣的組合行為。
參與者:
- Component
- 制訂可被 Decorator 動態添加權責的物件之介面
- ConcreteComponent
- 定義可被 Decorator 動態添加權責的物件
- Decorator
- 持有指向 Component 的 Reference。需制訂符合 Component 的 Interface
- ConcreteDecorator
- 將額外權責添加到 Component 身上
以上角色合作方式: Decorator 會將訊息傳遞給 Component 去處理,不過在傳遞之前及之後可做些而外的事情。
優點:
- 比靜態的繼承機制更有彈性。同樣是將額外的權責添加到物件身上,但 Decorator Pattern 比靜態 (多重) 繼承更有彈性:只要隨時把 Decorator 掛上去或拿掉就能在執行期動態增刪權責。反觀繼承,每加一項權責就得增設一個 Class,Class 太多, 系統也會變得複雜。此外,只要有不同的單項 Decorator Class,就能自由排列組合疊加想要的功能。Decorator 也更容易將同一個屬性引進兩次。
- 避免將功能過剩的 Class 擺在 class 階層頂端。Decorator 是一種 [貨到付款] 的方式,而不適 [先付款再取貨]。你不必先弄一個未卜先知包羅萬象的複雜 class,可以先弄簡單的,以後再視實際需要以 Decorator 一點一點添加功能上去;Decorator 又可疊加使用,所以應用程式不必為用不到的功能付出代價。我們還能輕易定義新的 Decorator,且仍然與原先被包覆的元件、甚至未知的擴充元件維持互不干擾的獨立地位。如果是用繼承方式,就會將過多功能塞到 Class 階層頂端,導致 child class 塞滿一堆無關功能。
缺點:
- Decorator 與 Component 不同。雖然 Decorator 是一種透明包覆體,但以 [物件等同性] 角度來看,被外覆的 Component 物件就已不再是原先的 Component 物件。所以一旦用了 Decorator,就不能仰賴物件等同性質。
- 小型物件太多了。 用到 Decorator 的系統,往往會有一大堆看起來差不多的小物件,不同之處只在互聯的方式,而不在於所屬 class 或 variable 內容。對懂得人來說,雖然這種系統很容易客製化,但也不意學習與除錯。
# Abstracty Factory 抽象工廠模式
使用情境: 假想現在有一個工廠專門供應運動服裝,有球衣、球褲、球鞋等,有一系列的產品,但隨著事業越做越大,與不同的品牌合作,像是 Nike、Adidas 等,而原本工廠的系統添加新產品或是產品系列的話會需要更改原本的程式,若是越來越多品牌或是商品與之合作,程式只會越來越複雜,且重複相關的程式碼會一直在系統裡排程。再說供應商應該會經常更新他們的產品目錄,我想不會有人希望每次都要更改核心程式碼。
使用效果: 使用抽象工廠模式的話,需要將產品系列(如球衣、球褲、球鞋等)做一個介面,讓不同品牌的產品(如 Nike 球衣、Adidas 球衣等)去實作對應的介面;再來要將品牌作為抽象工廠,其中的方法包含產品系列中的所有產品,也就是球衣、球褲、球鞋等,讓不同品牌的具體工廠去實作,並回傳該品牌的產品。
如此一來,就可以依據抽象工廠創建的品牌工廠類別,去回傳特定品牌的產品系列,舉個例子來說,Nike 工廠類別只會做 Nike 的球衣、Nike 的球褲及 Nike 的球鞋,並不會去做 Adidas 的球衣球褲等等。
簡單來說: 製造類似但不能混再一起設計的時候
Class Diagram:
Skeleton Code:
1 | interface ProductA { |
特色: 雖然具體工廠(如 Factory1)實例化了具體產品(如 ProductA1),但其創建的方法必須回傳相應的抽象產品(如 cteateProductA () 回傳的產品型態是 ProductA,而 ProductA 是抽象界面,並非是是具體類別 ProductA1)。所以客戶端就不需要知道你用的是哪個工廠做了哪個特定的產品,唯一的窗口只有抽象工廠,並在初始化階段定義其出廠型態,如此就可以避免不必要的耦合發生在客戶與產品之間。
參與者:
- AbstractFactory
- 宣告可生成各抽象成品物件的操作
- ConcreteFactory
- 具體實作出可建構具象成品物件的操作
- AbstractProduct
- 宣告某成品物件類型之介面
- ConcreteProduct
- ConcreteFactory 所建構的成品物件
- AbstractProduct Interface 的實作
- Client
- 只觸及 AbstractFactory 與 AbstractProduct 兩抽象類別所訂之 Interface
以上角色合作方式:
- 在執行期,ConcreteFactory class 的物件個體通常只有單獨一個,專門針對某種特定實作陣營建構出具體可用之成品物件。如果想生出其他陣營的成品物件,客戶碼就得改用另一種 ConcreteFactory。
- AbstractFactory 將製造成品物件的責任交付給 child class ConcreteFactory 去處理。
優點:
- 將 Concrete class 隔離開來。AbstractFactory Pattern 讓你掌控 Application 所產生的物件 class。因為 Factory 所創建成品物件的程序與責任封裝起來,將客戶碼隔絕在真正的實作類別之外,因此客戶碼只能透過後者的抽象介面來操控這些物件
- 易於將整族成品物件抽換掉。在相同 Application,ConcreteFactory class name 只會出現一次,只出現在具現之處而已,所以很容易改變 Application 所用之 ConcreteFactory 類型。又因為 AbstractFactory 可生成一整族成品物件,所以只要換成其他 Factory class ,就能一舉將成品物件組態整個換掉。
- 增進成品物件的一致性。 若整族成品物件的設計初衷,本來就是該聚在一起使用,就有必要確保應用程式在同一時間只會使用某一物件陣營。AbstracFactory 目的即在於此。
缺點:
- 難以提供新的成品物件種類 讓 AbstractFactory 能產生新的 Product class,並不是件容易的事,因為它的介面將[能產生哪幾種 Product]寫死了;想擴充此介面,就得修改 AbstractFactory class 及所有 child class。
補充: 添加工廠比添加產品簡單,因為當你添加產品項目需要更改所有現有工廠方法。Mature 的時候使用這個 Pattern,當我們知道我們有很多產品 (且不會增加產品類別) 的時候我們只需要添加 factory 就可以擴廠,為甚麼是 Mature 時候因為添加 Product 有很高的難度。
# Bridge 橋接模式
使用情境: 現在有個交通工具的類別,底下的子類有汽車和機車,你希望加入道路行為的元素去建構不同的交通工具,並將各種道路行為汽車和機車創造了類別,像是會走上高架的機車與只會走平面道路的機車等等,之後如果要新增其他交通工具的子類別,比如說公車、腳踏車等等,就會需要分別對不同道路行為的交通工具塑造子類別,整個程式架構就會往橫向不斷發展,看起來會很糟。
使用效果: 而橋接模式會將交通工具與道路規則給抽離,從原本的繼承關係到聚合關係來解決問題。也就是你可以取一個交通工具的類別,而它的道路行為就用引用的方式來實現,而不是封裝所有的狀態和行為在一個類別中。
使用時機:
- 想避免將 Abstract 與 implememtation 綁再一起 EX: 想在 runtime 期間仍能選擇若切換成不同的實作方式
- 希望用 child class 去個別擴充 abstract 與 implementation。Bridge Pattern 可以讓你自由搭配不同 Abstract 與 implementation 還能個別擴充
- 如果只有 implementation 有變,但 abstract 沒變,就不應該波及 Client Code。也就是說,client code 不必重新編譯。
- 想將 Abstract 的 implementation 完全隱蔽起來不讓外面知道,只讓外面看到 class 套的 interface
- 太多 class: 該設法將 powerful 的 class 階層拆成兩半。Rumbaugh 把這種癡肥的 class 階層叫做 [巢狀一般化 nested generalizations]
- 希望能共享散佈在許多物件裡的實作 Code,但又不能影響 Client Code
Class Diagram:
Skeleton Code:
1 | abstract class Abstractation { |
特色: 橋接模式把「抽象」與「實作」分開來,並讓「抽象」擁有「實作」,形成一個聚合的關係,而這樣的關係也被視為橋接模式定義的一部分。讓兩者可以獨立變化,這也意味著開發者也方便分工。如果你想要組織和劃分具有某些功能(如道路行為)的多個變體(如交通工具),就可以使用橋接模式,來幫助你的程式能夠獨立的加入新的抽象和實作,並且使「抽象」與「實作」更專於自己本身的邏輯與細節,更加符合 Single Responsibility Principle 單一職責原則。
參與者:
- Abstraction
- Abstract 體的 Interface
- 維護指向 Implementor 的物件 Reference
- RefinedAbstraction
- 擴充 Abstraction 所制定的 Interface
- Implementor
- 定義實作 class 的共同 Interface。他未必要和 Abstraction 一樣,而且事實上可以是相去甚遠。Implementor Interface 通常只含最基本的操作,讓 Abstraction 可以組出更高階操作
- ConcreteImplementor
- 實作出 Implementor 所制定的介面
以上角色的合作方式:
Abstraction 將 client message 的要求傳遞給對應的 Implementor 去處理
優點:
- 將 Interface 與實作隔離開來。在 runtime,實作不會與 Interface 綁死,而是可動態綁定的,甚至抽換。拆成 Abstraction 與 Implementr 這兩部分,實作碼就不再有 complie time 的依存關係,如此一來,即使改變所用的實作碼 class,也不須重新 complie Abstraction class 與 client code。這對於維持同一 library 不同版本之間 binary code 相容性來說,是非常重要的性質。隔離兩者也連帶促成更好的分層式設計,系統的高階部分只需知道 Abstraction 及 Implementor 即可。
- 易於擴充。 Abstraction 與 Implementor 兩體系均可個別擴充。
- 隱藏內部實作細節。 Client Code 看步道諸如共享實作物件、參用計數等實作細節。
缺點:
補充:
Abstraction 是 Handle (控制者),Implentator 是 body (事務) 會處理一些事情。
Bridge Pattern 只會知道 Abstraction 會隱藏掉所有 Implementor (隱藏操作)。
與 Strategy Pattern 差異 Strategy 是 Behavior Pattern Bridge 是 Structral Pattern 前者著重在行為套用,後者著重階層關係。
Bridge Pattern 能夠套用所有 Implementor 但 Strategy 不行,這是要根據你所選擇的標的,比如今天的標的是 Bag 可以有 Wallet backpack child class 他們都有 color texture 等參數這時候的 Implentor 就會像是更改顏色更改材質 (更改 properties) 這種非行為做為。
# W4 CH16 Command Visitor Iterator (命令、訪問者、迭代器模式)
# Iterator 迭代器模式
使用情境: 假設我們需要去維護一個有迭代物件 (List,Map,Set,Iterator,Collection) 的程式,那這個迭代物件一定要使用某種方法去走訪所有迭代物件中的物件,然而可能因為需求不同而需要不一樣的走訪方法添加到迭代物件中,這就逐漸模糊了迭代物件的主要職務,除此之外由於迭代物件提供不同走訪元素的方法,因此 Client Side 程式需要與特定迭代物件類別耦合。
使用效果: 若使用 Iterator Pattern,就會將迭代物件的走訪行為封裝到一個 Iterator class 有多少走訪方法就有多少個 Iterator class 並且都實作相同的 Interface,且多個 Iterator class 還可以走訪同一個迭代物件,彼此獨立開來,如果需要一個特別的方法來走訪迭代物件,只需要創建一個新的 Iterator class 不需要去更改迭代物件或是 Client Side Program
Class Diagram:
Skeleton Code:
1 | interface Iterator { |
特色: 適合處理複雜的數據結構
想要對 Client side 隱藏其複雜性,透過適合的 Iterator 去處理 Client side 需求與集合元素,也可以並行迭代
每個 Iterator 都有自己的狀態彼此互相不影響,且實現了 OCP (開閉原則),新增的 class 也不會影響原有的城市
Iterator 雖然好處多但假設只是處理簡單的結構 Ex:Set.Array 就有點殺雞焉用牛刀了!
使用時機:
- 即使對聚合物件內部結構一無所知。仍可存取物件內容。
- 想以多種方式尋訪聚合物件。
- 想一致介面尋訪各種不同得聚合物件時 (多型尋訪)
參與者:
- Iterator
- 宣告存取尋訪元素介面。
- Concrete Iterator
- 具體實作出 Iterator Interface
- 記載正在尋訪的現行元素位置
- Aggregate
- 此 Interface 宣告車建立 Iterator 物件之操作
- ConcreteAggregate
- 具體實作出建立 Iterator 物件的 Interface,傳回適當的 ConcreteIterator 物件個體。
以上角色合作方式:
- ConcreteIterator 記載以尋訪到的 Aggregate 哪一個元素,並可依尋訪下一個。
優點:
- 可提供多尋訪方式。 複雜的 Aggregate 有許多可能的尋訪方式,像產生機械碼及語意檢查都需要尋訪語法剖析樹,尋訪方式可能會適中序或前序。只要改用另一個 Iterator,即可輕易改變尋訪演算法。也可以衍生出新的 Iterator child class 以提供悉的尋訪方式。
- Iterator 可簡化 Aggregate Interface。 有了 Iterface 尋訪 Interface,Aggregate 就不必再重寫一次,所以可簡化 Aggregate Interface。
- 針對同一聚合體,可以同時存在許多尋訪者駐足其中。 Iterface 物件會記載自己尋訪狀態,所以可同時有好幾個一起活動。
實作:
- 誰來控制過程?
- 首要問題是:究竟是誰在主導尋訪過程?是 Iterator 還是 Client Code (Main)
- Iterator 主導 (內部 Iterator): Iterator 會自己移動,並在過程逐一執行客戶所交代的動作。
- Client Code 主導 (外部 Iterator): 必須親自叫 Iterator 向前移動
- 誰來定義尋訪演算法?
- 並不只有 Iterator 才能定義尋訪演算法,Aggregate 也會定義
- Aggregate 負責尋訪:此時 Iterator 便只是來拿紀錄尋訪狀態而已;這種 iterator 叫做游標 (cursor),因為他僅是指向 Aggregate 內的位置而已。Client Code 通常會以 Cursor 當參數呼叫 Aggregate 的 Next () action 操作。Next () 則會改變 Cursor 狀態。
- Iterator 負責尋訪:就很容易用不同方式尋訪同一個 Aggregate,也容易再利用同一套演算法來尋訪其他 Aggregate。然而,尋訪演算法可能需要存取 Aggregate 的 private variable,這會違反 Aggregate 的封裝原則。
- Iterator 有多穩健?
- 再尋訪 Aggregate 的同時還去改變聚合體內容是很危險的;若有增刪元素之舉,可能就會存取到同一元素兩次,也可能完全沒看到。簡單的解法是先將 Aggregate 複製一份,再去尋訪這個複製品;但通常頗耗時間。
- 穩健 (robust) Iterator 不必複製一份,也能保證增刪元素部會干擾到尋訪過程。
- 其他額外的 Iterator 操作:
- 最小的 Iterator 會包含 First () Next () IsDone () CurrenetItem ()
- Iterator 可以有存取特權:
- 為 Composite 設的:
- Null Iterator
# Vistor 訪問者模式
Behavior Pattern
使用情境: 想像一下有位經驗豐富的保險經紀人,為了擁有更多的客戶,他必須對於不同的對象去設計不同的保單,比方說對於一般民眾,可能會賣的是醫療相關的保單;而面對公司戶的話,可能是針對竊盜險、天然災害等等類型的保單,由此對症下藥,提高成交的機率。以上述例子來看,保險經紀人就相當於訪問者模式中的 Visitor,他擁有不同的做法(不同類型的保單)去對應不一樣的客戶(也就是實體物件),而要實現的做法端看於是哪一個客戶而定。
使用效果: 也就是說,當套用到訪問者模式的時候,客戶端不會調用需要的方法,而是讓物件本身(也就是客戶)作為參數並傳遞給訪問者物件(保險經紀人),這時就會發揮物件導向的特性,多型(在第三天有介紹到),訪問者會依據傳遞過來的物件去執行該物件所需的訪問方法。
雖然這樣的結構若是要添加其它行為,還是得修改物件的程式,但至少修改的程式不至於影響程式的運作,只需要實現一個新的訪問者類別就可以了。
Class Diagram:
Skeleton Code:
1 | interface Element { |
特色: Visitor Pattern 把 object 跟 behavior 分開
當操作邏輯發生變化 -> 只需要更改 Visitor method 而不是在實體 object 更改 (Element),
如果要添加新的方法 -> 修改 Visitor Interface 以及被 implement 的 concrete class 原有物件不受影響 (Element)
目的: 定義能逐一施行於物件結構裡各個元素的操作,讓你不必修改作用對象的 class interface,就能定義新的操作。
時機:
- 如果物件結構含有許多介面各異的 class,希望能依據物件的 Concrete class 來執行對應的動作時。
- 需要對整個物件結構體系執行各種互不相干的操作,又不希望將操作全都塞入這些 class 裡。Vistor 可將操作集中置於另一個 class 身上,即使同一份物件結構會被許多程式所共用,彼此也互不干擾。
- 當物件結構的 class 不常變動,但施於其上的操作卻常有增減時,可考慮使用 Vistor。不過,物件結構的 class 一旦有變。就得連帶修改每一個 Vistor Interface,代價很高;因此,萬一物件結構的 class 本來就經常變動,或許將操作直接在這些 class 裡會比較好。
參與者:
- Vistor
- 替物件結構裡的每一種 ConcreteElement class 制定對應的 Visit () 操作。Visit 可由操作的名子和外貌簽名 (signature) 區分出是哪一個 ConcreteElement class 送來的訊息。再透過該 ConcreteElement 特有的 Interface 來存取元素內容。
- ConcreteVistor
- 具體實作出 Vistor 所定的 Interface。每一個操作都針對物件結構中某一物件 class 實作出演算法的一部分,演算法可從 ConcreteVistor 得知目前的脈絡 (context) ,在整個尋訪過程中均可在此儲存局部狀態已累積資訊。
- Element
- 制定以 Vistor 物件當參數的 Accpet () Interface
- ConcreteElement
- 具體實作出 Element 所訂的 Accept ()
- Client
- 可尋訪整群 Element 物件
- 提供高階 Interface 讓 Vistor 尋訪 Element
- 可以是 Composite 也可以容器 (List Array Set)。
以上角色合作方式:
- 使用 Visitor Pattern 的 Client Code,必須先建立一個 ConcreteVistor 物件,再於尋訪物件結構過程中,將這個 Vistor 物件餵给每一個遇到的 Element。
- 當 Element 被尋訪到時,會呼叫 Vistor 的對應操作;如有必要,也會把自己列入參數送過去,讓 Vistor 藉以存取自己的內容。
優點:
- 容易添加新的操作。 只要新增一個 Vistor,即可輕易增添一項與複雜結構內個別元素相關的操作;反之這些操作全都散布在許多元素類別裡,就得修改每一個類別才能增添新的操作。
- 每一種 Vistor 都將相關操作集中起來,將無關的屏除在外。 相關的行為不再散置於物件結構類別裡,而是集中於一個 Vistor 上;無關的行為則切到其他 Vistor child class 裡。這可簡化物件結構元素的 class 以及 Vistor 所含的演算法,演算法所需的資料結構也都藏在 Vistor 裡不對外曝光。
- 難以增添新的 ConcreteElement class。 Vistor Pattern 難以增設新的 Element child class: 每增加一種 ConcreteElement,就得替 Vistor 增加一種 abstract method,每一個 ConcreteVistor 也都得時作之。有時 Vistor 可提供內定 (default method) 的實作版本公 Child class 沿用,但這是例外,非常態。 因此,再採用 Vistor Pattern 之前請先思考:最可能變動的,究竟是施於物件結構的演算法呢,還是物件結構的組成 class ? 萬一常有新的 ConcreteElement 出現,Vistor 類別階層維護起來就很辛苦;或許將操作寫在物件結構的組成類別裡還更簡單呢。維有當 Element 類別階層已經夠穩定了,而你常會增加新的操作或更改演算法時,才適合用 Vistor 幫你管理變局。
- 處理不同的類別階層。 我們可用 Iterator 尋訪複合結構裡的物件並呼叫它們的操作,但同一個 Iterator 物件無法尋訪異質的物件結構 (這意味者 Iterator 只能尋訪 Item 或 Item child class 內的元素)。但 Vistor Pattern 沒有這種限制:即使物件沒有共同的親代 class,只要將它們逐一列入 Vistor Interface ,就能處理。
- 累積狀態。 Vistor Pattern 可在拜訪物件結構各個元素時順便累積狀態。如果沒有 Vistor,就得將截至目前為止所累積的狀態當參數餵給主導尋訪的操作,否則就得設成全域變數。
缺點:
- 破壞封裝性: Vistor Pattern 假設 ConcreteElement Interface 已經強大到足以讓 Visor 做任何想做的事,可是這會迫使你公開一些可存取 Element internal 狀態的操作,破壞物件封裝原則。
實作重點:
- 雙重分派。 以效果而言,Visitor Pattern 不須改變 class 類榮即可添加新的操作進去;這正是著名的雙重分派技術 (double dispatch)。有些程式直接提供這種能力 (EX:CLOS),至於 C++ 與 Small talk 就只有單一分派 (single dispatch)。
- 在單一分派語言裡,事由 [訊息名字] 及 [訊息接收者型別] 兩項便因共同決定起用哪一個操作,向 "GenerateCode" 會啟用哪一個版本,得看對象是哪一種 Node 而定。在 C++ 語言裡,若對一個 VariableRefNode 物件呼叫 GenerateCode (),就會啟用 VariableRedNode::GenerateCode 這個本版 (會產生變數存取的機械碼); 若對象改為為 AssignmentNode 物件,則會啟用 AssignmentNode::GenerateCode (會產生設值動作的機械碼)。因此,會啟用哪一個操作,是由 [訊息類型] 及 [訊息接受者型別] 兩者共同決定。
- 雙重分派則是由 [訊息類型] 及 [兩個訊息接收者型別] 三者共同決定,像 Accept () 就是一種雙重分派的操作,它的意義取決於兩個型別;Vistor 和 Element。雙重分派技術讓 Visitor 能針對各種不同的 Element 類型啟用不同版本的操作。
- 這正是 Vistor Pattern 的關鍵:真正會啟用的操作,取決於 Vistor 的型別與作用對象 Element 的型別。我們不必再將操作靜態繫結在 Element Interface 裡,可先將操作寫在 Vistor 裡,等到 Runtime 才利用 Accept () 繫結想要的版本。如此一來,想擴充 Element Interface,只須定義新的 Visitor child class,不必定義一大堆新的 Element child class
- 誰負責尋訪物件結構?
- 既然有雙重分派,那麼為甚麼不也弄個三重,多重 可根據任意數量的型別決定啟用的操作。
特色:
- 適用於頻率高的功能。
- Increase 精確度
- 當我們想要加操作元素的方法的時候,我們可以將供譨抽出來放置在 Visitor 上面。
- 高內聚 cohesion 非相關操作分開在不同的 Class
- 容易加 vistor
- 可以累計狀態
- visitor 可以是 singleton 單例,假設他不包含實例或共享變數。
# Command 命令模式
Behavior Pattern
使用情境: 通常一個表單上至少有一個按鈕,可能是清除所有文字框的內容,或是要取消填寫表單,送出表單等等,在最初的程式可能會將所有的按鈕都創建一個類別,並封裝其所需要的資訊,來完成按鈕的功能,這看起來似乎沒什麼問題。不過後來發現,某些功能的程式一直重複出現在不同的類別,換句話說,不同的按鈕實現了相同的功能,不一樣的地方可能只有細微的變化,這樣的設計感覺有點問題。
使用效果: 而命令模式會將所有與需求相關的資訊(包括作用的物件、方法名稱等等)傳達給一個單獨的命令類別,而不同的命令類別會實作一個相同的介面,而這個介面就可以讓發送者不必耦合各個具體的命令類別。如此一來,所有的按鈕功能都會在不同的命令類別實現,也因為這樣,就不會有重複的程式碼出現。
Class Diagram:
Skeleton Code:
1 | interface Command { |
特色: 使用命令模式,可以不用更改我們現有的程式就可以添加新的命令,使程式擴展非常方便,而且也減少了調用者與接收者之間的耦合,甚至可以將一組簡單的命令組合成一個複雜的命令,使用的彈性非常高。若是想要實現一個低耦合的設計,不想讓客戶端程式有太多可避免的資訊,就可以使用命令模式來幫助我們做到這點。
使用時機:
- 想用 [欲執行之動作] 來參數化物件時。
- 想在不同時間指定、暫佇 (queue)、執行命令時。Command 物件的生命週期不受訊息發送者限制,如果訊息接收者可用與訂址空間無關的方式表達,就可將 Command 物件傳遞到另一個行程去執行。
- 支援復原功能。Command 的 Execute () 可儲存狀態已被復原;Command 介面裡也可有個 Unexecute () 以抵銷前一次的 Execute () 效果。執行過的 Command 會存在歷程表列中,只要使用 Unexecute () 和 execute () 來回穿梭於歷程列表中,就可做出無限次數的復原與重做。
- 支援日誌功能,系統損壞時能再重新執行過。Command 介面裡只要有載入與儲存操作,就能永久記錄變動日誌;系統若損毀,就從磁碟載入命令日誌,再逐一 Execute () 所載命令。
- 以基礎操作組出高階操作系統,常見於支援交易功能 (transaction) 的資訊系統中。交易是一連串資料異動的封裝體。如果用 Command Pattern 描述這種交易,因為每一種 Command 都有共同介面,所以可用同樣的方式進行每一筆交易,也更容易衍生出新的交易類型。
參與者:
- Command
- 制定執行命令之介面
- ConcreteCommand
- 將 Receiver 物件和對應的動作繫結起來
- 實作 Receiver 物件和對應動作
- Client
- 建立 ConcreteCommand 物件並設定他的 Receiver
- Invoker
- 要求 Command 執行命令
- Reciever
- 知道如何根據收到的訊息執行命令。任何類別都能當作 Receiver
以上角色合作方式:
- Client 建立 ConcreteCommand 物件並設定 Receiver
- Invoker 物件會將 ConcerteCommand 物件存起來
- Invoker 呼叫所存 Command 之 Execute () ,要求它去執行命令。若 Command 是可復原的,ConcreteCommand 就會在執行 Execute () 之前先儲存狀態。
- ConcreteCommand 物件呼叫所載 Receiver 之操作,以真正執行命令。
優點:
- 降低耦合度 - 易擴充 - 容易設計組合命令
- Command 將 [引發命令的物件] 與 [知道如何執行的物件] 隔離開來。
- Command 是一級物件 (first-class object),可和一般物件一樣使用及擴充。
- 可將個別命令組合成複合命令,如 MacroCommand class。複合命令通常會做成 Composite Pattern
- 容易添加新的 Command class ,不必更改既有 class。
缺點:
- 若系統需要非常大量的 Command,會影像到 Command Pattern 的使用
特色:
- 可以將 Reciever 設置成有多種 child class type 也可以在 Command 裡面設立作用所有於所有 Reciever 的方法
- Command 可以設置 unexecute () method
- Concrete Command 實際上是去呼叫那些共用 Interface 的 child class 的實作方法。比如有個 ConcreteCommand 叫做 CopyArticleCommand 他可以作用於不同 Receiver (不同 Documnet) 上個內部實作細節
- 可以用不同的 DataStructure 來當作 Invoker 儲存 Command 的地方。
- 創建 ConcreteCommand Invoker 都需要將 Receiver 當作參數傳進個別的 Constructor
# W5 CH10 Singleton CH17 Factory CH16 Memento (單例、工廠、備忘錄模式)
# Singleton (單例模式)
使用情境: 程式對於共享資源 (資料庫) 的存取操作,我們的資料庫連結器一個程式就會去限制只能 Instanced 實例化出一個 object,來避免部必要的資源浪費
使用效果: 假設今天你創建了一個物件,過了一段時間以後又再創了一個新的物件,但因為之前已經又創過了這個物件,所以存取的物件會是已經創好的物件,而不是新創的物件
Class Diagram:
Skeleton Code:
1 | class Singleton { |
特色:
把 Constructor private 化無法從外界實例化,當需要實例化或取得 singleton object 的時候只需要呼叫 getInstance () 就可以得到實例化的 object 的,且確保該物件永遠只會有一個,只會在第一次的呼叫方法的時候才被初始化。
優點:
- 掌控此為一個體的所有存取動作。
- 減少命名空間 (namespace)
- 操作內部結構仍有擴充空間。
- 允許不定個數的物件個體。
- 比類級別操作更富彈性。
# Factory 工廠模式
Creational Pattern
使用情境: 想像一下,一開始的應用程式只可以接受現金支付,但隨著科技的進步,大眾的消費方式也逐漸多元化,這時只能接受現金支付的應用程式為了跟上消費者的需求,就必須整合其他支付方式到應用程式中。
但是大多數的程式都與「現金」這個類別耦合,如果要新增其他類別,就必須得完整改過有關支付功能程式,才能夠整合完另一種之支付方式,維護成本相當高。 倘若日後又要新增其他類別,到最後程式碼看起來肯定非常混亂且許多地方重複。
使用效果: 使用工廠方法的話,會將「支付方式」作為一個工廠類別,而「支付的東西」作為產品的介面,不同的支付方式就會由不同的「支付工廠」(像是現金支付、LINE PAY、APPLE PAY 等)去創建對應的「支付的東西」(像是現金、LINE PAY 的 QRcode、APPLE PAY 感應等)。
Class Diagram:
Skeleton Code:
1 | interface Product { |
特色: 如果希望程式可以方便擴展內部組件的方法的話,使用工廠方法就可以滿足此需求,也可以避免重複的程式碼在應用程式運作中工作,以節省系統的資源。
目的:
定義可資生成物件的 Interface,但讓 child class 去決定該具現哪一種 class 的 object,this pattern 可以讓 class concrete 化程序交付給 child class 去處置。
時機:
- 當 class 無法明指欲生成的物件類別時。
- 當 class 希望 child class 去指定生成的物件類型。
- 當 class 將瘸力下放給一個或多個輔助用途的 child class,你又希望將 [交付給那些 child class] 的知識集中在一處時。
參與者:
- Product
- 定義 factory method 所造物件之介面
- ConcreteProduct
- 具體時做出 Product Interface
- Creator
- 宣告 factory method,他會回傳 Product 類型的物件。factory method 預設內容是回傳預設的 ConcreteProduct 物件。
- ConcreteCreator
- 覆寫 factory method 以回傳 ConcreteProduct 的物件個體。
以上角色合作方式:
- Creator 的 child class 必須定義 factory method 實體,以回傳適切的 ConcreteProduct 物件個體。
優點:
- 替 child class 預留掛勾 (Hook)
- 連接平行地位 (parallel) class 階層
簡單工廠根據參數建立實例
工廠方法是是 Client 去委託工廠去製造不同類別物件
Factory Method Vs Simple Factory
Factory VS Abstract Factory
# Memento 備忘錄模式
使用情境: 現在有個文字編輯器,在執行任何操作之前,此編輯器就會記錄所有物件的狀態並且儲存到一個地方去,而當用戶想要恢復上一個狀態的時候,編輯器就會從歷史紀錄中拿到最近一筆的物件快照來恢復所有物件的狀態。但如果要產生這些物件快照的話,就會需要取的物件的所有屬性才能夠複製到儲存的地方去,但是大多數的物件不會開放所有屬性給其它人存取,有些重要的資訊會存成私有屬性,這可能就會造成物件快照不完全。
使用效果: 備忘錄模式會將創建物件快照的方法封裝到實際所有者(Originator)本身,因此不是其它物件從外部複製編輯器的狀態,是編輯器本身可以自己創建快照,如此就沒有公有、私有屬性的存取問題了。這些快照會儲存在 Memento 類別中,再由 Caretaker 類別統一做管理,但不能修改 Memento 中的狀態,若是想要恢復上一個狀態的話,Originator 可以存取 Memento 中的所有屬性來恢復。
Class Diagram:
Skeleton Code:
1 | import java.util.LinkedList; |
特色: Memento 很適合做上一步、下一步的功能,可以在不違反封裝 (Encapsulation) 的情況下生成物件快照 (物件當下內容),這些快照會讓 Caretaker 管理,當作 Originator 狀態的歷史紀錄,使用者有需要的時候就可以拿來做恢復的動作,不過使用者過於頻繁創建 Memento 系統會消耗過多的記憶體。
動機:
有時我們需要紀錄物件內部狀態,譬如說:取消動作的復原機制 (undo),或者修復錯誤回到前一個查核點 (checkpoint),都需要先將物件狀態另存某處,才能將物件弄回前一個狀態。可是物件通常都會將部分狀態封裝起來不讓外界觸碰,也無法存到外面;如果硬將內部狀態曝光,又會違反封裝性,有損應用程式的穩固及擴充能力。
參與者:
- Memento
- 存放 Originator 物件的內部狀態。存放資訊的多寡,要試 Originator 的狀況而定。
- 避免 Originator 以外的人存取他。基本上 Memento 具有兩個介面:CareTaker 只看的到窄介面–只能將 Memento 整個地給其他物件;Originator 就能看到寬介面–可存取所有資料,以便回復至前一狀態。理論上,只有產生這個 Memento 物件的那個 Originator 才有權存取內部狀態。
- Originator
- 根據自己的現行狀態建立 Memento 物件。
- 利用 Memento 回復自己的內部狀態。
- CareTaker
- 負責 Memento 物件的安全。
- 絕不會操弄或檢視 Memento 的內容
- Client Code 也可以當 Caretaker
以上角色合作方式:
- CareTaker 向 Originator 索取一個 Memento 物件,持有一陣子之後再送回給 Originator
- 有時 CareTaker 不會把 Memento 送回給 Originator,因為 Originator 可能不必回到之前的狀態。
- Memento 是被動的,只有原先的建立者 Originator 才會設定或取出他的狀態。
優點:
- 維持封裝界線。
- 可簡化 Originator。
缺點
- Memento 可能很耗資源。
- 定義寬介面和窄介面。
- 管理 Memento 的淺在代價。
Memento VS Command
# W6 CH16 Observer,Adapter (觀察者模式,調解模式)
# Observer 觀察者模式
使用情境: 假設某個影音平台提供用戶可以建立自己的頻道來上傳影片,並提供給此平台所有的用戶觀看,若是用戶對某個頻道很感興趣,可以每天上他的頻道去查看有沒有發布新的影片,有點浪費時間,另外,頻道上傳新的影片後,就會向所有用戶發布通知,有興趣的用戶看到或許會是開心的,但不感興趣的用戶就會覺得非常惱人。如果套用觀察者模式,讓此平台提供一個訂閱頻道的服務,當某頻道有上傳新的影片,就會去通知有訂閱此頻道的所有訂閱者,訂閱者本身不用主動去看有訂閱的頻道有沒有發布新的影片,省掉了訂閱者主動去關注的時間,而且通知只會給有訂閱的人,並不會發給沒有訂閱的用戶,因此用戶也不會收到莫名的頻道發布影片的通知。
使用效果: 以上述的例子,我們可以把頻道視為 Publisher,而訂閱者就是 Observer,每個 Observer 會實作相同的介面並且 Publisher 只會通過此介面與它們溝通,接著在在 Publisher 中建立一個訂閱機制,可以讓 Observer 訂閱或是取消訂閱;而當 Publisher 發布影片的時候,就會去遍歷其訂閱者並調用通知的方法將訊息傳遞給 Observer。
Class Diagram:
Skeleton Code:
1 | import java.util.ArrayList; |
特色: 訂閱機制,當一個物件有狀態改變,而這個改變可能會需要改變其他物件的時候,或是某些物件需要觀察其他物件的時候,這就可以使用觀察者模式來解決,在 Publisher 中建立訂閱機制,讓 Observer 可以去訂閱或取消訂閱就可以囉!
目的: 定義一對多的物件依存關係,讓物件狀態一有變動,就自動通知其他相依物件做該做的更新動作。
時機:
- 當一個抽象觀念有兩個層面,且其中一方會依賴另一方時。可將它們分別置於兩種物件上,以便於個別改變及再改利用。
- 某一物件有變化時,綁定物件也得跟著改變,但事先又不知道後者到底有多少時。
- 當物件必須能夠通知其他物件,但又不能假設後者是誰;換句話說,不希望這些物件之間連結的過於緊密時。
參與者:
- Subject
- 認得 Observer。任何數量的 Observer 物件均可訂閱此 Subject。
- 提供增刪 Observer 物件的介面。
- Observer
- 制定自我更新的介面,在 Subject 有變時藉以通知自己該隨之改變。
- ConcreteSubject
- 儲存 ConcreteSubject 物件的 Reference。
- 儲存應該要和 Subject 保持一致的狀態。
- 實作出 Observer 的自我更新介面,確保與 Subject 得狀態維持同步。
以上角色合作方式:
- 當 ConcreteSubject 產生足以讓 Observer 的狀態不再一致時,就會主動通知所有該通知的 Observer。
- 當 ConcreteObserver 收到通知後,可向 Subject 洽詢資訊,利用取回的資訊同步化自己的狀態。
- 請注意:策動改變的 ConcreteObserver 物件會將自我更新動作延後到 ConcreteSubject 過來知會一聲後才去進行。Notify () 不一定是由 Observer 發動,也可以由 Subject 或其他物件策動。
特色:
Observer Pattern 可讓你個別改變 Subject 和 Observer,也能個別予以再利用,也能單獨加入新的 Observer 而不動到 Subject 或其他既有的 Observer。
優點:
- Subject 和 Observer 之間的抽象耦合。 Subject 只知道有一串 Observer 物件,只知道每一個物件都遵循抽象類別 Observer 所訂之簡單介面,並不知道他們所屬的具象類別為何。因此 Subject 與 Observer 之間的耦合關係是抽象的、是微弱的。
- 由於 Subject 和 Observer 並不緊密耦合在一起,因此可分屬不同的抽象層級:低層級的 Subject 可通知高層級的 Observer,保持兩層級的暢通。如果將 Subject 和 Observer 綁在一起,那麼這些物件就全都散布在高低兩層級 (這違反了分層原則),或者被迫只能存於其中一個層級之中 (這削弱了分層威力)。
- 支援廣播功能 和普通訊息的傳遞方式不同,Subject 的通報訊息並不只名接收者是誰,而是自動廣播給所有訂戶。Subject 並不關心有多少訂戶,他唯一的責任只是知會 Observer 一聲而已。因此隨時可自由增刪 Observer。至於要處理還是忽略訊息,全由 Observer 做主。
缺點:
- 不預期的更新 由於 Observer 互不知道彼此的存在,可能也不知道改變 Subject 會造成什麼樣的代價。一個狀似單存的動作,可能會連鎖反應到一大堆 Observer 以及與之相一的物件,如果沒有妥善制訂或維護相依關係,可能會導致難以追查的偽造更新訊息。
- 如果只用簡單的更新協定,我們看不出 Subject 到底是那裡有變,Observer 也就無法查出究竟是甚麼地方變了,只得自行設法推理;這會讓問題更嚴重。
# Adapter 適配器模式
Structural Patterns
使用情境: 舉一個最貼近生活的例子,那就是充電頭與插座,每當要出國的時候,行李裡面一定要有的是手機充電器,不過每個國家的充電插座規格都不盡相同,為了要讓我們的充電頭與插座可以相容,就一定要有特定的轉接頭,才能讓手機充電。
使用效果: Adapter 的角色就是一個轉接頭,為了讓充電頭可以符合其他國家的插座規格,就得需要有符合該插座的充電頭(Adaptee)的行為模式(method),使原本的充電頭可以套用,如此來達到使用者的目的,也就是在其他國家充電。再來 Adapter 有分成兩種,一個是 Object Adapter,另一個則是 Class Adapter,前者實現了一個物件的介面並包含另一個;而後者須用多重繼承來實現它,也就是要用支援多重繼承的程式語言來實現,如果還不是很清楚的話,可以看以下的 Class Diagram 來了解他們之間微妙的差異。
Class Diagram:
Skeleton Code:
1 | interface Target { |
特色: 因為有許多程式語言都不支援多重繼承,所以上面的程式碼範例使用的是 Object Adapter。當遇到某些功能無法添加到父類別來讓子類別重用時,就可以將這些缺少的功能放到 Adapter 內,來滿足子類別。
時機:
- 想利用現有的類別,但他的街面卻與你所需要的不符。
- 想做一個可再利用的類別,且能與其他毫不相關、甚至尚未出現的類別搭配使用,也就是說,想與介面未必相容的類別合作無間。
- (只限物件版) 享用一些現有子類別,雖然介面不相同,但又不適合一個個再去衍伸子類別修改介面。物件轉換器可幫你轉換他們的父類別介面。
參與者:
- Target
- 定義 Client 所用的應用領域相關介面。
- Client
- 與符合 Target 介面的物件合作
- Adaptee
- 需要被轉換的既有介面
- Adapter
- 將 Adaptee 轉換成 Target 介面
以上角色合作方式:
Client 呼叫 Adapter 物件的操作,Adapter 則會去呼叫 Adaptee 負責這類任務的操作。
效果:
Adapter class version 與 object version 各有不同的取捨之處。
- Class Version
- 再將 Adaptee 轉換成 Target 時。Adapter 的內容會綁死在特定的 Adaptee Concrete class/subclass 身上,因此 class converter 無法同時將一個 class 以及 subclassf 全部予以轉換。
- Adapter 可覆寫 Adaptee 的某些行為,因為 Adapter 是 Adaptee 的 sub class。
- 只引進一個物件個體,不會引進額外的指標去間接存取 Adaptee。
- Object Version
- 一個 Adapter 物件個體即可處理多個 Adaptee,也就是說:Adaptee 自己以及所有 subclass (如果有的話)。Adapter 也能將某些功能一次添加到所有 Adaptee 身上。
- 較難覆寫 Adaptee 的行為;必須先去繼承 Adaptee,還要讓 Adapter 指涉至 Adaptee subclass,而不是 Adaptee 而已。
優點:
# W7 CH16 Builder, Flyweight, Chain of Responsibility (建造者模式,享元模式,責任練模式)
# Builder 建立者模式
Creational Pattern
使用情境: 以火鍋作為例子,簡單的火鍋可以分成湯底、菜盤和肉盤,如果要更精緻的話可以加些不一樣的火鍋料、多一些沾醬、甚至是更高級的料理等等。一個簡單的火鍋物件就可能會需要這些元素,而這些元素就代表著創建一個物件所需要帶入的參數,不過並不是所有的火鍋都會需要用到所有的參數,沒用到的參數就顯得非常多餘,程式碼看起來就會非常冗贅。
使用效果: 而 Builder 就會讓一個火鍋元素編成一個步驟,例如 addMeat ()、addVeg () 等,並讓每一種建立火鍋的物件來執行這一系列的步驟。而某些步驟你可能會需要不同的變化,比方說肉盤你可以選擇要牛肉、豬肉或雞肉等等,這樣的情況你會需要創建不同建立火鍋的物件,它們去實現相同的步驟,不過方式不同,如此一來就可以生成不一樣的火鍋物件。接著可以更進一步創建一個 Director 類別,並定義執行建立火鍋步驟的順序,再讓建立火鍋的物件來實現步驟。這樣,Director 類別可以對客戶端完全隱藏建立火鍋的細節,而客戶只需要與 Director 類別及 Builder 做關聯,使用 Director 物件建構,再從 Builder 獲得結果就可以囉!
Class Diagram:
Skeleton Code:
1 | interface Builder { |
特色: 使用 Builder 就可以恣意安排建構的步驟,且這些步驟都具有重用性,方便調用。之後碰到生成某項產品需要許多參數或是某產品創建時可以用不同種方式實現(如豬肉鍋、牛肉鍋),就可以使用 Builder 來解決問題了!適用於變化多樣的物件中,比如需要增加 / 減少很多屬性的 火鍋
目的:
從複雜物件的布局中抽取生成程序,以便用同一個生成程序製造各種不同的物件佈局。
時機:
- 欲將 [建造複雜物件的演算法] 與物件的零件及組裝方式保持獨立時。
- 想讓同一個物件生成程序能夠產生數種不同佈局形式的物件時。
參與者:
- Builder
- 抽象介面,用來生成 Product 的各零件。
- ConcreteBuilder
- Builder Interface 的具體實作,負責建構及組裝 Product 的各零件。
- 定義並記載它所造出的物件之佈局形式。
- 存取 Product Interface
- Director
- 利用 Builder Interface 來建構物件。
- Product
- 欲產生的複雜物件。它的內部佈局形式以及組裝程序,全都是由 ConcreteBuilder 負責定義。
- 包含各零件的類別,類別裡又含有可將這些零件組裝成最終結果的介面。
以上角色合作方式:
- Client Code 先建立 Director 物件,並將組態設定成想要的 Builder 物件。
- Director 會在需要建立 Product 的各零件時通知 Builder。
- Builder 處理 Director 送來的命令,將零件一一加進 Product 裡。
- 最後 Client Code 從 Builder 手中取得 Product。
優點:
- 能夠改變成品物件的內部佈局形式 Builder 物件提供 Direvcotr 建構成品物件的抽象介面,此介面可將成品物件的表達方式、內部佈局、組裝方式隱藏起來。因為成品物件是透過抽象介面建構而成的,所以只要定義新的 Builder,就能改變成品物件的內部構造。
- 將生成程序與內部佈局的程式碼隔離開來。 Concrete sub class 都含有生成與組裝出特定物件類型的程式碼。請注意:這些程式碼只要寫一次即可;其他種類的 Direcotr 只要利用同一堆零件,即可拼出其他不同的成品物件。
- 對生成程序掌控得更細緻。 Builder Pattern 與其他僅在一個步驟之內就建好成品物件的生成模式不一樣,成品物件是在 Director 的監控下一步建出來的,等到完全建好之後 Director 才向 Builder 索取成品物件。因此 Builder Interface 比其他生成模式更著重在建構成品物件的細部程序,也更能細緻地掌控生成程序及最終成品的內部構造。
# Flyweight 享元模式
Structural Patterns
使用情境: 大家在使用電腦的時候,最基本的技能肯定包含打字,每當在文字編輯器輸入一個字,電腦就會創建一個「字」的類別,而「字」的屬性大致上有字型、顏色、大小等等。
假設在沒有使用 Flyweight 的情況下,每輸入一個字就會創造一個字的類別,無形中系統的記憶體就很有可能很快地被占滿,導致性能變差;反之,使用者第一次輸入「A」,會產生一個「A」的類別,若使用者再輸入一次「A」,則會返回之前所創建的「A」類別,如此就不會在無意間創出許多重複的類別,避免大部分的記憶體被占用。
使用效果: 物件有分[內在狀態]與[外在狀態],內在狀態就像是「A」的字型、顏色、大小,能夠再不同的物件之間做共享,因為他們彼此相似;而外在狀態就像是「A」的行、列,表示了這個字在此文件的位置,即使是相同物件,也不會在同樣的文件裡有相同的位置,故這些屬性也不會相似,因此就不能在不同物件間共享。
Class Diagram:
Skeleton Code:
1 | import java.util.HashMap; |
特色: 減少電腦記憶體占用,如果你的程式有大量重複的物件,就可以試著將 Flyweight Pattern 套用在你的程式上面,如此一來就可以節省大量的記憶體,電腦性能也不會因為因此而降低。
時機:
Flyweight Pattern 是否有效,端看如何使用、用在何處。請在以下條件全都成立時才考慮用 Flyweight Pattern:
- 當應用程式使用了一大堆物件時:
- 過多物件耗用過多空間時:
- 物件的大部分狀態都可歸為外在狀態時:
- 如果將外在狀態拿掉,就能將好幾群物件轉換成少數幾個共用物件時:
- 如果應用程式並不仰賴 [物件等同性質] 時。因為 Flyweight 物件可共用,所以只要在觀念上是同一物件,就會視為相同物件。
參與者:
- Flyweight
- 宣告外在狀態的介面
- ConcreteFlyweight
- 具體時做 Flyweight Interface ,並添加在狀態所需的儲存空間。ConcreteFlyweight 物件必須是可共用的,所存的狀態必須是內在狀態,並且與所處環境無關。
- UnsharedConcreteFlyweight
- 並非所有 Flyweight subclass 都得共浴:Flyweight Interface 只是提供這樣的可能,而非強制。在物件結構的某一層次 (像是 Row 和 Column 類別層次) 常會利用 UnsharedConcreteFlyweight 物件包含其他的 ConcreteFlyweight 物件。
- FlyweightFactory
- 建立與管理 Flyweight 物件。
- 確保 Flyweight 物件被妥善共用,當 Client 要求一個 Flyweight 物件時,FlyweightFactory 物件會給他既存的一個;如果還沒有,就建一個新的。
- Client
- 持有指向一個或多個 Flyweight 物件的指標。
- 計算或儲存 Flyweight 的外在狀態。
以上角色合作方式:
- Flyweight 運作所需的狀態必須訂為內在或外在。內在狀態直接存取於 ConcreteFlyweight 物件中,外在狀態則由 Client Object 計算或儲存,並在必要時將他遞給 Flyweight。
- Client 不應直接具現出 ConcreteFlyweight,必須透過 FlyweightFactory 物件來取得,以確保有被妥善共用。
優點:
Flyweight Pattern 可能會在傳遞、搜尋、計算外在狀態時耗費時間,尤其是當這些資訊原本是列在內在狀態時;不過可由省下來的空間得到補償,而且共用的 Flyweight 越多,空間就越省。
能省多少空間,端看下列幾項因素:
- 因共用而省下的物件個體總數;
- 每一個物件的內在狀態量;
- 外在狀態是計算而來還是儲存下來的。
共用的 Flyweight 物件越多就越省空間,可共用的狀態越多也越省空間:如果物件用了越多內在及外在狀態,且外在狀態可動態計算求得而不必存下來的話,就可省下最多空間。因此可分兩種途徑來節省空間:以共享物件降低內在狀態的空間代價,以計算時間換取外在狀態的空間代價。
Composite Pattern 常會用 Flyweight Pattern 製作層級狀結構,以共用其中的 Leaf 節點。不過如此一來,身為 Flyweight 的 Leaf node 就不能存放指回父節點的指標,應改為外在狀態,由外界遞給 Flyweight 物件來用。這會大大影響到層級結構裡物件的互通方式。
# Chain of Responsibility 責任鍊模式
Behavioral Design Pattern
使用情境: 原本你的應用程式從客戶端發送請求後,會需要經過一些資料處理的步驟,才能存入系統的資料庫中,而這些資料處理的步驟可能會因為系統不斷的更新,而導致步驟的順序變更,抑或是添加新的檢驗步驟來增加系統的安全性,因此這個檢驗的程式就會變得非常難以維護且又不好理解,你必須得重構這整件事情。
使用效果: 使用責任鏈模式的話,會將每個檢查的步驟封裝成一個獨立的類別,其他的請求及數據就用參數的方式來傳遞給類別中的方法。而這些檢查步驟會串成一個鏈結,而且擁有下一個檢查步驟物件的屬性,因此客戶端的請求會沿著鏈結一直傳遞下去,每個節點都有機會去處理這個請求。
Class Diagram:
Skeleton Code:
1 | abstract class Handler { |
特色: 可以實現較鬆散的耦合設計,可以隨時添加新的檢查步驟,也可以移除或修改步驟的方法,甚至是重新調整檢查步驟的順序,端看應用程式的邏輯做動態更新,是不是非常方便呢!
目的: 讓多個物件都有機會處理某一訊息,以降低訊息發送者和接收者之間的耦合關係。他將接收者物件串聯起來,讓訊息經流其中,直到被處理了為止。
時機:
- 希望同一訊息不只有一個物件可處理,至於哪個會雀屏中選,事先並不知情:系統會自動挑選。
- 希望能將某一訊息丟給好幾個物件,不必指明誰是接收者。
- 希望能動態指定那些物件可處理某一訊息。
參與者:
- Handler
- 制定處理訊息介面
- (可有可無) 實作出指向後繼者的連結
- ConcreteHandler
- 處理所負責的訊息
- 可存取後繼者
- 如果訊息可處理,便處理;否則就轉遞給後繼者。
- Client
- 將訊息送到串鍊裡的 ConcreteHandler 物件。
以上角色合作方式:
- 當 Client 發出訊息後,訊息會在串鍊裡流竄,直到有個 ConcreteHandler 物件處理為止。
優點:
- 降低耦合性。
- 更有彈性地替物件增添權責。
缺點:
- 不保證會有最終接收者。
# W8 Proxy Pattern Template Pattern (代理模式、樣板模式)
# Template Pattern 樣板模式
Behavior Pattern
使用情境:
假設現在有個程式是用來分析文件,使用者可以用各種格式 (TXT、CSV 等等) 的文檔,再用此程式來提取這些文檔中的資訊,而目前的程式會根據不同格式的檔案,給對應的 class 去處理文件,而這些處理不同格式檔案的 class 除了處理個式的程式完全不同,其他資料處理程序幾乎一模一樣,因此程式重複性很高。要解決以上問題,就可以使用 Template Pattern。首先,我們將處理檔案的步驟一一轉乘方法,而這些方法會由一個主要的方法 (Template Method) 來調用,這些都會寫在 Abstract Class 裡面,然而這些步驟的方法可以是抽象的,不一定要先寫好步驟內容,但不是 Template Method 本身。
另外一個方法,稱為 Hook ,他是個可選步驟,即使沒有複寫 Hook,模板方法還是能運作,通常 Hook 會放在關鍵步驟之前或之後,為子類別提供額外處理資料的擴展點。
Class Diagram:
1 | abstract class Template { |
特色:
如果想要擴展處理流程的特定步驟,而不是整個流程或是其結構的話,就可以使用 Template Pattern 來解決此問題,他會將整個處理流程轉換為一系列單獨的步驟,因此子類就可以去實作或複寫這些步驟,也部會改變父類所定義的結構,不過可能會受到已定義的結構而限制了步驟的延伸,倘若 Template Pattern 的步驟越多,越難維護。
目的:
對於操作,只先定義好演算法的輪廓,某些步驟則留給子類別去填補,以便在不改變演算法整體構造的情況下讓子類別去精煉某些步驟。
時機:
- 希望演算法的不變之處只寫一次,可變之處則留給子類別去實作時。
- 如果許多子類別都會有共同的行為,就應該抽取出來集中至另一個共同的類別,避免一再重複。這也是 Opdyke 與 Johnson 所說的 [萃取式的重整措施]: 一開始先設法找出既存程式碼之間的異同,再將相異之處切出來另訂新的操作,最後將原先的程式碼換成 template method,在 template method 裡呼叫那些新的操作。
- 希望能夠控制子類別的擴充範圍時。妳可以讓 template method 在某些定點呼叫 [Hook] 函數,將可能的擴充範圍侷限在這些定點上。
參與者:
- AbstractClass
- 將完整演算法裡的某先基礎步驟訂為 PrimitiveOperation 抽象操作,細節則是留待 ConcreteClass 來實作。
- 具體實作 Template Method,定義出演算法的整體輪廓。它會呼叫 PrimitiveOperation,也會呼叫 AbstractClass 或其他物件所提供的操作。
- ConcreteClass
- 具體實作出 PrimitiveOperation,以實施子類別該有的步驟。
以上角色合作方式:
- ConcreteClass 仰賴 AbstractClass 製作出演算法的不變之處。
優點:
Template Method Pattern 是很基本的程式碼再利用技術,特別常用於類別庫,因為它們本來就是想將程式庫裡的共同行為萃取出來。
Template Method 會造成一種顛倒的控制結構,有時戲稱 [好萊鎢原理]:[別跟我們講,讓我們來告訴你!] 。也就是說,Parent Class 反而會去呼叫 SubClass 的操作,而非正常的 [子類別會去呼叫父類別的操作] 模式。
Template Method 會去呼叫以下幾種操作:
- Concrete Operation(ConcreteClass or outside)
- AbstractClass Concrete Operation
- PrimitiveOperation (Abstract Operation)
- Factory Method
- Hook Operation,本身提供預設的行為,必要時子類別亦可擴充之。預設版式通常是不作事。
Template Method 有必要明定那些操作是 Hook Type (可被 OverRiding)、那些是抽象操作 (必須 OverRiding)。畢竟對子類別 Designer 來說,如果想有效的再利用 Abstract Class,就必須知道那些操作本來就是設計成讓人 OverRide 的。
子類別如果想擴充父類別的某些操作的行為,可以覆寫它,並在裡面呼叫覆類別的操作
不幸的是:大家常會忘記還要去呼叫繼承而來的父類別操作。因此比較好的做法是改寫成 Template Method,由父類別掌控子類別可能有的擴充手法:父類別的 Template Method 會去呼叫 Hook Operation,子類別則挑選想 OverRide 的 Hook。
# Proxy Pattern 代理模式
Structural Pattern
使用情境:
Proxy Pattern 他在做的事情是 [控制和管理對所有保護的物件的訪問行為] ,在現實生活中,可把信用卡當作是我們銀行帳戶中的代理,他能夠代替現金使用,同時也能夠在需要的時候提供一種獲取現金的一種方式。
舉例:學校的網路通常都會限制某些網站不能存取,像是社群網站、色情網站等等,好讓學生不在電腦課上亂逛有的沒的網站,而這就是 Proxy 的作用。
首先他會先檢查你要連線的主機,是否為它的限制站點列表中的其中之一,如果是的話可能就會被限制存取或是直接跳回頁面,總之就是不會讓你連線到被限制的站點;反之你的要求就會被代理接受,就可以連線到你想要的網站。
Class Diagram:
1 | interface Service { |
特色:
Proxy Pattern 最大的好處就是它的安全性,可以避免大量的程式重複,間接提高了應用程式的效能,不過因為需要多一步的檢查步驟,也就可能造成服務響應的延遲,而且需要新增許多新的 class,因此程式碼會變得比較複雜。
# W12 Prototype Pattern (原型模式)
# Prototype Pattern (原型模式)
使用情境:
假設今天有個複雜的物件,你想要創建它的分身,就必須先創一個同一類別的新物件,然後再遍歷(go through)此類的所有屬性以後才能複製到新的物件。不過可能會碰到一個問題,就是並不是所有的屬性都會是公開的,有可能是私有的,所以不能端看物件的外表而了解其內在。
當遇到這種情況以後,即使物件本身沒有私有的屬性,但只要需要創建分身,就得寫一大串的程式碼將要創建的物件初始化,而這些程式碼又是重複的,會造成程式碼顯得又臭又長。
倘若使用 Prototype,所有支援克隆(clone)的物件都會有個共同的介面,而克隆的方法都會在所有的類別實現,此方法會創建一個當前類別的物件,並將舊物件所有的屬性複製到新的物件上,即使是私有的屬性也會移並複製過去。
當一個設計很複雜的時候再 OOAD 的設計中可能會需要很多物件,但有些物件有需要複製的需求,在沒有複製功能的情況下,我們會 New 出一個新物件,這會造成大量的記憶體占用,因此我們可以透過 ProtoType Pattern 的特性來共享部分元件的記憶體位置。
Class Diagram:
Skeleton Code:
1 | //淺複製 |
淺複製 & 深複製
要實作 Prototype Pattern 有兩種方法,淺複製 (Shallow Clone) 和深複製 (Deep Clone)。而在複製的資料類型中,又分為基本資料類型(int、double、byte、boolean… 等)以及引用類型(class、interface… 等)。
# 淺複製
如果原型物件的成員變數為基本資料類型,將複製一份給新的物件;
如果原型物件的成員變數為引用類型,則只將原本的物件位址複製給新的物件。
也就是原型物件和複製物件的成員變數指向相同的内存位址。
意思就是,淺複製中,只複製了自己本身以及基本資料類型的成員,而引用類型的成員並沒有被複製。
# 深複製
如果原型物件的成員變數不管是基本資料類型或是引用類型,都會複製給新的物件。
簡單來說,深複製中,除了自己被複製,所有的變數也被複製給新的物件。
優點
- 效能較佳
- 隱藏了創建新物件的復雜性
缺點 - 每個類別都需要有 clone 的方法
- clone 位於類別的內部,當需要做更新時,會直接異動到該類別,違反了 OCP