15分鐘入門23種設計模式:圖解,範例和對比

語言: CN / TW / HK

本文力圖在15分鐘內,通過範例類比,讓你對面向物件的23種設計模式形成提綱挈領的認識,從而讓我們在面臨程式碼設計問題時更加成竹在胸。本文原始碼: UML, Sample Code

開門見山

我們直奔主題,分類呈現23種設計模式的廬山真面目:

| 建立型 (5)
Creational | 結構型 (7)
Structural | 行為型 (11)
Behavioral | | ------ | ----- | ----- | | 工廠方法 Factory method
抽象工廠 Abstract factory
建造者 Builder
原型 Prototype
單例 SingleTon | 介面卡 Adapter
橋接 Bridge
組合 Composite
裝飾 Decorator
外觀 Facade
享元 Flyweight
代理 Proxy | 責任鏈 Chain of responsibility
命令 Command
直譯器 Interpreter
迭代器 Iterator
中介 Mediator
備忘錄 Memento
觀察者 Observer
狀態 State
策略 Strategy
模板方法 Template method
訪問者 Visitor |

這23種設計模式源於GoF所著的"Design Patterns - Elements of Reusable Object-Oriented Software" 一書(也有將該書直接簡稱為GoF),譯著為 “設計模式:可複用面向物件軟體的基礎”。原書將這23種設計模式分為三類:

  • 建立型包含5種模式,涉及物件/物件組合的建立構建。
  • 結構性包含7種模式,涉及物件/類之間的關係。
  • 行為型包含11種模式,涉及物件/類的行為、狀態、流程。

從該書的標題我們可以瞭解到,設計模式是一個面向物件開發方法下的概念,是解決程式碼設計/軟體架構問題的可複用的元素,同時是基本元素(elements)。引用原書的例子,我們大家所熟識的MVC模式,Model-View-Controller,就可以解構為幾種設計模式的組合演變,比如可以在View和Model的關係中看到觀察者模式 Observer、組合模式 Composite、裝飾模式 Decorator,在Controller中發現策略模式的影子。通過對23種基礎模式的有機利用和結合,可以進一步演化出更復雜的軟體架構。限於篇幅,本文不會講解每種設計模式的定義和背景,讀者可以參考設計模式簡介來學習定義。

設計模式的UML、類比和範例

這個部分,我們逐步從嚐鮮到類比,深入理解一些比較常見有趣的設計模式的UML及其經典例項。GoF原書中也推薦學習者從“模式怎樣相互關聯”以及“研究目的相似的模式“出發來學習和選擇設計模式。首先看看最簡單常見的策略模式和另一個同屬行為型模式狀態模式

| | 策略模式 Strategy | 狀態模式 State | | ------ | ----- | ----- | | UML | strategy.png | state.png | | 範例 | - Comparator#compare() 和 Collections#sort()
- Spring Security: PasswordEncoder | - 標準範例: javax.faces.lifecycle.LifeCycle#execute()
- 形似樣例:Java Thread State, ExoPlayer | | 概述 | 讓外部對演算法的相互替換無感 | 允許一個物件根據內部狀態改變行為 | | 關鍵字 | Strategy, rule | State, switch, phase, lifecycle | | 核心角色 | Strategy | State |

策略模式和狀態模式在UML圖形上非常相像,他們之間的主要區別如下: - 狀態物件可以持有上下文物件(呼叫方),但策略模式一般存在這種依賴。 - 狀態模式可以在彼此之間進行跳轉替換,比如呼叫了播放器的play方法,那麼狀態可能從stop->playing,這個操作可以用狀態物件完成。 - 一個策略和呼叫方的關係(依賴)可能弱於狀態和上下文物件的關係(持有、屬性)。 - 策略的不同可能隻影響一個行為,但是狀態的不同影響狀態持有物件行為的方方面面。

整體上策略模式要比狀態模式更加簡明易懂,應用場景更廣,在大型專案中的應用也隨處可見。而狀態模式雖然也是對常見概念的抽象,其應用卻相對有限,其原因可能是,在更多的情況下,把行為的差異定義在不同的狀態中,可能並非符合直覺的操作:與其把狀態也定義為物件承載行為,不如把狀態定義為一個標記,直接用ifswitch判斷來的直接。或者換言之,大多數情況下,問題還沒有複雜到要用狀態模式的程度。

藉助這種對比的視角,我們來學習更多模式。先看看以下三種結構型設計模式:

