第十一章設計模式
設計模式
設計模式:對軟體設計中給定上下文中常見問題的通用的、可重用的解決方案
注重複用性,可維護性,可拓展性
可以分為三類:
- Creational patterns(建立型模式):關注物件的建立
- Structural patterns(結構型模式):解決類或物件的構成
- Behaviroal patterns(行為類模式):描述類或物件互動和分配職責的方式
Creational patterns(建立型模式)
Factory Method pattern(工廠方法模式)
為什麼需要?
當client不知道建立哪個具體類的例項的時候,或者不想再client程式碼中指明要具體建立的例項時,用工廠方法。
原理:
定義一個用於建立物件的介面,讓其子類來決定例項化哪一個類(物件的),從而使一個類(物件的)的例項化延遲到其子類。
具體實現:
優勢:
- 無需將適用於特定目的的類繫結到client程式碼中,降低了耦合度。
- 程式碼只需以Product作為介面來使用,所以它可以和任何使用者自定的任何ConcreteProduct工作。
- 可以簡化一些複雜的建立過程。
潛在劣勢:
- 使用者可能需要建立Creator(PS:這是那裡來的?)的子類,以便他們可以建立特定的ConcreteProduct。
- 如果客戶端必須對建立者進行子類化,那麼這是可以接受的,但 是如果不是這樣,那麼客戶端必須處理另一個進化點
使用工廠方法模式可以方便遵循OCP原則,也就是如果有了新的Product,那麼只需要建立一個ConcreteFatory而不是在原來的Factory中修改。
冷知識:子類繼承父類,那麼它的方法丟擲的異常要比父類丟擲的異常要小。它的構造方法丟擲的異常要比父類丟擲的異常大。
Structural patterns(結構型模式)
Adapter(介面卡)
為什麼需要?
在實現某些功能的時候,我們將這些功能的行為抽象成了一個介面,然後已經按照這個介面實現了很多類了。這時候我們有了新的需求,這時候我們可以選擇繼續繼承這個介面然後實現,但是我們在網上發現已經存在了相應的類(但是方法名字,引數等和現在的介面不一樣),因此就會想辦法利用到這個類,這時候就需要介面卡的思想來實現中間的這個介面卡。
原理:
可以使用extends(繼承)或者delegation(委託)來實現,在介面卡類使用extends就是繼承lagacy類(也就是恰好存在的可以利用的類),然後再實現介面中的類方法。不過這種方法耦合度高。因此更好使用delegation。所謂delegation就是在介面卡類中建立lagcacy類的物件,然後通過呼叫這個類來實現方法。這樣在本類中不會存在多餘的類方法。
優點:
- 客戶端通過介面卡可以透明地呼叫目標介面。
- 複用了現存的類,程式設計師不需要修改原有程式碼而重用現有的適配者類。
- 將目標類和適配者類解耦,解決了目標類和適配者類介面不一致的問題。
缺點:
- 對類介面卡來說,更換介面卡的實現過程比較複雜。
Decorator(裝飾器)
為什麼需要?
在不改變現有物件的結構情況下,動態地給該物件增加一些額外的功能。為什麼不通過生成子類的方式呢?因為做不到,比如該類是final類或者即使做到了,通過繼承的方式會產生大量的子類。還有就是新增的物件的功能要求可以動態地增加,也可以動態地撤銷時。
原理:
它的結構如下
Component:這個介面定義被裝飾物應該執行的公共操作
ConcreteComponent:這是一個具體的實現類,是裝飾的起始物件,在其基礎上新增功能,將通用的方法放到此物件中。
Decorator:這是一個抽象類並且是所有裝飾類的基類。它實現了Component的介面所定義的方法,它也包含了一個protected的component指向被裝飾的物件(也就是ConcreteComponent的具體例項)。將在構造方法中為這個component賦值。即如下:
public Decorator(Component input){
this.component = input;
}
ConcreteDecorator:這個類是實際的裝飾類,只要你想,可以建立任意種裝飾,它繼承自Decorator類。
優點:
- 採用裝飾模式擴充套件物件的功能比採用繼承方式更加靈活。
- 可以設計出多個不同的具體裝飾類,創造出多個不同行為的組合。
- 和使用繼承的方式相比使用裝飾類可以避免建立很多子類而導致如果需求是要求子類的功能自由組合而導致編寫的子類因組合爆炸而寫大量類。
缺點:
- 裝飾模式增加了許多子類,如果過度使用會使程式變得很複雜。
冷知識:java中變數與函式的可見級別
成員類 | 包內 | 子類 | 所有類 | |
---|---|---|---|---|
public | T | T | T | T |
protected | T | T | T | F |
default | T | T | F | F |
private | T | F | F | F |
Behaviroal patterns(行為類模式)
Strategy(整體地替換演算法)
為什麼需要?
有多種不同的演算法實現同一個任務務,但需要client根據需要動態切換演算法,而不是寫死在程式碼裡。
原理:
為不同的實現演算法構造抽象介面,利用delegation,執行時動態傳入client傾向的演算法類例項
具體結構如下:
優點:
- 對OCP原則支援,如果有新的實現演算法,可以方便拓展
- 將具體演算法從Client的上下文中分隔開來
缺點:
- Client需要知道所有的演算法類,並自行決定使用哪一個策略類。
Template Method(模板模式)
為什麼需要?
當不同的客戶端對某一類事情具有相同的演算法步驟,但是每個步驟的具體實現不同的時候,使用Template Method方法比較好。
原理:
定義一個抽象父類或者介面,在其中定義通用邏輯和個步驟的抽象方法宣告。在子類中進行各步驟的具體實現。具體結構如下:
優點:
- 封裝不變部分,擴充套件可變部分。把認為不變部分的演算法封裝到父類中實現,而可變部分的則可以通過繼承來繼續擴充套件。
- 提取公共部分程式碼,便於維護。
- 行為由父類控制,子類實現
缺點:
- 演算法骨架需要改變時需要修改抽象類。
- 按照設計習慣,抽象類負責宣告最抽象、最一般的事物屬性和方法,實現類負責完成具體的事務屬性和方法,但是模板方式正好相反,子類執行的結果影響了父類的結果,會增加程式碼閱讀的難度。
Iterator(迭代器模式)
為什麼需要?
客戶端希望遍歷被放入 容器/集合類的一組ADT物件,無需關心容器的具體型別。也就是說,不管物件被放進哪裡,都應該提供同樣的遍歷方式。在java中,ArrayList,LinkedList,HashSet,HashMap等都使用了這一模式。
原理:
具體結構如下:
抽象容器角色(Aggregate):負責提供建立具體迭代器角色的介面,一般是一個介面,提供一個iterator()方法,例如java中的Collection介面,List介面,Set介面等。
具體容器角色(ConcreteAggregate):就是實現抽象容器的具體實現類,比如List介面的有序列表實現ArrayList,List介面的連結串列實現LinkedList,Set介面的雜湊列表的實現HashSet等。
抽象迭代器角色(Iterator):負責定義訪問和遍歷元素的介面。
具體迭代器角色(ConcreteIterator):實現迭代器介面,並要記錄遍歷中的當前位置。
在java中如果計劃使用迭代器模式,應該讓自己的聚集類實現Iterable介面,然後自己的迭代器實現Iterator介面。
優點:
- 迭代器模式使得訪問一個聚合物件的內容而無需暴露它的內部表示,即迭代抽象。
- 迭代器模式為遍歷不同的集合結構提供了一個統一的介面,從而支援同樣的演算法在不同的集合結構上進行操作。
缺點:
- 迭代器模式在遍歷的同時更改迭代器所在的集合結構會導致出現異常。所以使用foreach語句只能在對集合進行遍歷,不能在遍歷的同時更改集合中的元素。
冷知識:ArrayList實現了一個RandomAccess的空介面(介面中沒有內容),這是一個標誌介面,如果是實現了這個介面的List,那麼使用for迴圈的方式獲取資料會優於用迭代器獲取資料。我也測試了一下,參考資料[5]中的大部分時間都是for快一點。不過要資料量大100倍。
Vistor(訪問者模式)
為什麼需要?
對特定型別的object的特定操作(visit),在執行時將二者動態繫結到一起,該操作可以靈活更改,無需更改被visit的類
原理:
具體結構如下:
Visitor抽象訪問者介面:它定義了對每一個元素(Element)訪問的行為,它的引數就是可以訪問的元素,它的方法個數理論上來講與元素個數(Element的實現類個數)是一樣的,從這點不難看出,訪問者模式要求元素類的個數不能改變(不能改變的意思是說,如果元素類的個數經常改變,則說明不適合使用訪問者模式)。
ConcreteVisitor具體訪問者角色:它需要給出對每一個元素類訪問時所產生的具體行為。
Element抽象節點(元素)角色:它定義了一個接受訪問者(accept)的方法,其意義是指,每一個元素都要可以被訪問者訪問。
ConcreteElement具體節點(元素)角色:它提供接受訪問方法的具體實現,而這個具體的實現,通常情況下是使用訪問者提供的訪問該元素類的方法。
ObjectStructure結構物件角色:這個便是定義當中所提到的物件結構,物件結構是一個抽象表述,具體點可以理解為一個具有容器性質或者複合物件特性的類,它會含有一組元素(Element),並且可以迭代這些元素,供訪問者訪問。
優點:
- 訪問者模式使得易於增加新的操作 訪問者使得增加依賴於複雜物件結構的構件的操作變得容易了。僅需增加一個新的訪問者即可在一個物件結構上定義一個新的操作。相反, 如果每個功能都分散在多個類之上的話,定義新的操作時必須修改每一類。
- 訪問者集中相關的操作而分離無關的操作 相關的行為不是分佈在定義該物件結構的 各個類上,而是集中在一個訪問者中。無關行為卻被分別放在它們各自的訪問者子類中。這 就既簡化了這些元素的類,也簡化了在這些訪問者中定義的演算法。所有與它的演算法相關的數 據結構都可以被隱藏在訪問者中。
缺點:
-
增加新的 ConcreteElement類很困難
Visitor模式使得難以增加新的 Element的子類。每新增一個新的 ConcreteElement都要在 Vistor中新增一個新的抽象操作,並在每一個 ConcretVisitor類中實現相應的操作。有時可以在 Visitor中提供一個預設的實現,這一實現可以被大多數的 ConcreteVisitor繼承,但這與其說是一個規律還不如說是一種例外。
所以在應用訪問者模式時考慮關鍵的問題是系統的哪個部分會經常變化,是作用於物件結構上的演算法呢還是構成該結構的各個物件的類。如果老是有新的 ConcretElement類加入進來的話, Vistor類層次將變得難以維護。在這種情況下,直接在構成該結構的類中定義這些操作可能更容易一些。如果 Element類層次是穩定的,而你不斷地增加操作獲修改演算法,訪問者模式可以幫助你管理這些改動。
-
破壞封裝
訪問者方法假定ConcreteElement介面的功能足夠強,足以讓訪問者進行它 們的工作。結果是,該模式常常迫使你提供訪問元素內部狀態的公共操作,這可能會破壞它 的封裝性。
Commonality and Difference of Design Patterns(設計模式的共性與差異)
共性樣式1:
只使用“繼承”,不使用“delegation”
核心思路:OCP/DIP
依賴反轉,客戶端只依賴“抽象”,不能 依賴於“具體” 發生變化時最好是“擴充套件”而不是“修改”
主要使用:介面卡模式和模板方法模式
共性樣式2:
兩棵“繼承樹”,兩個層次的“delegation”
主要使用:整體地替換演算法,迭代器模式,工廠方法模式,訪問者模式
參考資料
[1] 為什麼提倡面向介面程式設計