1. 程式人生 > >GOF設計模式

GOF設計模式

為什麼學習設計模式

剛工作的時候其實閱讀過一遍這本書,但是當時並沒有太多感悟。最近重讀時才發現,學習設計模式其實是很有必要的。

不管是有意還是無意識中,日常工作中的很多程式碼實際上就是某一種設計模式,學習設計模式一方面有助於規範程式碼,另一方面也有助於理解他們的程式碼。

在程式碼中,規範是很重要的一個環節,儘量使用較為通用和為人所知的方法來解決問題實際上也是規範的一環。

閱讀之後,稍微整理了這些設計模式,以便將來不時翻閱記憶。

ABSTRACT FACTORY(抽象工廠)

建立一系列相關或者相互依賴的物件的介面。它隱藏了要建立的類的資訊,以及如何建立這些類,在讓邏輯清晰的同時也能夠在不改變客戶程式碼的前提下對類進行修改。但由於在擴充套件產品數量的時候要對所有的具體實現的工廠類同時進行擴充套件,所以抽象工廠難以支援新的產品。在實現上,可以用Singleton模式建立工廠的例項,對具體的工廠,除了建立子類之外,也可以選擇使用Prototype,在建立類的介面定義方面,除了為每一種產品都定義一個介面之外,也可以僅使用一個介面,通過不同的引數來區分所需要的產品型別。

BUILDER(生成器)

將複雜的物件的構建過程的抽象出來。Builder對外提供構建產品的步驟的介面,產品的裝配細節只有Builder的提供者可見,而使用者可以通過呼叫這些介面,可以較為精確的控制自己想要構建的產品。與ABSTRACT FACTORY不同,BUILDER是在最後一步才返回產品。

PROTOTYPE(原型)

通過建立好的例項為基礎,通過拷貝它們來建立新的例項的方法。好處在於能夠隨時新增新的原型,缺點就是所有的原型類都必須實現拷貝操作,否則無法正常工作。實現上,需要有一個用於管理原型的原型管理器,也可以新增一個初始化函式在拷貝完成之後對行例項進行初始化。

SINGLETON(單例)

保證類僅有一個例項,且只能通過一個訪問點來訪問這個例項。由於是唯一的例項,所以能夠很容易的控制它的行為,控制何時能訪問它,也可以建立多個子類,在需要的時候替換這個例項的類。當然單例不侷限於只有一個例項,你也可以擁有多個例項,依據需要而定。單例是對全域性變數的一種改進,能夠避免名稱空間的汙染。在實現上,可以通過模板類的方式來快速實現一個單例基類。

ADAPTER(介面卡)

包裝一個已有的類,使它包裝後的介面能夠與另一個類一同工作。介面卡在實現上有兩種方式,一種是通過多繼承的方式,稱為類介面卡,它不需要額外的指標來指向需要適配的物件,缺點是無法同時適配它的子類。另一種是通過將需要適配的物件作為成員,用指標指向它,優點是可以同時適配物件及它的子類。

BRIDGE(橋接) 

將抽象部分與它的實現分離,使得它們可以相互獨立變化。相比於繼承,橋接能更加靈活的對抽象部分和實現部分獨立地進行修改、擴充和重用。使用橋接,抽象類的實現能夠在執行時進行配置,抽象與實現的相互獨立有助於降低實現部分對編譯的依賴,有助於分層,從而產生更好的結構化系統,此外還能對客戶隱藏實現細節。實現上,當Implementor僅有一個時可以不需要抽象的Implementor類,但使用橋接仍有許多優點。在Implementor的選擇上,可以通過在構造器中傳遞引數來決定,也可以代理給某個物件例如前面的抽象工廠。幾個抽象介面可以同時共享一個實現,使用引用計數方法來共享物件。 

COMPOSITE(組合) 

將物件組合成樹形結構以表示“部分-整體的層次結構”。COMPOSITE使得單個物件和組合物件在使用上具有一致性。它能夠很容易的增加一些新的元件,但帶來的問題是設計上的一般化,你很難限制組合中的元件。實現上,需要維持一個顯示的父部件的引用。由於COMPOSITE目的之一就是儘量讓Leaf和Composite在外觀和行為上保持一定的一致,所以應當儘量最大化Component的介面,對一些只對Composite有意義的介面,Leaf保持預設行為。但這樣做可能會帶來安全性問題,因為從Leaf中刪除和增加物件是不允許的。在Component上維護Component的子節點列表會浪費儲存空間,因為葉節點不存在子節點。在需要頻繁查詢遍歷的情況下,使用快取記憶體能提升效率。在元件的儲存上,使用何種資料結構取決於實際的需求。 

DECORATOR(裝飾) 

