第3章
行為型模式
本章介紹行為型模式。行為型模式關注對象互動、通信和控制流。大多數行為型模式都基于組合和委托而不是繼承。我們将在本章中研究以下行為型模式:
- 責任鍊模式
- 指令模式
- 解釋器模式
- 疊代器模式
- 觀察者模式
- 中介者模式
- 備忘錄模式
- 狀态模式
- 政策模式
- 模闆方法模式
- 空對象模式
- 通路者模式
3.1 責任鍊模式
計算機軟體是用來處理資訊的,有多種不同的方式來組織和處理資訊。從前文了解到,當我們在讨論面向對象程式設計時,應該賦予一個類單一的職責,進而使得類容易維護和擴充。
設想一個場景,需要對一批從用戶端來的資料進行多種不同的操作,我們會使用多個不同的類負責不同的操作,而不是使用一個類內建所有操作,這樣做能讓代碼松耦合且簡潔。
這些類被稱為處理器,第一個處理器會接收請求,如果它需要執行操作則會進行一次調用,如果不需要則會将請求傳遞給第二個處理器。類似地,第二個處理器确認并将請求傳遞給責任鍊中的下一個處理器。
1.目的
責任鍊模式可以讓處理器按以下方式處理:如果需要則處理請求,否則将請求傳遞給下一個處理器。
2.實作
如圖3-1所示的類圖描述了責任鍊模式的結構和行為。