| | 介面卡 | 橋接模式 | 外觀 | | ------ | ------ | ----- | ----- | | UML | adapter.png | bridge.png | facade.png | | 範例 | RecyclerView.Adapter | 範例比較少:
- Collections#newSetFromMap()
- (ADB?),如Spring中Service和Repository的關係 | 非常常見,如:Facades, FacesContext, ExternalContext, DataSource#getConnection() | | 概述 | 將一個類的介面轉換成滿足另一個要求的介面 | 將抽象部分與它的實現部分分離 | 為子系統中的一組介面提供一個一致易用的介面 | | 關鍵字 | Adatper | Wrapper | Context | | 核心角色 | Adpter, Adaptee | Bridge | Facade |

介面卡模式、橋接模式和外觀模式同屬結構型設計模式,他們三者概念上很相像,都是通過建立介面來為類的方法建立或重構關係,比如,似乎我們用外觀的視角去解釋介面卡,也能解釋的通,Adapter就是在幫助Adaptee建立統一介面,或者建立橋樑。

設計模式就是這樣,非要較真,所有的設計模式都大同小異(至少在一個型別之內),這是學習設計模式的一個誤區。回到上面的三個設計模式上,他們的核心區別更多體現在時機和出發點上:介面卡Adapter強調相容性,橋接Bridge強調抽象與實現的分離,而外觀Facade強調簡化複雜性。我們分辨這些模式也應該從意圖出發來看。

Spring的三層結構也融合體現了Facade和Bridge的設計,Service和Repository之間偏重體現Bridge模式理念,而Controller和Service之間更像Facade模式:Controller整合Service,對外提供API:

Spring應用的三層結構

下面我們再看幾種常見的行為型模式的類比分析:

| | 代理 | 裝飾 | 中介 | | ------ | ------ | ----- | ----- | | UML | proxy.png | decorator.png | mediator.png | | 範例 | - Java Reflect API: Proxy
- Java EJB: Enterprise JavaBean, JavaX Inject, JavaX PersistenceContext
- ActivityManagerActivityManagerService
- PerformanceInspectionServicePerformanceTestManagementService | - Java IO: GZIPOutputStream and OutputStream, Reader and BufferedReader
- java.util.Collections, checkedXXX(), synchronizedXXX()unmodifiableXXX() 系列方法,拓展集合
- HttpServletRequestWrapper and HttpServletResponseWrapper
- JScrollPane | - Java Message Service, JMS by Oracle
- java.util.Timer (all scheduleXXX() methods), java.util.concurrent.ExecutorService (the invokeXXX() and submit() methods) | | 概述 | 通過代理來控制對一個物件的訪問 | 動態地給一個物件新增功能 | 封裝物件之間的互動(傳話筒) | | 關鍵字 | Delegate | Wrapper | MessageQueue, Dispatcher | | 核心角色 | Proxy | Decorator | Mediator |

這裡,從類之間關係上看,代理和裝飾更為相似,而中介則不同,它只是名字上和代理相近。關於代理(訪問和控制)和裝飾(增強和擴充套件)的區分,同樣可以從目的和意圖的角度區分。以代理來為例,它的首要作用是建立訪問通道,比如安卓中,應用和系統之間用Binder來進行IPC,而在應用程序和系統程序間,為了這種IPC呼叫,大量應用了代理模式,名為Proxy的物件隨處可見。而在設計Hydra Lab的過程中,為了讓測試使用者能方便的在測試例項中通過SDK訪問一些Hydra Lab Test Agent的服務方法,我們也應用了一個簡明的靜態代理來實現這種不同環境下的訪問。

在代理模式下,有了訪問通道,自然就可以做到對通訊的控制,比如基於許可權的、或是基於格式驗證的。而裝飾模式著眼於增強、擴充套件,比如BufferedRead對於Reader的增強。從這個角度講,一個類如果叫AuthWrapper就會比較奇怪,AuthProxy則更常見一些,因為授權這種操作明顯更強調控制。當然這取決於具體情境。

中介其實是很寬泛的概念,解耦通訊的雙方或多方,比較火熱的各類MQ框架其實是這個模式的一個衍生。

