前段時間,在自己糊里糊涂地寫了一年多的代碼之后,接手了一坨一個同事的代碼。身邊很多人包括我自己都在痛罵那些亂糟糟毫無設計可言的代碼,我不禁開始深思:自己真的比他高明很多嗎?我可以很自信地承認,在代碼風格和單元測試上可以完勝,可是設計模式呢?自己平時開始一個project的時候有認真考慮過設計模式嗎?答案是并沒有,我甚至都數不出有哪些設計模式。于是,我就拿起了這本設計模式黑皮書。
中文版《設計模式:可復用面向對象軟件的基礎》,譯自英文版《Design Patterns: Elements of Reusable Object-Oriented Software》。原書由Erich Gamma, Richard Helm, Ralph Johnson 和John Vlissides合著。這幾位作者常被稱為“Gang of Four”,即GoF。該書列舉了23種主要的設計模式,因此,其他地方經常提到的23種GoF設計模式,就是指本書中提到的這23種設計模式。
把書看完很容易,但是要理解透徹,融匯貫通很難,能夠在實際中靈活地選擇合適的設計模式運用起來就更是難上加難了。所以,我打算按照本書的組織結構(把23種設計模式分成三大類)寫三篇讀書筆記,一來自我總結,二來備忘供以后自己翻閱。與此同時,如果能讓讀者有一定的收獲就更棒了。我覺得本書的前言有句話很對,“第一次閱讀此書時你可能不會完全理解它,但不必著急,我們在起初編寫這本書時也沒有完全理解它們!請記住,這不是一本讀完一遍就可以束之高閣的書。我們希望你在軟件設計過程中反復參閱此書,以獲取設計靈感”。
本節將介紹行為模式,包括職責鏈模式、命令模式、解釋器模式、迭代器模式、中介者模式、備忘錄模式、觀察者模式、狀態模式、策略模式、模板方法模式和訪問者模式等十一種模式。行為模式涉及到算法和對象間職責的分配。行為模式不僅描述對象或類的模式,還描述它們之間的通信模式。這些模式刻畫了在運行時難以跟蹤的復雜的控制流,它們將你的注意力從控制流轉移到對象間的聯系方式上來。行為類模式使用繼承機制在類間分派行為,如模板方法和解釋器。行為對象模式則使用對象復合而不是繼承,一些行為對象模式描述了一組對等的對象怎樣相互協作以完成其中任一對象都無法單獨完成的任務,如職責鏈、中介者和觀察者。其他的行為對象模式常將行為封裝在一個對象中并將請求指派給它,如策略模式、命令模式、狀態模式、訪問者模式和迭代器模式。
1. 職責鏈模式(Chain of responsibility)
職責鏈模式,使多個對象都有機會處理請求,從而避免請求的發送者和接收者之間的耦合關系。將這些對象連成一條鏈,并沿著這條鏈傳遞該請求,直到有一個對象處理它為止。
動機:
考慮一個圖形用戶界面中的上下文有關的幫助機制。用戶在界面的任一部分上點擊都可以得到幫助信息,幫助信息依賴于點擊的界面上下文,如果該部分沒有幫助信息,則顯示一個關于當前上下文的比較一般的幫助信息。很顯然應該按照從特殊到最普遍的順序來組織幫助信息,而且提交幫助請求的對象并不明確知道誰是最終提供幫助的對象。職責鏈模式的想法是,給多個對象處理一個請求的機會,從而解耦合發送者和接受者。該請求沿對象鏈傳遞,鏈中收到請求的對象要么親自處理它,要么轉發給鏈中的下一個候選者,直至其中一個對象處理它。
UML結構圖:
代碼示例:
下面是動機那一節中提到的提供幫助信息的簡單示例。
public class HelpHandler
{
private HelpHandler successor;
public HelpHandler(HelpHandler helpHandler)
{
this.successor = helpHandler;
}
public virtual void HandleHelp()
{
if(successor != null)
{
successor->HandleHelp();
}
}
}
public class Widget : HelpHandler
{
private Widget parent;
public bool HasHelp()
{
//Check if we have help
}
public Widget(parent) : HelpHandler(parent)
{
}
}
public class Button : Widget
{
public Button(Widget button) : Widget(button)
{
}
public override void HandleHelp()
{
if(this.HasHelp())
{
//offer help on the button
} else {
this.HandleHelp();
}
}
}
適用情況:
(1) 有多個的對象可以處理一個請求,哪個對象處理該請求運行時刻自動確定;
(2) 你想在不明確指定接收者的情況下,向多個對象中的一個提交一個請求;
(3) 可處理一個請求的對象結合應被動態指定。
2. 命令模式(Command)
命令模式,將一個請求封裝為一個對象,從而使你可用不同的請求對客戶進行參數化。如,對請求進行排隊、記錄請求日志,以及支持可撤銷的操作。
動機:
有時必須向某對象提交請求,但并不知道關于被請求的操作或者請求的接受者的任何消息。例如,用戶界面工具箱包括按鈕和菜單這樣的對象,它們執行請求響應用戶輸入。但工具箱不能顯示地在按鈕或菜單中實現該請求,因為只有使用工具箱的應用知道該由哪個對象做哪個操作。而工具箱的設計者無法知道請求的接受者或執行的操作。命令模式通過將請求本身變成一個對象來使工具箱對象可向未指定的應用對象提出請求。這一模式的關鍵在于抽象的Command類,它定義了一個執行操作的接口,如Execute操作。
UML結構圖:
代碼示例:
下面是一個簡單的Command模式的示例,我們實現了開關的“從開到關”和“從關到開”轉換的command類。
public interface ICommand
{
void Execute();
}
public interface ISwitchable
{
void PowerOn();
void PowerOff();
}
public class CloseSwitchCommand : ICommand
{
private ISwitchable switchable;
public CloseSwitchCommand(ISwitchable switchable)
{
this.switchable = switchable;
}
public void Execute()
{
this.switchable.PowerOn();
}
}
public class OpenSwitchCommand : ICommand
{
private ISwitchable switchable;
public OpenSwitchCommand(ISwitchable switchable)
{
this.switchable = switchable;
}
public void Execute()
{
this.switchable.PowerOff();
}
}
public class Switch
{
ICommand closedCommand;
ICommand openedCommand;
public Switch(ICommand closedCommand, ICommand openedCommand)
{
this.closedCommand = closedCommand;
this.openedCommand = openedCommand;
}
public void Close()
{
this.closedCommand.Execute();
}
public void Open()
{
this.openedCommand.Execute();
}
}
適用情況:
(1) 抽象出待執行的動作以參數化某對象;
(2) 在不同的時刻指定、排列和執行請求;
(3) 支持取消操作;
(4) 支持修改日志,這樣系統崩潰時,這些修改可以被重做一遍;
(5) 用構建在原語操作上的高層操作構造一個系統。
3. 解釋器模式(Interpreter)
解釋器模式,給定一個語言,定義它的文法表示和一個解釋器,這個解釋器使用所定義的文法表示來解釋語言中的句子。
動機:
例如,對于搜索匹配一個模式的字符串問題,正則表達式是描述字符串模式的一種標準語言。與其為每一個模式都構造一個特定的算法,不如使用一種通用的搜索算法來解釋執行一個正則表達式,該正則表達式定義了待匹配字符串的集合。解釋器模式描述了如何為簡單的語言定義一個文法,如何在該語言中表示一個句子,以及如何解釋這些句子。
UML結構圖:
代碼示例:
下面是一個只包含TerminalExpression和NonterminalExpression的解釋器模式的簡單框架。
public class Context
{
}
abstract class AbstractExpression
{
public abstract void Interpret(Context context);
}
public class TerminalExpression : AbstractExpression
{
public override void Interpret(Context context)
{
// Interpret for terminal expression
}
}
public class NonterminalExpression : AbstractExpression
{
public override void Interpret(Context context)
{
// Interpret for nonterminal expression
}
}
適用情況:
當有一個語言需要解釋執行,并且你可將該語言中的句子表示為一個抽象語法樹時,可使用解釋器模式。
(1) 文法簡單。對于復雜的文法,文法的類層次變得龐大而無法管理;
(2) 效率不是一個關鍵問題。最高效的解釋器通常不是通過直接解釋語法分析樹實現的,而是首將它們轉換成另一種形式。
4. 迭代器模式(Iterator)
迭代器模式,提供一種方法順序訪問一個聚合對象中各個元素,而又不需暴露該對象的內部表示。
動機:
一個聚合對象,如列表,應該提供一種方法來讓別人可以訪問它的元素,而又不需要暴露它的內部結構。此外,針對不同的需要,可能要以不同的方式遍歷這個聚合對象。迭代器模式的關鍵思想是,將對列表的訪問和遍歷從列表對象中分離出來并放入一個迭代器對象中。迭代器類定義了一個訪問該列表元素的接口,迭代器對象負責跟蹤當前的元素。
UML結構圖
代碼示例:
下面一個簡單的例子實現了一個Iterator來訪問Item集合。
public class Item
{
private string name;
public Item(string name)
{
this.name = name;
}
public string Name => this.name;
}
public interface IAbstractCollection
{
Iterator CreateIterator();
}
public class Collection : IAbstractCollection
{
private ArrayList items = new ArrayList();
public Iterator CreateIterator()
{
return new Iterator(this);
}
public int Count => items.Count;
public object this[int index]
{
get {return items[index];}
set {items.Add(value);}
}
}
public interface IAbstractIterator
{
Item First();
Item Next();
bool IsDone {get;}
Item CurrentItem {get;}
}
public class Iterator : IAbstractIterator
{
private Collection collection;
private int current = 0;
public Iterator(Collection collection)
{
this.collection = collection;
}
public Item First()
{
current = 0;
return collection[current] as Item;
}
public Item Next()
{
++current;
if(IsDone)
return null;
else
return collection[current] as Item;
}
public Item CurrentItem
{
get {return collection[current] as Item;}
}
public bool IsDone
{
get {return current >= collection.Count;}
}
}
適用情況:
(1) 訪問一個聚合對象的內容而無需暴露它的內部表示;
(2) 支持對聚合對象的多種遍歷;
(3) 為遍歷不同的聚合結構提供一個統一的接口(即支持多態迭代)。
5. 中介者模式(Mediator)
中介者模式,用一個中介對象來封裝一系列的對象交互。中介者使各個對象不需要顯示地相互作用,從而使其耦合松散,而且可以獨立地改變它們之間的交互。
動機:
面向對象設計鼓勵將行為分布到各個對象中,這種分布可能會導致對象間有許多連接。大量的相互連接使得一個對象似乎不太可能在沒有其他對象的支持下工作。可以通過將集體行為封裝在一個單獨的中介者對象中避免這個問題。中介者負責控制和協調一組對象間的交互,中介者充當一個中介以使組中的對象不再相互顯示引用。這些對象僅知道中介者,從而減少了相互連接的數目。
UML結構圖:
代碼示例:
下面這個簡單的中介者的例子,所謂的中介者就是由它來與Component1和Component2來交互,使得它們之間是松耦合的。
public interface IComponent
{
void SetState(object state);
}
public class Component1 : IComponent
{
public void SetState(object state)
{
// Set state for component1
}
}
public class Component2 : IComponent
{
public void SetState(object state)
{
// Set state for component2
}
}
public class Mediator
{
public IComponent Component1 {get; set;}
public IComponent Component2 {get; set;}
public void ChangeState(object state)
{
this.Component1.SetState(state);
this.Component2.SetState(state);
}
}
適用情況:
(1) 一組對象以定義良好但是復雜的方式進行通信,產生的相互依賴關系結構混亂且難以理解;
(2) 一個對象引用其他很多對象并且直接與這些對象通信,導致難以復用該對象;
(3) 想定制一個分布在多個類中的行為,而又不想生成太多的子類。
6. 備忘錄模式(Memento)
備忘錄模式,在不破壞封裝性的前提下,捕獲一個對象的內部狀態,并在該對象之外保存這個狀態。
動機:
有時有必要記錄一個對象的內部狀態。為了允許用戶取消不確定的操作或從錯誤中恢復過來,需要實現檢查點和取消機制,而實現這些機制,你必須事先將狀態信息保存在某處,這樣才能將對象恢復到它們先前的狀態。但是對象通常封裝了其部分或所有的狀態信息,使得其狀態不能被其他對象訪問。備忘錄模式就派上用場了,一個備忘錄是一個對象,它存儲另一個對象在某個瞬間的內部狀態,而后者稱為備忘錄的原發器。當需要設置原發器的檢查點時,取消機制會向原發器請求一個備忘錄,原發器用掃描當前狀態的信息初始化該備忘錄。
UML結構圖:
代碼示例:
下面是一個簡單的Memento模式的例子,我們在Originator中有一個Memento對象,用來保存運行過程中Originator的state,這樣當它需要回退時可以取回之前保存在Memento中的state。
public class State
{
}
public class Memento
{
private State state;
public Memento(State state)
{
this.state = state;
}
public State GetState()
{
return this.state;
}
public void SetState(State state)
{
this.state = state;
}
}
public class Originator
{
private Memento memento;
private State currentState;
public Originator(State state)
{
this.currentState = state;
this.memento = new Memento(state);
}
public SetMemento(Memento memento)
{
memento.SetMemento(currentState);
}
public void Revert()
{
this.currentState = memento.GetState();
}
}
適用情況:
(1) 必須保存一個對象在某個時刻的(部分)狀態,這樣以后需要時它才能恢復到先前的狀態。
(2) 如果一個用接口來讓其他對象直接得到這狀態,將會暴露對象的實現細節并破壞對象的封裝性。
7. 觀察者模式(Observer)
觀察者模式,定義對象間的一種一對多的依賴關系,當一個對象的狀態發生改變時,所有依賴于它的對象都得到通知并被自動更新。
動機:
將一個系統分割成一系列相互協作的類有一個常見的副作用,需要維護相關對象間的一致性。觀察者模式描述了如何建立這種相關關系。這一模式的關鍵對象是目標和觀察者,一個目標可有任意數目的依賴它的觀察者。一旦目標的狀態發生改變,所有的觀察者都得到通知。
UML結構圖:
代碼示例:
下面是一個經典的關于觀察者模式的示例。我們有一個Heater用來加熱水,當水溫達到一定得溫度后會觸發BoilEvent,而BoilEvent上綁定為委托都會被執行。
public class Heater
{
private int temperature;
public delegate void BoilHandler(int param);
pubilc event BoilHandler BoilEvent;
public void BoilWater()
{
foreach(int t in Enumerable.Range(0, 101))
{
temperature = t;
if(temperature > 95)
{
if(BoilEvent != null)
{
BoilEvent(temperature);
}
}
}
}
}
public class Alarm
{
public void MakeAlert(int param)
{
Console.WriteLine($"Alarm : the temperature of water reached {param}");
}
}
public class Display()
{
public static void ShowMessage(int param)
{
Console.WriteLine($"Display: the temperature of water reached {param}");
}
}
Usage:
Heater heater = new Heater();
heater.BoilEvent += (new Alarm()).MakeAlert;
heater.BoilEvent += Display.ShowMessage;
heater.BoilWater();
適用情況:
(1) 當一個抽象模型有兩個方面,其中一個方面依賴于另一個方面。
(2) 當對一個對象的改變需要同時改變其他對象,而不知道具體有多少對象待改變。
(3) 當一個對象必須通知其他對象,而它又不能假定其他對象是誰。
8. 狀態模式(State)
狀態模式,允許一個對象在其內部狀態改變時改變它的行為。對象看起來似乎修改了它的類。
動機:
考慮一個表示網絡連接的類TCPConnection,一個它的對象的狀態可以處于Established、Listening或Closed。當一個TCPConnection對象接收到其他對象的請求時,它根據自身的當前狀態做出不同的反應。State模式描述了TCPConnection如何在每一種狀態下表現出不同的行為。這一模式的關鍵思想是引入一個稱為TCPState的抽象類來表示網絡的連接狀態,TCPState類為各個表示不同的操作狀態的子類聲明了一個公共的接口,而它的子類則會去實現與特定狀態相關的行為。
UML結構圖:
代碼示例:
下面是狀態模式的簡單示例。SateContext類中有一個對IState對象,動態執行時具體的State變化后相應的相應Request的方法也會變化。
public interface IState
{
void Handle()
{
}
}
public class ConcreteStateA : IState
{
public void Handle()
{
// Handle for ConcreteStateA
}
}
public class ConcreteStateB : IState
{
public void Handle()
{
// Handle for ConcreteStateB
}
}
public class StateContext
{
private IState state;
public StateContext(IState state)
{
this.state = state;
}
public void Request()
{
this.state.Handle();
}
}
適用情況:
(1) 一個對象的行為取決于它的狀態,并且它必須在運行時刻根據狀態改變它的行為;
(2) 一個操作中含有龐大的多分支的條件語句,且這些分支依賴于該對象的狀態。
9. 策略模式(Strategy)
策略模式,定義一系列的算法,把它們一個個封裝起來,并且使它們可以相互替換。本模式使得算法可獨立于使用它的客戶而變化。
動機:
有許多算法可對一個正文流進行分行,將這些算法硬編碼進使用它的類中是不可取的。因為這樣做不僅使得客戶程序變得復雜難以分割,而且可能還需要支持一些我們并不會使用的換行算法。因此,我們可以定義一些類來封裝不同的換行算法,從而避免這些問題。假設一個Composition類負責維護和更新一個正文瀏覽程序中顯示的正文換行。換行策略不是Composition類實現的,而是由抽象的Compositor類的子類(實現不用的換行策略)各自獨立地實現的,Composition類只維護一個對Compositor對象的引用。
UML結構圖:
代碼示例:
下面是一個簡單的策略模式示例。我們對value1和value2會根據相應的策略來執行加法或者減法。
public interface ICalculate
{
int Calculate(int value1, int value2);
}
public class Minus : ICalculate
{
public int Calculate(int value1, int value2)
{
return value1 - value2;
}
}
public class Plus : ICalculate
{
public int Calculate(int value1, int value2)
{
return value1 + value2;
}
}
public class CalculateClient
{
private ICalculate strategy;
public int Calculate(int value1, int value2)
{
return strategy.Calculate(value1, value2);
}
public void SetCalculate(ICalculate strategy)
{
this.strategy = strategy;
}
}
適用情況:
(1)許多相關的類僅僅是行為有異。策略提供了一種用多個行為中的一個行為來配置一個類的方法;
(2)需要使用一個算法的不同變體;
(3)算法使用客戶不應該知道的數據。這種情況下可使用策略模式以避免暴露復雜的、與算法無關的數據結構。
10. 模板方法模式(Template Method)
模板方法,定義一個操作中的算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟。
動機:
考慮一個提供Application和Document類的應用數據,Application類負責打開一個已有的外部形式存儲的文檔,如一個文件。一旦一個文檔中的信息從該文件中讀出后,它就由一個Document對象表示。用框架構建的應用可以通過繼承Application和Document來滿足特定的需求。例如,一個繪圖應用定義DrawApplication和DrawDocument子類;一個電子表格應用定義SpreadsheetApplication和SpreadSheetDocument子類。OpenDocument定義了一個打開文檔的每一個主要步驟(檢查該文檔是否打開,創建相應的Document對象,讀取等)。我們稱OpenDocument為一個模板方法,它用一些抽象的操作定義一個算法,而子類將重定義這些操作提供具體的行為。
UML結構圖:
代碼示例:
下面是動機這一節中介紹的例子的簡單實現。
abstract class Document
{
public abstract void DoRead();
}
abstract class Application
{
public abstract bool CanOpenDocument();
public abstract Document DoCreateDocument();
public void OpenDocument()
{
if(CanOpenDocument())
{
Document document = DoCreateDocument();
document.DoRead();
}
}
}
public class DrawApplication : Application
{
public override bool CanOpenDocument()
{
}
public override Document DoCreateDocument()
{
// Return DrawDocument
}
}
public class SpreadsheetApplication : Application
{
public override bool CanOpenDocument()
{
}
public override Document DoCreateDocument()
{
// Return SpreadsheetDocument
}
}
適用情況:
(1) 一次性實現一個算法的不變部分,并將可變的行為留給子類來實現;
(2) 各個類中公共的行為應被提取出來并集中到一個公共父類中以避免代碼重復;
(3) 控制子類擴展。
11. 訪問者模式(Visitor)
訪問者模式,表示一個作用于某對象結構中的各元素的操作。它使你可以在不改變各元素的類的前提下定義作用于這些元素的新操作。
動機:
考慮一個編譯器,它將源程序表示為一個抽象語法樹。該編譯器需在抽象語法樹上實施某些操作以進行“靜態語義”分析,也需要生成代碼。因此它可能要定義許多操作以進行類型檢查、代碼優化、流程分析,此外,還可使用抽象語法樹進行優美格式打印、程序重構等等。這些操作大多要求對不同的節點進行不同的處理。這里有兩個問題,一個是,將所有這些操作分散到各種結構點類中會導致整個系統難以理解、難以維護和修改。另一個是,增加新的操作通常需要重新編譯所有這些類。因此,我們可以將每個類中相關的操作包裝在一個獨立的對象(稱為Visitor)中,并在遍歷抽象語法樹時將此對象傳遞給當前訪問的元素。
UML結構圖:
代碼示例:
下面是一個visitor模式的簡單示例。我們用CarElement來接收一個visitor對象,在visitor類中對car的不同組成部分實現不同的visit方法。
public interface CarElementVisitor
{
void visit(Wheel wheel);
void visit(Engine engine);
void visit(Body body);
void visit(Car car);
}
public interface CarElement
{
void accept(CarElementVisitor visitor);
}
public class Wheel : CarElement
{
private string name;
public Wheel(string name)
{
this.name = name;
}
public void accept(CarElementVisitor visitor)
{
visitor.visit(this);
}
}
public class Engine : CarElement
{
public void accept(CarElementVisitor visitor)
{
visitor.visit(this);
}
}
public class Body : CarElement
{
public void accept(CarElementVisitor visitor)
{
visitor.visit(this);
}
}
public class Car : CarElement
{
private List<CarElement> elements;
public Car()
{
this.elements = new List<CarElement>()
{
};
}
public void accept(CarElementVisitor visitor)
{
foreach(CarElement element in elements)
{
element.accept(visitor);
}
visitor.visit(this);
}
}
public class CarElementPrintVisitor : CarElementVisitor
{
public void visit(Wheel wheel)
{
Console.WriteLine("Visiting wheel")
}
public void visit(Engine engine)
{
Console.WriteLine("Visiting engine")
}
public void visit(Body body)
{
Console.WriteLine("Visiting body")
}
public void visit(Car car)
{
Console.WriteLine("Visiting car")
}
}
適用情況:
(1) 一個對象結構包含很多類對象,它們有不同的接口,而你想對這些對象實施一些依賴其具體類的操作;
(2) 需要對一個對象結構中的對象進行很多不同的并且不相關的操作,而你想避免讓這些操作“污染”這些對象的類;
(3) 定義對象結構的類很少改變。但經常需要在此結構上定義新的操作,改變對象結構類需要重定義對所有訪問者的接口,這可能需要很大的代價。
參考文獻:
《設計模式:可復用面向對象軟件的基礎》
Chain-of-responsibility pattern
Command pattern
Iterator pattern
Mediator pattern
Memento pattern
Template method pattern
Visitor pattern
C# 中的委托和事件