圖3-1包括以下類:
- Client(用戶端):用戶端是使用責任鍊模式的應用程式的主要結構。它的職責是執行個體化一個處理器的鍊,然後在第一個對象中調用handleRequest方法。
- Handler(處理器):這是一個抽象類,提供給所有實際處理器進行繼承。它擁有一個handleRequest方法,用來接收需要處理的請求。
- ConcreteHandler(具體處理器):這是一個實作了handleRequest方法的具體類。每一個具體處理器都維持一個引用,指向鍊中下一個具體處理器,需要檢查它自身是否能處理這個請求,不能就将請求傳遞給鍊中的下一個具體處理器。
每一個處理器需要實作一個方法,該方法被用戶端所使用,并能夠設定下一個處理器,當它無法處理請求時,将請求傳給下一個處理器。這個方法可以加入到Handle基類當中。
在每一個ConcreteHandler類中,我們實作下列代碼,檢查它是否能處理請求,不能則會傳遞請求:
用戶端負責在調用鍊頭之前建立處理器鍊。這次調用會被傳遞,直到發現了能正确處理這個請求的處理器。
以汽車服務程式為例。每有一個損壞的汽車進入,首先由機修工進行檢查,如果在機修工的專業範圍内,機修工會對汽車進行維修。如果機修工不會維修,他們會把損壞的汽車傳遞給電工。如果電工也無法修理壞車,他們會将車交給下一個專家。
圖3-2展示了如何運作。
3.适用情況和示例
以下是責任鍊模式的适用情況和示例:
- 事件處理器:舉個例子,大部分圖形使用者界面架構使用責任鍊模式來處理事件。例如,一個視窗包含了一個面闆,面闆上有一些按鈕,我們需要寫按鈕的事件處理器。如果我們決定跳過它并傳遞它,責任鍊中的下一個處理器面闆将會處理這個請求。如果面闆也跳過了它,它将會被傳遞到視窗。
- 日志處理器:與事件處理器類似,每一個處理器都要麼記錄一個基于其狀态的特殊請求,要麼将請求傳送給下一個處理器。
- servlet:在Java中,javax.servlet.Filter( http://docs.oracle.com/javaee/7/api/javax/servlet/Filter.html )被用來過濾請求或者響應。doFilter方法把過濾器鍊作為一個參數接收,它能夠傳遞請求。
3.2 指令模式
在面向對象程式設計當中,一個很重要的事情是設計能夠使得代碼松耦合。舉個例子,我們需要開發一個複雜的程式,用來繪制諸如點、線、線段、圓、矩形等許多圖形。
為了讓代碼能夠實作所有種類的形狀,我們需要實作很多操作來處理菜單操作。為了讓程式可維護,我們需要建立一個統一的方法來定義所有的指令,這樣做便能夠将所有實作細節隐藏在程式之中(這個程式實際上就是用戶端)。
指令模式能夠做到:
- 提供一個統一的方法來封裝指令和其所需要的參數來執行一個動作。
- 允許處理指令,例如将指令存儲在隊列中。
如圖3-3所示的類圖展示了指令模式的實作。
前面的類圖中包括以下元素:
- Command(指令類):這是表示指令封裝的抽象類。它聲明了執行的抽象方法,該方法應該由所有具體指令實作。
- ConcreteCommand(具體指令類):這是指令類的實際實作。它必須執行指令并處理與每個具體指令相關的參數。它将指令委托給接收者。
- Receiver(接收者):這是負責執行與指令關聯的操作的類。
- Invoker(調用者):這是觸發指令的類。通常是外部事件,例如使用者操作。
- Client(用戶端):這是執行個體化具體指令對象及其接收者的實際類。
最初,我們的想法是在一個大的if-else塊中處理所有可能出現的指令:
之後,我們決定為繪圖程式應用指令模式。首先建立一個指令接口:
下一步是将所有對象(如菜單項和按鈕)定義為類,實作指令接口和execute()
方法:
在重複上一個操作并為每個可能的操作建立一個類之後,用以下方法替換前面實作的if-else代碼塊:
從代碼中看到調用者(觸發performAction方法的用戶端)和接收者(實作指令接口的類)是分離的。我們可以輕松擴充代碼而無須更改它。
指令模式的适用性和示例如下:
- Undo/Redo operation(撤銷/重做操作):指令模式允許我們将指令對象存儲在隊列中。這樣就可以實作撤銷和重做操作。
- Composite command(組合指令):複雜指令可以使用組合模式由簡單指令組成,并按順序運作。通過這種方式,我們可以以面向對象的方式建構宏。
-
The asynchronous method invocation(異步方法調用):指令模式用于多線程應用程式。指令對象可以在背景以單獨的線程執行。
java.lang.Runnable是一個指令接口。
在以下代碼中,Runnable接口充當指令接口,由RunnableThread實作:
用戶端調用指令以啟動新線程:
3.3 解釋器模式
計算機用來解釋句子或表達式。當需要編寫一系列處理這種需求的代碼時,首先要知道句子或表達式的結構,要有一個表達式或句子的内部表示。多數情況下,最合适的結構是基于組合模式的組合結構。我們将在第4章中進一步讨論組合模式。現在可以将組合表示視為将相似性質的對象集合在一起。
解釋器模式定義文法的表示以及該文法的對應解釋。
解釋器模式使用組合模式來定義對象結構的内部表示。除此之外,它還添加了實作來解釋表達式并将其轉換為内部結構。是以,解釋器模式屬于行為型模式。解釋器模式的類圖如圖3-4所示。
解釋器模式由以下類組成:
- Context(環境):Context用于封裝解釋器的全局資訊,所有具體的解釋器均需通路Context。
- AbstractExpression(抽象表達式):一個抽象類或接口,聲明執行的解釋方法,由所有具體的解釋器實作。
- TerminalExpression(終結符表達式):一種解釋器類,實作與文法的終結符相關的操作。終結符表達式必須始終被實作和執行個體化,因為它表示表達式的結尾。
- NonTerminalExpression(非終結符表達式):這是實作文法的不同規則或符号的類。對于每一個文法都應該建立一個類。
在實踐當中,解釋器模式用來解釋正規表達式。為這種場景實作解釋器模式是一個很好的練習,這裡我們選擇一個簡單的文法作為例子。我們将應用解釋器模式來解析帶有一個變量的簡單函數f (x)。
為了簡單,我們選擇逆波蘭表示法,這是一種在運算符末尾添加操作數的表示法。1 + 2變為1 2 +, (1 + 2)* 3變為1 2 + 3 *。優點是不再需要括号,是以它簡化了任務。
下面的代碼為表達式建立了接口:
實作具體類需要下列元素:
- Number(數字)類:它解釋所有數字。
- Operatorc((操作符)+、-、*、/)類:在下面的例子中,将使用加号(+)和減号(-)。
帶你讀《Java設計模式及實踐》之三:行為型模式第3章
現在到了複雜的部分,我們需要實作操作符類,操作符類是組合表達式,由兩個表達式組合而成:
類似地,接下來實作一個減号類:
可以看到,我們已經建立了一個類,該類允許我們建構一棵這樣的文法樹:操作是節點,變量和數字是葉子。結構非常複雜,可用于解釋表達式。
現在寫一段代碼,通過建立好的類來實作文法樹:
解釋器模式适用于表達式被解釋并轉換為其内部表示的情況。内部表示是基于組合模式的,是以解釋器模式不适用于複雜的文法。
Java在java.util.Parser中實作了解釋器模式,它用于解釋正規表達式。在解釋正規表達式時傳回比對器對象。比對器使用基于正規表達式的模式類建立的内部結構:
3.4 疊代器模式
疊代器模式可能是Java中最廣為人知的模式之一。Java程式員在使用集合(collection)
時,并不需要關注其類型是數組、清單、集合(set)還是其他,有些人并不知道這些集合包其實是使用了疊代器模式來實作的。
我們可以以相同的方式處理集合,無論它是清單還是數組,這是因為它提供了一種疊代其元素而不暴露其内部結構的機制。更重要的是,不同類型的集合能夠使用相同的統一的機制。這種機制被稱為疊代器模式。
疊代器模式提供了一種順序周遊聚合對象元素而不暴露其内部實作的方法。
疊代器模式基于兩個抽象類或接口,可以通過成對的具體類來實作。類圖如圖3-5所示。
疊代器模式使用了以下類:
- Aggregate(抽象容器):應該由所有類實作的抽象類,并且可以由疊代器周遊。這對應于java.util.Collection接口。
- Iterator(抽象疊代器):抽象疊代器是疊代器抽象類,它定義周遊容器對象的操作以及傳回對象的操作。
- ConcreteAggregate(具體容器):具體容器可以實作内部不同的結構,但會暴露處理周遊容器的具體疊代器。
- ConcreteIterator(具體疊代器):這是處理特定具體容器類的具體疊代器。實際上,對于每個具體容器,必須實作一個具體疊代器。
每一個Java程式員在日常工作中都會使用疊代器。讓我們看看如何實作疊代器。首先,定義一個簡單的疊代器接口:
然後實作一個簡單的容器,它維護一個String類型數組:
我們在容器中嵌套了疊代器類。這是最好的選擇,因為疊代器需要通路容器的内部變量。我們可以在這裡看到它的外觀:
如今,疊代器在大多數程式設計語言中都很流行,它可能是Java中使用最廣泛的集合包。當使用以下循環結構周遊集合時,它也在語言級别實作:
可以使用泛型機制來實作疊代器模式,這樣就可以避免強制轉換生成的運作時錯誤。
在Java現有版本中的java.util.Iterator 類和java.util.Collection 類,是實作新容器和疊代器很好的例子。當需要具有特定行為的容器時,我們應該考慮擴充java.
collection包中實作的一個類,而不是建立一個新類。
3.5 觀察者模式
随着本書的進展,我們不斷提到解耦的重要性。當減少依賴關系時,我們可以擴充、開發和測試不同的子產品,而無須了解其他子產品的實作細節,隻需要知道它們實作的抽象。
盡管如此,在實踐當中,子產品是需要協同工作的。一個對象往往能夠知道另一個對象的變化。例如在遊戲中實施汽車類,汽車的引擎需要知道加速器何時改變其位置。一般的解決方案是建立一個引擎類,一直輪詢檢查加速器位置,看它是否已經改變。而更智能的方法是使加速器調用引擎以通知它有關更改。但如果想得到設計良好的代碼,這還不夠。
如果加速器(Accelerator)類保留對引擎(Engine)類的引用,當需要在螢幕上顯示Accelerator的位置時會發生什麼?最好的解決方案是讓兩者都依賴于抽象,而不是讓加速器依賴于引擎。
觀察者模式使得一個對象的狀态改變時,已經登記的其他對象能夠觀察到這一改變。
觀察者模式的類圖如圖3-6所示。
觀察者模式依賴于以下類:
- Subject(主題):主題通常是由類實作的可觀察的接口。應通知的觀察者使用attach方法注冊。當它們不再需要被告知變更時,使用detach方法取消注冊。
- ConcreteSubject(具體主題):具體主題是一個實作主題接口的類。它處理觀察者清單并更新它們的變化。
- Observer(觀察者):觀察者是一個由對象實作的接口,應該根據主題中的更改來進行更新。每個觀察者都應該實作update方法,該方法通知它們新的狀态變化。
3.6 中介者模式
在許多情況下,當設計和開發軟體應用程式時會遇到這樣的場景,程式中有必須互相通信的子產品和對象,最簡單的實作方法是讓它們彼此了解并直接發送消息。
但是,這種做法可能會造成混亂。例如,想象一個通信應用程式,程式中每個用戶端必須連接配接到另一個用戶端,那麼用戶端需要管理許多連接配接,這對于用戶端來說其實并沒有意義。更好的解決方案是讓用戶端都連接配接到中央伺服器,讓伺服器管理用戶端之間的通信。用戶端将消息發送到伺服器,伺服器對用戶端所有的連接配接都保持活動狀态,并且可以向所有收件人廣播消息。
另一個例子是需要一個專門的類來在圖形界面中的不同控件之間扮演中介者,這些控件包括按鈕、下拉清單和清單。例如,GUI中的圖形控件可以保持對彼此的引用,以便互相調用它們的方法。但顯然這麼做會建立一段耦合度高的代碼,其中每個控件都依賴于所有其他控件。更好的方法是在需要完成某些事情時讓視窗負責向所有必需的控件廣播消息。當控件中的某些内容修改時,它會通知視窗,該視窗将檢查哪些控件需要通知,然後通知它們。
中介者模式定義了一個對象,該對象封裝了一組對象的互動方式,進而減少了它們之間的互相依賴。
中介者模式基于兩個抽象—Mediator和Colleague,如圖3-7所示。
中介者模式依賴于以下類:
- Mediator(抽象中介者):抽象中介者定義了參與者的互動方式。在此接口或抽象類中聲明的操作與場景相關。
- ConcreteMediator(具體中介者):它實作了中介者聲明的操作。
- Colleague(抽象同僚角色):這是一個抽象類或接口,用于定義需要調解的參與者如何進行互動。
- ConcreteColleague(具體同僚角色):這是實作Colleague接口的具體類。
當有許多實體以類似的方式進行互動并且這些實體應該解耦時,就應該使用中介者模式。
在Java庫中,中介者模式用于實作java.util.Timer。timer(計時器)類可用于排程線程以固定間隔運作一次或重複多次運作。線程對象對應于ConcreteColleague類。timer
類實作了管理背景任務執行的方法。
3.7 備忘錄模式
封裝是面向對象設計的基本原則之一。我們知道每個類都承擔一項職責。當向對象添加功能時,我們可能意識到需要儲存其内部狀态,以便能夠在以後階段恢複它。如果直接在類中實作這樣的功能,這個類可能會變得太複雜,最終可能會違反單一職責原則。同時,封裝阻止我們直接通路需要記憶的對象的内部狀态。
備忘錄模式用于儲存對象的内部狀态而不破壞其封裝,并在以後階段恢複其狀态。
備忘錄模式依賴于三個類—Originator、Memento和Caretaker,如圖3-8所示。
備忘錄模式依賴于以下類:
- Originator(發起者):發起者是我們需要記住狀态的對象,以便在某個時刻恢複它。
- Caretaker(管理者):這是負責觸發發起者的變化或觸發發起者傳回先前狀态的動作的類。
- Memento(備忘錄):這是負責存儲發起者内部狀态的類。備忘錄提供了兩種設定和擷取狀态的方法,但這些方法應該對管理者隐藏。
備忘錄實際上比聽起來容易得多。我們将它應用于汽車服務應用程式。機械師必須測試每輛車。他們使用自動裝置測量汽車的所有輸出,以獲得不同的參數(速度、擋位、制動器等)。他們執行所有測試并且必須重新檢查那些看起來可疑的測試。
首先建立originator類,我們将它命名為CarOriginator,添加兩個成員變量。state表示測試運作時汽車的參數,這是我們想要儲存的對象的狀态。第二個成員變量是結果,這是汽車的測量輸出,我們不需要将其存儲在備忘錄中。這是帶有空嵌套備忘錄的發起者:
現在我們為不同的狀态運作汽車測試:
3.适用情況
隻要需要執行復原操作,就會使用備忘錄模式。它可用于各種原子事務,如果其中一個操作失敗,則必須将對象恢複到初始狀态。
3.8 狀态模式
有限狀态機是計算機科學中的一個重要概念。它具有強大的數學基礎,代表了一個可以處于有限數量狀态的抽象機器。有限狀态機用于計算機科學的所有領域。
狀态模式隻是面向對象設計中的有限狀态機的實作。類圖如圖3-9所示。
3.9 政策模式
行為模式的一個特定情況,是我們需要改變解決一個問題與另一個問題的方式。正如在第1章中學到的那樣,改變是不好的,而擴充是好的。是以,我們可以将兩塊代碼封裝在一個類中,而不是用一部分代碼替換另一部分代碼。然後可以建立代碼所依賴類的抽象。這樣會使代碼變得非常靈活,我們可以使用任何實作了剛剛建立的抽象的類。
政策模式定義了一系列算法,封裝了每個算法,并使它們可以互換。
政策模式的結構實際上與狀态模式的相同。但是實作和意圖完全不同。如圖3-10所示。
政策模式非常簡單:
- Strategy(抽象政策):特定政策的抽象。
- ConcreteStrategy(具體政策):實作抽象政策的類。
- Context(環境):運作特定政策的類。
3.10 模闆方法模式
顧名思義,模闆方法模式為代碼提供了一個模闆,可以由實作不同功能的開發人員填寫。了解這一點的最簡單方法是考慮HTML模闆。你通路的大多數網站都遵循某種模闆。例如,網站通常有頁眉、頁腳和側邊欄,它們之間會有核心内容。這意味着模闆定義了頁眉、頁腳和側邊欄,每個内容編寫者都可以使用此模闆添加其内容。
使用模闆方法模式的目的是避免編寫重複的代碼,以便開發人員可以專注于核心邏輯。
模闆方法模式實作的最好方式是使用抽象類。抽象類可以提供給我們所知道的實作區域,預設實作和為實作而保持開放的區域即為抽象。
例如,實作一個非常進階别的資料庫抽取查詢。我們需要執行以下步驟:
1)建立一個資料庫連接配接;
2)建立一個query語句;
3)執行query語句;
4)解析并傳回資料;
5)關閉資料庫連接配接。
可以看到,打開和關閉連接配接部分都是一樣的,是以可以用模闆方法模式實作這一部分,其餘部分則根據需要獨立地實作。
3.11 空對象模式
空對象模式是本書中涉及的最輕的模式之一。有時它被認為隻是政策模式的一個特例,考慮到它在實踐中的重要性,它也有自己獨特的部分。如果我們使用測試驅動的方法開發程式,或者隻是想在沒有應用程式的其餘部分的情況下開發子產品,可以簡單地用模拟類替換沒有的類,模拟類具有相同的結構但是什麼也不做。
實作
在圖3-11中可以看到我們隻是建立了一個NullClass,它可以替換程式中的真實類。如前所述,這隻是我們選擇什麼都不做的政策模式的一個特例。
3.12 通路者模式
回到我們在讨論指令模式時介紹的形狀應用程式。我們應用了指令模式,是以必須重做已實作的操作。現在考慮增加儲存功能。
如果将一個抽象的Save方法添加到基本形狀類中,并且為每個形狀擴充它,我們就解決了問題。這個解決方案可能是最直覺的,但不是最好的。首先,每個類都應該承擔一項責任。其次,如果需要更改我們想要儲存每個形狀的格式會發生什麼?如果實作相同的方法來生成XML,那麼是否必須更改為JSON格式?這種設計絕對不遵循開放/封閉原則。
通路者模式将操作與其操作的對象結構分開,允許添加新操作而不更改結構類。
通路者模式在單個類中定義了一組操作:它為每個類型的對象定義一個方法,該方法來自它必須操作的結構。隻需建立另一個通路者即可添加一組新操作。類圖如圖3-12所示。
通路者模式基于以下類:
- Element(元素):表示對象結構的基類。結構中的所有類都是它派生的,它們必須實作
- accept(visitor:Visitor)方法。
- ConcreteElementA(具體元素A)和ConcreteElementB(具體元素B):這是我們想要添加在Visitor類中實作的外部操作的具體類。
- (Visitor)通路者:這是基本的Visitor類,它聲明了與每個ConcreteElementA相對應的方法。方法的名稱相同,但每種方法都按其接受的類型區分。我們可以采用這種解決方案,因為在Java中可以使用名稱相同而實際不一樣的方法,但如果有需要,我們可以聲明具有不同名稱的方法。
- ConcreteVisitor(具體通路者):這是通路者的實作。當需要一組單獨的操作時,隻需建立另一個通路者。
3.13 總結
本章讨論了各種行為型模式。我們研究了一些常用的行為型模式,例如責任鍊、指令模式、解釋器模式等。這些模式有助于我們以受控方式來管理對象的行為。在下一章中,我們将研究有助于我們管理複雜結構的結構型模式。