| | 觀察者 | 訪問者 | | ------ | ------ | ----- | | UML | observer.png | visitor.png | | 範例 | - java.util.Observer, Observable
- java.util.EventListener
- ReactiveX Interface Observer | - AnnotationValueVisitor
- ElementVisitor
- TypeVisitor
- SimpleFileVisitor
- VisitCallback
- ClassVisitor (ASM 9.4) | | 概述 | 對個觀察者監聽一個主題物件 | 表示一種對某物件中各元素的只讀操作 | | 關鍵字 | Observable, Observer, Subject,
Subscription | Visitor | | 核心角色 | Observer, Subject | Visitor, Element |

這兩個模式之間在實現上其實並沒有太多聯絡。但二者都是想去“讀”,不會直接改變被讀物件的狀態。觀察者通過訂閱監聽的方式被動地讀,而訪問者是主動視角,以一種獨特的方式讀。和觀察者很相近的“Listener”,是更常見的概念,更輕量,因而也更廣泛。

| | 責任鏈 | 備忘錄 | | ------ | ------ | ----- | | UML | chain-of-responsibility.png | memento.png | | 範例 | - OkHttp Interceptors
- java.util.logging.Logger
- javax.servlet.Filter | - Activity#onSaveInstanceState(...)
- Java Serializable | | 概述 | 建立處理鏈條傳遞請求 | 捕獲物件狀態並儲存,以備狀態恢復 | | 關鍵字 | Chain, Interceptor, Filter, proceed, Response | State, Lifecycle, Context | | 核心角色 | Handler | Memonto, Originator, Caretaker |

責任鏈和備忘錄模式雖然意圖和設計上都不相同,但二者都有非常濃厚的IoC控制反轉的味道,和生命週期的設計聯絡緊密。玩遊戲的同學對備忘錄模式最容易建立理解,一個存檔就是一個持久化的State,遊戲本身的存讀檔服務作為caretaker,幫你保證你肝的進度不會白費。所以備忘錄模式其實非常的常見,軟體世界裡俯拾皆是。

| | 命令 | 直譯器 | | ------ | ------ | ----- | | UML | command.png | interpreter.png | | 範例 | - IShellOutputReceiver
- Java Runnable | - java.util.Pattern
- java.text.Normalizer
- java.text.Format
- javax.el.ELResolver | | 概述 | 將請求封裝為物件,從而方便引數化和請求佇列管理 | 定義文法和表示方式 | | 關鍵字 | Executor | Expression | | 核心角色 | Command, Receiver, Invoker(Executor) | Interpretor, Expression |

上面兩者也無法直接類比,但是當二者合體,命令的解釋和執行一氣呵成,一個指令碼語言的c執行器雛形就誕生了。這裡的命令模式其實比“命令”本身在設計上有更周全的考慮,它還包括了對執行結果的接收介面的預留。

| | 抽象工廠 | 工廠方法 | | ------ | ------ | ------ | | UML | abstract-factory.png | factory_method.png | | 範例 | - DocumentBuilderFactory(JavaX)
- TransformerFactory(JavaX)
- XPathFactory(JavaX)
- BeanFactory#getBeanProvider(Spring) | java.util.Calendar#getInstance() | | 概述 | 將一個類的介面轉換成滿足另一個要求的介面 | 由工廠的子類決定建立的例項物件 | | 關鍵字 | Factory, new..., create... | Factory, newInstance, Creator | | 核心角色 | AbstractFactory | Creator |

其他模式還包括:建築者模式,原型模式,享元(類似多例),單例;組合;模板方法,迭代器。這些模式或是不常用,或是過於常用常見,且都比較簡單,限於篇幅本文不再一一詳述。

通過這個類比學習的過程,我們可能會逐步感受到,設計模式的重點並不在於類之間關係的嚴格定義、羅列和排布,無意義的爭辯、論證會陷入“把設計模式當作一個嚴格的學術理論”的誤區。更多的,我們應該從問題的意圖出發,發散思考解決方案中可能包含的設計元素,然後根據實際情況精簡到合理的規模。

所以我們不必糾結於相近的兩種模式的嚴格界定和區分,比如,無需辯駁一種實現究竟是用的代理還是裝飾,而是理解這兩種模式的看問題的角度意圖,融會貫通,靈活組合運用:如果你強調的角度是功能拓展,那設計方案就是裝飾;如果你強調的是訪問控制,那就是代理。很多初學者覺得很多模式很相似,感到多餘,這是很正常的感受和學習階段;隨著更多應用和實戰,你會成長和洞察更多模式的意義;後來你已經成為設計大師,靈活運用設計模式、AOP、函式式、演算法乃至ML解決各類問題,講述和推動方案的實現,設計模式的探討和辯論只不過是茶餘飯後的談資。這一點,在原書“怎樣選擇設計模式”章節中,也有提及。

