1 設計模式簡介
軟件設計模式是一套被反覆使用的,多數人知曉的、經過分類編寫、代碼設計經驗的總結。它描述了在軟件設計過程中的一些不斷重複發生的問題,以及該問題的解決方案,它是解決特定問題的一系列套路,是前輩們代碼設計經驗的總結,具有一定的普遍性,可以反覆使用。軟件設計模式的目的是爲了提高代碼的可重用性,代碼的可讀性和代碼的可靠性。
1.1 設計模式和原則
一個具有良好結構的設計具備如下基本的特徵。
1.2 學習設計模式的意義
設計模式的本質是面向對象設計原則的實際運用,是對類的封裝性、繼承性和多態性以及類的關聯關係和組合關係的充分理解。正確使用設計模式具有以下優點:
- 可以提高程序員的思維能力、編程能力和設計能力。
- 使程序設計更加標準化、代碼編制更加工程化,使軟件開發效率大大提高,從而縮短軟件的開發週期。
- 使設計的代碼可重用性高、可讀性強、可靠性高、靈活性好、可維護性強。
- 可以大大增強代碼的質量,在提升代碼結構的同時,使得代碼性能的優化也成爲了可能。
2 行爲型模式組成
在閱讀Java設計模式中,我曾經摘抄道:“世界上80%的人,都在默默無聞中渡過自己的一輩子,都在抱怨中過着每天的日子,都在對社會以及對周圍的親人和朋友不滿足來打發日誌。”自己也是一個喜歡抱怨的人,經常抱怨自己的工作、抱怨身邊的人, 抱怨真的很可怕。曾國藩曾經說過:“牢騷太甚者,其後必多抑塞。蓋無故而怨天,則天必不許,無故而尤人,則人必不服,感應之理然也。”抱怨太多,就是折磨自己。而且抱怨解決不了問題。與其抱怨,不如改變。有時間抱怨,不如想辦法改變,讓自己擺脫困境,很多時候,我們雖然無法改變已經發生的事實,但是我們可以轉變自己的心態。
2.1 行爲型設計模式的特徵
2.2 種類
在Java設計模式及實踐中,一共有12種設計模式的實踐屬於此類。
由於一些較爲常見的設計模式已經習得,便不再此處贅述簡要闡述,在之後的行文中,主要闡述如下的行爲型設計模式:
- 命令模式
- 解釋器模式
- 中介者模式
- 備忘錄模式
- 狀態模式
- 策略模式
- 訪問者模式
2.3 行爲型設計模式詳解
2.3.1 責任鏈模式
責任鏈,通過其名稱,我們可以認識到,有一個對象的鏈的存在,通過鏈表的方式串聯起來,而請求的處理則是沿着這條鏈進行,直到有一個對象能處理這個請求。
使多個對象都有機會處理請求,從而避免請求的發送者和接收者之間的耦合關係。將這些對象連成一條鏈,
並沿着這條鏈傳遞該請求,直到有一個對象處理它爲止。
這一模式的想法是,給多個對象處理一個請求的機會,從而解耦發送者和接受者.
2.3.1.1 參與者
-
Handler
定義一個處理請求的接口。
(可選)實現後繼鏈。 -
ConcreteHandler
處理它所負責的請求。
可訪問它的後繼者。
如果可處理該請求,就處理之;否則將該請求轉發給它的後繼者。 -
Client
向鏈上的具體處理者(ConcreteHandler)對象提交請求。在實際使用中,由Client負責對象鏈的構建,實例化一個處理器的鏈,然後在第一個對象中調用handleRequest,其實如果想要實現的簡單一點,只要把具體處理者放在一個列表中,然後讓請求輪流走過找到合適的處理着直到請求得到處理也行的。
2.3.1.2 UML類圖
在具體的處理器類中有如下的邏輯:
protected Handler successor;
public void setSuccessor(Successor successor) {
this.successor = successor;
}
public void handleRequest(Request request) {
if (canHandle(request)) {
// code to handle the request
} else {
successor.handleRequest()
}
}
2.3.1.3 參見
2.3.2 命令模式
將一個請求封裝爲一個對象,從而使你可用不同的請求對客戶進行參數化;對請求排隊或記錄請求日誌,以及支持可撤消的操作。
2.3.2.1 適用性
1.抽象出待執行的動作以參數化某對象。
2.在不同的時刻指定、排列和執行請求。
3.支持撤銷和重做操作操作。
4.支持修改日誌,這樣當系統崩潰時,這些修改可以被重做一遍。
5.用構建在原語操作上的高層操作構造一個系統。
6.異步方法調用。
2.3.2.2 UML
2.3.2.3 實踐
直觀的想法
public void performAction(ActionEvent e) {
Object obj = e.getSource();
if (obj = fileNewNameMenuItem) {
doFileNewAction();
} else if (obj = fileOpenMenuItem) {
doFileOpenAction();
} else if (obj = fileOpenRecentMenuItem) {
doFileOpenRecentMenuAction()
} else {
doFileSaveAction();
}
}
上圖是用來處理一個客戶端菜單的按鈕的處理邏輯。起初想法是在一個大的if-else中處理所有可能出現的命令。
之後決定進行如下的修改,使用命令模式:
public interface Command {
public void execute();
}
public class OpenMenuItem extends JMenuItem implements Command {
public void execute() {
// code to open a document
}
}
publicvoid performAction(ActionEvent e) {
Command command = (Command)e.getSource();
command.execute();
}
可以看到代碼消滅了冗長的if-else,使得代碼更加緊湊,可讀性也更突出。
在《重構2》中,Martin也闡述了使用命令模式來取代函數的用法。可以學習。
2.3.2.4 參考
2.3.3 解釋器模式
這種模式主要是用來解釋句子或表達式。首先要知道的是句子和表達式的結構,要有一個表達式或句子的內部表示。可以使用解釋器模式來處理逆波蘭表達式
解釋器模式一般使用組合模式來定義對象結構的內部表示。
2.3.3.1 UML類圖
2.3.3.2 參與者
- AbstractExpression(抽象表達式)
聲明一個抽象的解釋操作,這個接口爲抽象語法樹中所有的節點所共享。 - TerminalExpression(終結符表達式)
實現與文法中的終結符相關聯的解釋操作。
一個句子中的每個終結符需要該類的一個實例。 - NonterminalExpression(非終結符表達式)
爲文法中的非終結符實現解釋(Interpret)操作。 - Context(上下文)
包含解釋器之外的一些全局信息。 - Client(客戶)
構建(或被給定)表示該文法定義的語言中一個特定的句子的抽象語法樹。
該抽象語法樹由NonterminalExpression和TerminalExpression的實例裝配而成。
調用解釋操作。
在實現時,可以定義Expression表達式,然後讓所有的節點均實現該接口。
public interface Expression {
public void interpret();
}
public class Number implements Expression {
private float number;
public Number(float number) {
this.number = number;
}
public void interpret() {
return number;
}
}
public class Plus implements Expression {
Expression left;
Expression right;
public void interpret() {
return left.interpret() + right.interpret();
}
}
因此,通過這些類型我們建立了一課語法樹:操作是節點,變量和數字是葉子。結構非常複雜,可用於解釋表達式。
2.3.4 迭代器模式
提供一種方法順序遍歷對象元素而不暴露其內部實現的方法。
2.3.4.1 UML類圖
其實迭代器非常常用,幾乎每天都要使用。
public interface Iterator
{
public Object next();
public boolean hasNext();
}
只不過要在實現的容器中實現該迭代器接口,以某種策略遍歷容器中的元素。
2.3.4.2 實踐
2.3.5 觀察者模式
我們不斷提到解耦的重要性,當減少依賴時,我們可以擴展、開發和測試不同的模塊,而無須瞭解其他模塊的實現細節。
觀察者模式定義對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,所有依賴於它的對象都得到通知並被自動更新。
2.3.5.1 UML
2.3.5.2 參與者
-
Subject(目標)
目標知道它的觀察者。可以有任意多個觀察者觀察同一個目標。
提供註冊和刪除觀察者對象的接口。 -
Observer(觀察者)
爲那些在目標發生改變時需獲得通知的對象定義一個更新接口。 -
ConcreteSubject(具體目標)
將有關狀態存入各ConcreteObserver對象。
當它的狀態發生改變時,向它的各個觀察者發出通知。 -
ConcreteObserver(具體觀察者)
維護一個指向ConcreteSubject對象的引用。
存儲有關狀態,這些狀態應與目標的狀態保持一致。
實現Observer的更新接口以使自身狀態與目標的狀態保持一致
2.3.5.3 實踐
2.3.6 中介者模式
本質是解耦了多個同事之間的關係,每個對象都有中介者對象的引用,只跟中介者對象打交道,我們通過中介者對象統一管理這些交互關係。
即同事與同事之間並不交互,而每個同事都保存了一箇中介者對象,由該對象與其他同事進行交互。
用一箇中介對象來封裝一系列的對象交互。中介者使各對象不需要顯式地相互引用,從而使其耦合鬆散,而且可以獨立地改變它們之間的交互。
2.3.6.1 UML
2.3.6.2 參與者
-
Mediator
中介者定義一個接口用於與各同事(Colleague)對象通信。 -
ConcreteMediator
具體中介者通過協調各同事對象實現協作行爲。
瞭解並維護它的各個同事。 -
Colleagueclass
每一個同事類都知道它的中介者對象。
每一個同事對象在需與其他的同事通信的時候,與它的中介者通信
2.3.6.3 實踐
參見GOF23設計模式之中介者模式的實現
在跨部門協作時,比如與算法組溝通,要開發一個新的算法,我們是要與算法組長溝通的,而不會直接和算法組具體的某個人進行溝通的。此時組長,就相當於一箇中介者。
public interface Mediator {
public void register(String dname, Deparment d);
public void command(String dname);
}
/** * 時間:2015年4月12日09:59:50
* 抽象同事類:抽象出所有部門的共同之處。
* */
package com.bjsxt.cn.mediator;
public interface Deparment {
public void selfAction();
public void outAction();
}
2.3.7 備忘錄模式
封裝是面向對象設計的基本原則之一。我們知道類都承擔一項職責。當向對象添加功能時,我們可能意識到需要保存其內部狀態,以便能夠在以後階段恢復它。這很常見,尤其在Notepad++和IDEA這類工具中,撤銷操作和取消撤銷等動作便使用了備忘錄模式。
備忘錄模式的目的是用於保存對象的內部狀態而不破壞其封裝結構,並在以後階段恢復其狀態。執行的是類似還原現場的工作
2.3.7.1 UML類圖
2.3.7.2 參與者
-
Memento
備忘錄存儲原發器對象的內部狀態。 -
Originator
類似於客戶端
原發器創建一個備忘錄,用以記錄當前時刻它的內部狀態。
使用備忘錄恢復內部狀態. -
Caretaker
負責保存好備忘錄。
不能對備忘錄的內容進行操作或檢查。
2.3.7.3 實踐
package com.chapter3.memento;
public class CarOriginator {
private String state;
public void setState(String state) {
this.state = state;
}
public String getState() {
return this.state;
}
public Memento saveState() {
return new Memento(this.state);
}
public void restoreState(Memento memento) {
this.state = memento.getState();
}
/**
* Memento class
*/
public static class Memento {
private final String state;
public Memento(String state) {
this.state = state;
}
private String getState() {
return state;
}
}
}
state表示測試運行時汽車的參數,這是我們想要保存的對象的狀態。
package com.chapter3.memento;
public class CarCaretaker {
public static void main(String s[]) {
new CarCaretaker().runMechanicTest();
}
public void runMechanicTest() {
CarOriginator.Memento savedState = new CarOriginator.Memento("");
CarOriginator originator = new CarOriginator();
originator.setState("State1");
originator.setState("State2");
savedState = originator.saveState();
originator.setState("State3");
originator.restoreState(savedState);
System.out.println("final state:" + originator.getState());
}
}
2.3.7.4 適用情況
只要需要執行回滾操作,就會使用備忘錄。
2.3.8 狀態模式
狀態模式知識面向對象設計中的有限狀態機的實現。
狀態模式定義:對象行爲的變化是由於狀態的變化引入,那麼即當內部狀態發生變化的時候,就會改變對象的行爲,而這種改變視乎就改變了整個類。
2.3.8.1 UML
2.3.8.2 適用性
-
一個對象的行爲取決於它的狀態,並且它必須在運行時刻根據狀態改變它的行爲。
-
一個操作中含有龐大的多分支的條件語句,且這些分支依賴於該對象的狀態。
這個狀態通常用一個或多個枚舉常量表示。
通常,有多個操作包含這一相同的條件結構。
State模式將每一個條件分支放入一個獨立的類中。
這使得你可以根據對象自身的情況將對象的狀態作爲一個對象,這一對象可以不依賴於其他對象而獨立變化。
2.3.8.3 實踐
Java重構-策略模式、狀態模式、衛語句
阿里巴巴出品的Java開發手冊提出,如果if-else分支過多,則可以使用衛語句、狀態模式、策略模式替換。
2.3.8.4 參與者
- Context
定義客戶感興趣的接口。
維護一個ConcreteState子類的實例,這個實例定義當前狀態。 - State
定義一個接口以封裝與Context的一個特定狀態相關的行爲。 - ConcreteStatesubclasses
每一子類實現一個與Context的一個狀態相關的行爲。
2.3.9 策略模式
行爲模式的一個特定情況,是我們需要改變解決一個問題與另外一個問題的方式。本質是分離算法,選擇實現。策略模式對應於解決某一個問題的一個算法組,允許用戶從該算法族中任意選擇一個算法解決某一問題,同時可以方便的更換算法或增加新的算法,並由客戶端決定調用哪個算法。
策略模式與狀態模式非常相似,狀態模式多用於有限狀態機,狀態之間的躍遷影響到行爲,而策略則多對應於某一個問題的具體算法。以禪道Bug而言,對於一個Bug而言有三種狀態,已激活,延遲處理和已關閉。
2.3.9.1 UML
策略模式的結構與狀態模式相同。
2.3.9.2 參與者
-
Strategy
定義所有支持的算法的公共接口。Context使用這個接口來調用某ConcreteStrategy定義的算法。 -
ConcreteStrategy
以Strategy接口實現某具體算法。 -
Context
用一個ConcreteStrategy對象來配置。
維護一個對Strategy對象的引用。
可定義一個接口來讓Stategy訪問它的數據。
2.3.9.3 實踐
策略模式+工廠模式 去除if-else
解鎖新姿勢:探討複雜的 if-else 語句“優雅處理”的思路
2.3.10 模板方法模式
使用模板方法模式的目的是避免編寫重複的代碼,以便開發人員可以專注於核心邏輯。
模板方法模式現實的最好方式是抽象類。抽象類可以提供給我們所知道的實現區域,默認實現和爲實現而保持開發的區域即爲抽象。
定義一個操作中的算法的骨架,而將一些步驟延遲到子類中。
TemplateMethod使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟。
2.3.10.1 參與者
- AbstractClass
定義抽象的原語操作(primitiveoperation),具體的子類將重定義它們以實現一個算法的各步驟。
實現一個模板方法,定義一個算法的骨架。
該模板方法不僅調用原語操作,也調用定義在AbstractClass或其他對象中的操作。 - ConcreteClass
實現原語操作以完成算法中與特定子類相關的步驟。
2.3.10.2 UML
2.3.10.3 實踐
這個模板方法已經得到了充分的實踐了。在智慧營區發過程中Task、EventResponse頻繁使用的就是模板方法。maven的生命週期如下:
2.3.11 空對象模式
模擬類具有相同的結構,但是什麼也不做。
2.3.11.1 UML
2.3.11.2 實踐
智慧營區中NullServiceEntity扮演了類似的結構。在JDK8引入的Optional中,有一個單例的成員empty也起到了類似的作用。
2.3.12 訪問者模式
訪問者模式的目的是將操作與其操作的對象結構分開,允許添加新操作而不更改結構類。
訪問者模式在單個類中定義了一組操作:它爲每個類型的對象定義一個方法,該方法來自它必須操作的結構。只需創建另一個訪問者即可添加一組新操作。
2.3.12.1 UML
2.3.12.2 參與者
- Visitor
爲該對象結構中ConcreteElement的每一個類聲明一個Visit操作。
該操作的名字和特徵標識了發送Visit請求給該訪問者的那個類。
這使得訪問者可以確定正被訪問元素的具體的類。
這樣訪問者就可以通過該元素的特定接口直接訪問它。 - ConcreteVisitor
實現每個由Visitor聲明的操作。
每個操作實現本算法的一部分,而該算法片斷乃是對應於結構中對象的類。
ConcreteVisitor爲該算法提供了上下文並存儲它的局部狀態。
這一狀態常常在遍歷該結構的過程中累積結果。 - Element
定義一個Accept操作,它以一個訪問者爲參數。 - ConcreteElement
實現Accept操作,該操作以一個訪問者爲參數。 - ObjectStructure
能枚舉它的元素。
可以提供一個高層的接口以允許該訪問者訪問它的元素。
可以是一個複合或是一個集合,如一個列表或一個無序集合。
2.3.12.3 適用場景
命令模式與訪問者有很大的相似性。
如果將一個抽象的save方法添加到基本形狀類中,並且爲每個形狀擴展它,我們就解決了這個問題。這個解決方案是最直觀的,但不是最好的。首先,每個類都應該只承擔一項責任。汽車,如果需要更改我們想要保存每個形狀的格式會發生什麼?如果是想相同的方法來生成XML,那麼是否必須更改爲JSON格式?這種設計絕對不遵循開放/閉合原則。
因此可以把這一組操作聚攏成一個訪問者,而操作的對象不需要發生變化,需要新的操作時,只需要添加一個新的訪問者即可。
- 對象結構穩定,但經常要在此對象結構上定義新的操作。
- 需要對一個對象結構中的對象進行很多並且不相關的操作,而需要避免這些操作”污染這些對象的類”,也不希望在增加新操作時修改這些類。
3 總結
本章討論了各種行爲型模式,我也快累死了,眼睛疼。之前較爲困惑的訪問者模式、備忘錄模式、中介者模式、狀態和策略模式、命令和解釋器模式也有了一定程度的理解。這些模式有助於我們以受控的方式來管理對象的行爲。等下週工作,如果有時間,就可以解決掉Task和EventResponse中那個冗長的switch-case了,使用策略+工廠的實現。
最後,也以p59頁的摘抄結束這個博客,“無論你遇見誰,他都是你生命中該出現的人,也一定會教會你一些什麼,永遠不要去責怪你生命中的任何人。”
2019-12-21 22:15週六於湖墅新村