維護一個指向修飾物件的指標,在請求時,裝飾者將請求轉發給被修飾的物件,並在物件處理請求前/後附加一些額外操作,以此來為物件動態新增額外的職責。它相比生成子類更加靈活,並且能夠較容易的組合這些額外功能,避免子類爆炸的問題。在無法生成子類的情況下,用於替代子類。實現上,為保證裝飾者和被修飾物件介面的一致性,它們應該有統一的基類。對它們的共同基類而言,這個基類應當儘量保持簡單,即集中於介面的定義而不是資料的儲存,否則會導致它的子類過於龐大。在處理一些本身已經很龐大的類時,慎用裝飾模式,可以選擇用Strategy代替。 

FACADE(外觀) 

定義一個高層介面來為子系統中的一組介面提供一個一致的介面,使得子系統更加容易使用。將系統劃分為子系統有助於降低系統複雜性,子系統通過統一的Facade來相互聯絡能減少子系統間的相互耦合。Facade提供的統一對外介面,也有助於在子系統演變得愈發複雜的情況下,還能對外保持較好的易用性。子系統和Facade的層次關係也能夠降低客戶和子系統間的耦合,便於消除客戶程式和子系統間的依賴關係,從而能夠更加方便的維護修改子系統。 

FLYWEIGHT(享元) 

主要用於應對大量細粒度物件,通過共享物件的方式,犧牲效能來換去儲存空間上的節省。實現上,首先是對問題的建模,如何將這些物件對映到共享的物件上,另外如何管理共享物件也是需要考慮的問題,應該有一個統一的共享物件管理器,在需要的時候新增刪除共享物件。 

PROXY(代理) 

提供一種通過代理來訪問物件的方式。應用上,主要有遠端代理(為一個不同地址空間的物件提供一個代表,一般儲存它的地址),虛代理(建立開銷很大的物件,如圖片物件,根據需求來建立物件),保護代理(用於控制對原始物件的訪問),智慧指引(智慧指標/引用計數)。

CHAIN OF RESPONSIBILITY(職責鏈) 

一條用於處理請求的物件鏈,並且在傳送請求時不需要為請求指定接收者,而由鏈上的接收者決定是否去處理這條請求。它降低了耦合並使系統更加靈活,但相對的它無法保證請求一定被接收。實現上,其一是連結的實現(已有連結或維護新連結),其二是請求的表示形式(Hardcode請求也接收者的對應關係或者使用整形/字串標識來識別請求)。 

COMMAND(命令) 

把請求封裝成物件,從而可以對請求進行排隊/記錄,並支援撤銷。命令本身包含了執行所需要的全部資訊(接收者/執行者/如何執行),利於命令呼叫者和執行者之間的解耦。實現上,提供一個Reserve操作就可以實現Redo和Undo,但是需要避免這一過程中可能產生的累積錯誤,對簡單命令,可以把接收者作為模板引數構建模板化的命令,從而簡化程式碼。 

INTERPRETER(直譯器) 

構建一種語言/文法來描述一類特定的問題,並定義相應的直譯器來解釋句子。它易於拓展和改變,文法也易於實現,但過於複雜的文法難以維護。實現上主要是構建語法樹和定義解釋操作,在某些情況下使用Flyweight共享符號有助於節省儲存空間。 

ITERATOR(迭代器) 

對聚合物件內部順序訪問的封裝,能夠在不暴露內部表示的情況下提供對聚合物件的遍歷,並支援用統一介面遍歷不同聚合結構。它簡化了聚合結構的介面,並且能夠在同一聚合上同時進行多個遍歷。實現上,由客戶控制迭代的迭代器被稱為外部迭代器,而由迭代器自身控制迭代稱為內部迭代器,外部迭代器更加靈活。遍歷演算法可以由迭代器負責,這時候迭代器可能會需要一些額外訪問權(友元),當然也可以由容器提供遍歷演算法,此時迭代器退化為一個記錄當前遍歷狀態的Cursor。在使用迭代器的時候一定要注意迭代器失效的問題,迭代的同時增加/刪除聚合中的元素是危險的,實現健壯的迭代器需要讓聚合來註冊迭代器,並在聚合元素變化的時候通知並更新迭代器。使用空迭代器(NullIterator)有助於處理邊界條件。 

MEDIATOR(中介者) 

對一系列物件互動的封裝,降低各物件之間的耦合。它將各個物件之間的互動集中到一個類中,有助於理清物件之間的相互關係,邏輯更加清晰。但這樣可能導致中介者過於龐大複雜,使得中介者本身難以維護。實現上主要是需要實現Mediator和Colleague之間的通訊,Observer模式可以用於這樣的通訊。 

MEMENTO(備忘錄) 