總結來講,初學設計模式,關注點可以放在: - 這個設計模式解決什麼型別的問題,意圖是什麼,以及它如何對概念進行抽象(關鍵角色)和解決(介面、關係)的。 - 用設計模式作為大家溝通軟體設計的語言,掌握這些術語,減少溝通成本。

如何學習和使用設計模式

本部分內容源自GoF原書中1.8章的內容“怎樣使用設計模式”,精簡了原書的7步為6步,並去除了翻譯腔:

  1. 瀏覽一遍該模式,掌握關鍵要素 這個模式的名字是什麼,意圖是什麼,裡面的關鍵角色是什麼,常見的關鍵詞?

  2. 回頭去研究結構部分、參與者部分和協作部分 進一步瞭解角色的職責和關係,有哪些介面,以及模式的適用性:這個模式更適合解決什麼型別的問題?

  3. 看看示例程式碼 例子能讓我們瞭解模式解決的實際問題,成為我們實現的參考。

  4. 參考模式中的命名方法 比如,在Strategy模式中,你可以直接給演算法命名末尾加上Strategy來體現這個模式;再比如,可以用create作為方法的開頭字首來凸顯工廠方法。

  5. 定義類和介面 選定好模式、完成命名後,下一步可以建立好類與介面之間的繼承/實現關係,定義代表資料和物件引用的例項變數。

  6. 實現模式 開始依據模式實現解決方案。

設計模式的引入是帶有一定成本的,學習成本和複雜性的增加就是其中之一,也可能會有效能上的損耗(雖然可以忽略),但它為架構帶來了靈活性,使其更加清晰可維護。接下來,作為拓展閱讀,我們可以探討一下設計模式的意義,獲得更深的理解。

設計模式的意義和批判

談及為什麼需要設計模式時,首先要回答什麼設計是好的設計。軟體是對現實問題複雜性的抽象和管理,Uncle bob說:“軟體應該是可變的”,正如現實世界“唯一不變的就是不斷地變化”,軟體應該能靈活地應對現實世界的需求。所以我們會討論軟體架構的可擴充套件性、可維護性、高可用、可重用、可移植性等。如果你只是在編寫一個又一個的指令碼、一次性工具或者程式設計練習題,當然不用把問題複雜化。但如果你希望你的軟體有更強的生命力和更廣闊的前景,那就要嚴肅對待軟體設計,防止程式碼腐化。

此外,一個人的力量是有限的,如果希望藉助協作來擴大軟體的服務範圍、影響力,那麼可讀性也就重要起來。“Good code is like well-written prose”,好程式碼應該像優美的散文;至少是自解釋的。引述GoF的原文,“所有結構良好的面向物件軟體體系結構中都包含了許多模式...內行的設計者知道:不是解決任何問題都要從頭做起...這些模式解決特定的設計問題,使面向物件設計更靈活、優雅,最終複用性更好。”所以這裡的兩層意思就一方面強調了模式對軟體設計本身的好處,一方面說明了這些模式建立了大家在面向物件設計上的共識和交流基礎。此外,大師們還總結了一些設計原則來框定好的設計。

SOLID設計原則

儘管很多教程將設計原則和設計模式放在一起討論,暗示設計模式是遵從了設計原則,實際上他們並非同出一家。而且設計原則有很多種說法,這裡我們分享Uncle Bob提出的最容易記憶的版本,SOLID 原則: - Single responsibility, 單一職責原則 SRP:就一個類而言,應該僅有一個引起它變化的原因。 - Open-close, 開閉原則 OCP:軟體實體應該對於擴充套件是開放的,對於修改是封閉的。 - Liskov substitution, 里氏代換原則 LSP。子型別必須能夠替換掉它們的父型別。把父類例項替換成子類例項,程式行為不應該有變化。 - Interface segregation, 介面隔離原則 ISP。 - 一個類對另外一個類的依賴性應當是建立在最小的介面上的。 - 客戶端程式不應該依賴它不需要的介面方法(功能)。 - Dependency inverse, 依賴倒轉原則 DIP: - 高層模組不應該依賴低層模組。兩個都應該依賴抽象。 - 抽象不應該依賴細節,細節應該依賴抽象。

我們可以認為這些設計模式是解決設計問題思考的準繩,也可以認為他們只是一種理念。正如Uncle bob 所說:"The SOLID principles are not rules. They are not laws. They are not perfect truths... This is a good principle, it is good advice..."。總之,瞭解這些可以幫助我們把握思考方向,但不能幫我們解決問題。換言之,對於初學者而言,設計原則可能沒有設計模式那樣強的實戰意義。

批判之聲

關於設計模式的批判,源自於對其創立所處時代的主流程式語言的侷限性的挑戰,以及對於面向物件本身的質疑。有人認為設計模式的提出反映了Java和C++自身語言特性的缺失;也有認為如果靈活運用aspect-oriented-programming,就用不著搞出來23種之多囉囉嗦嗦的設計模式了。對此,筆者覺得純粹的理論上的對錯沒那麼重要,軟體開發是科學和藝術的結合地帶,而設計模式是一個時代開發者思考的精華沉澱,能給我們帶來的不僅是具體方案,更多的是解決問題的思維方式,它們本身就存在於大量的程式設計實踐中,GoF對他們進行了提煉和綜述,這本身就是意義巨大的成果了,更何況他們已經成為工程師文化的一部分,成為了術語。例如,我們從Spring中既能看到AOP的應用、函數語言程式設計的應用,也能看到建造者、工廠模式、策略等等的應用。程式設計大師應該是博學和不拘一格的,程式碼的藝術正在於靈活和適時的運用,囿於固執信仰而拒絕經典或者新知斷不可取。

其他常見疑問FAQ

Q: 設計模式和後續流行的的Reactive、函數語言程式設計、AOP、IoC以及DI之間的關係是什麼?

A: 總體上,這些是不同維度的概念,總結為下表:

| 概念 | 釋義(譯) | 範疇 | | --- | --- | --- | | 設計模式 | Software design pattern | 軟體設計的解決方案 | | IoC | 控制反轉 Inversion of control | 軟體架構層面的一種設計模式 | | DI | 依賴注入 Dependency injection | 一種設計模式 | | AOP | Aspect-oriented programming,面向切面的程式設計 | 程式設計正規化(面向物件) | | 函數語言程式設計 | Functional programming | 程式設計正規化(宣告式) | | Reactive | Reactive programming, 響應式程式設計 | 程式設計正規化(宣告式) | | 微服務 | Microservices | 一種架構模式(面向服務的架構) |

Q: 是否還有其他設計模式?

A: 有的,隨著軟體開發實踐的演化,有越來越多的設計模式被總結出來,只不過可能還沒有一本經典將其整理入冊。比如常見的鎖的雙重檢查,也被認為是一種獨立於語言的併發型設計模式。DI也是設計模式 Concurrency Patterns,其角色包括注入器Injector,服務Service,客戶端Client和介面Interfaces。DI也是一種建立型Creational設計模式,其意圖在於優化類之間依賴關係,因而和整個軟體或模組的架構的相關性更密切。

References

  • Design Patterns, Elements of Reusable Object-Oriented Software
  • https://en.wikipedia.org/wiki/Software_design_pattern
  • http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
  • https://en.wikipedia.org/wiki/SOLID
  • https://sites.google.com/site/unclebobconsultingllc/getting-a-solid-start
  • https://en.wikipedia.org/wiki/Law_of_Demeter
  • https://www.jianshu.com/p/8cbc4bf897cb
  • https://coderanch.com/t/99717/engineering/Bridge-Facade-Pattern
  • https://stackoverflow.com/questions/3477962/when-do-we-need-decorator-pattern
  • https://stackoverflow.com/questions/6366385/use-cases-and-examples-of-gof-decorator-pattern-for-io
  • https://stackoverflow.com/questions/1673841/examples-of-gof-design-patterns-in-javas-core-libraries/

關於我

我是風雲信步,目前在微軟中國擔任研發經理。希望在這個空間和大家分享交流技術心得,職業生涯,團隊和專案管理,趨勢動態。旨在暢談、分享和記錄,不拘小節;但也不排除刨根問底、鑽牛角尖。

本人熱愛技術和打碼,尤其享受用技術解決實際問題的過程和結果;相信創造力是頂級能力,是人價值的放大器。此外,本人專注於軟體和程式碼質量、工程效率和研發能效方面多年,目前在微軟和團隊一起推動2023年新開源的專案Hydra Lab的完善與發展;歡迎和我在開源世界組隊打碼,造輪子 or 添磚加瓦。