在不破壞封裝的前提下捕獲一個物件的內部狀態並在外部儲存起來,以便在需要之時能夠將物件恢復到儲存的狀態。它能夠保持物件原有的封裝,把儲存工作交給備忘錄能夠簡化物件的設計,但使用上代價可能會很高(大量的拷貝/儲存),在某些語言下封裝性很難保證。實現上,需要語言支援寬/窄介面,寬介面由Memento對物件,而窄介面對外。另外在順序可以預測的情況下,可以使用增量儲存來記錄改變。 

OBSERVER(觀察者) 

定義了物件間一種一對多的依賴關係,當一個物件的狀態發生改變的時候,依賴於它的物件能夠得到通知並被自動更新。目標僅僅知道它有一系列觀察者,這能夠去除目標和觀察者之間的耦合,各自之間可以獨立的相互改變,能夠支援廣播,觀察者可以自由地選擇忽略還是執行請求,此外也可以時刻增刪觀察者。但帶來的問題是觀察者不知道其他的觀察者,它對最終改變的代價一無所知,如果觀察者之間存在相互的依賴,會導致最終結果的不確定。 

實現上,首先要建立目標到觀察者的對映,顯示的儲存引用或者用hash表等方法。也可以擴充套件介面,新增和改變內容相關數,讓觀察者能夠只對它感興趣的內容作出迴應。要避免懸掛引用的問題,刪除一個目標時,應當通知它的觀察者。 

在傳送的內容上,經常可以包含一些額外的資訊,這裡有兩個極端的情況,其一是傳送所有的資訊不管需要與否,稱之為推模型;另一個情況是除了通知之外什麼也不送出,觀察者需要顯示的請求資訊,稱之為拉模型。 

在傳送的時機上,有兩種選擇,其一是在狀態變化時自動傳送通知,另一個是在一系列狀態改變完成後手動呼叫傳送通知。無論哪種情況都要確保目標的狀態是一致的,避免在通知發出之後再改變自身狀態。 

在複雜的情況下可以建立管理器來維護觀察者和目標之間的引用,而不是之間由目標來維護對觀察者的引用。此外管理器還能夠定義合適的更新策略,根據目標請求來通知依賴目標的觀察者。 

STATE(狀態) 

在內部狀態改變時改變它的行為,避免了子類的生成,同時也避免了大量的條件語句。這個狀態通常用列舉表示。它將特定狀態下的行為分割開來,狀態之間的顯式切換讓物件的行為更加明確。實現上,狀態的切換可以由使用的環境(Context)來指定,當然也可以由State/State子類來指定它的後繼狀態。另外可以用表來驅動狀態,每一個狀態都對映到表中,從表中獲取後繼狀態。狀態的創建於銷燬也是一個問題,可以選擇在需要的時候建立/銷燬它們,也可以選擇提前建立好狀態物件,始終不銷燬它們。 

STRATEGY(策略) 

將一系列演算法封裝起來使它們可以相互替換,它可以在一定程度上代替繼承,並能消除一些條件語句。在一些情況下許多類只是行為上有區別,將這些行為作為“策略”單獨封裝從而可以配置一個類的行為,在類有多種行為的情況下,通過封裝成策略可以減少條件語句。它能夠提供相同行為的不同實現,客戶可以根據需求選擇。缺點是客戶必須瞭解Strategy,才能選擇合適的策略。由於多個策略共享介面,那麼會存在一些永遠不會被使用的引數,而為了避免這個問題則需要Strategy和Context之間更加緊密的耦合。實現上,具體策略需要知道上下文中它所需要的所有資訊,一種方法是把Context中資料放入引數傳遞給Strategy,但可能傳送一些不需要的資料。另一種辦法是將Context自身作為一個引數傳遞給Strategy,再由Strategy向Context請求資料。可以將Strategy作模板引數來配置一個類。為Strategy定義預設的物件能夠簡化它的使用,只有在預設行為不能滿足需求時才需要配置Strategy。 

TEMPLATE METHOD(模板方法) 

在基類中定義好演算法的骨架,再將一部分步驟延遲到子類之中。模板方法可以使子類在不改變演算法結構的情況下重定義某些步驟。模板方法是程式碼複用的基本技術。模板方法呼叫的操作有具體操作、AbstractClass操作、原語操作(抽象操作)、Factory Method和鉤子操作。實現上,儘量少用原語操作,在C++中原語操作定義為純虛擬函式,實現子類必須重寫它。需要重定義的操作越多程式就越冗長。命名上,儘量給被重定義的操作加上字首以便識別它們。 

VISITOR(訪問者) 

用於表示一個作用於某個結構中的各元素的操作。它使你在不改變各元素的類的前提下定義作用於這些元素的操作。使用Visitor能很容易的增加新的操作,並簡化了結構的介面,因為Visitor能分離相關操作。但當提供給Visitor的介面功能過於強大時,可能會破壞封裝性。