1. 程式人生 > >請求傳送者與接收者解耦——命令模式(一)

請求傳送者與接收者解耦——命令模式(一)

  裝修新房的最後幾道工序之一是安裝插座和開關,通過開關可以控制一些電器的開啟和關閉,例如電燈或者排氣扇。在購買開關時,我們並不知道它將來到底用於控制什麼電器,也就是說,開關與電燈、排氣扇並無直接關係,一個開關在安裝之後可能用來控制電燈,也可能用來控制排氣扇或者其他電器裝置。開關與電器之間通過電線建立連線,如果開關開啟,則電線通電,電器工作;反之,開關關閉,電線斷電,電器停止工作。相同的開關可以通過不同的電線來控制不同的電器,如圖1所示:

1 開關與電燈、排氣扇示意圖

       在圖1中,我們可以將開關理解成一個請求的傳送者,使用者通過它來發送一個“開燈”請求,而電燈是“開燈”請求的最終接收者和處理者,在圖中,開關和電燈之間並不存在直接耦合關係,它們通過電線連線在一起,使用不同的電線可以連線不同的請求接收者,只需更換一根電線,相同的傳送者(開關)即可對應不同的接收者(電器)。

       在軟體開發中也存在很多與開關和電器類似的請求傳送者和接收者物件,例如一個按鈕,它可能是一個“關閉視窗”請求的傳送者,而按鈕點選事件處理類則是該請求的接收者。為了降低系統的耦合度,將請求的傳送者和接收者解耦,我們可以使用一種被稱之為命令模式的設計模式來設計系統,在命令模式中,傳送者與接收者之間引入了新的命令物件(類似圖1中的電線),將傳送者的請求封裝在命令物件中,再通過命令物件來呼叫接收者的方法。本章我們將學習用於將請求傳送者和接收者解耦的命令模式。

1 自定義功能鍵

       Sunny軟體公司開發人員為公司內部OA系統開發了一個桌面版應用程式,該應用程式為使用者提供了一系列自定義功能鍵,使用者可以通過這些功能鍵來實現一些快捷操作。Sunny

軟體公司開發人員通過分析,發現不同的使用者可能會有不同的使用習慣,在設定功能鍵的時候每個人都有自己的喜好,例如有的人喜歡將第一個功能鍵設定為“開啟幫助文件”,有的人則喜歡將該功能鍵設定為“最小化至托盤”,為了讓使用者能夠靈活地進行功能鍵的設定,開發人員提供了一個“功能鍵設定”視窗,該視窗介面如圖2所示:

“功能鍵設定”介面效果圖

       通過如圖2所示介面,使用者可以將功能鍵和相應功能繫結在一起,還可以根據需要來修改功能鍵的設定,而且系統在未來可能還會增加一些新的功能或功能鍵。

       Sunny軟體公司某開發人員欲使用如下程式碼來實現功能鍵與功能處理類之間的呼叫關係:

//FunctionButton:功能鍵類,請求傳送者
class FunctionButton {
	private HelpHandler help; //HelpHandler:幫助文件處理類,請求接收者
	
    //在FunctionButton的onClick()方法中呼叫HelpHandler的display()方法
public void onClick() {
		help = new HelpHandler();
		help.display(); //顯示幫助文件
	}
}

在上述程式碼中,功能鍵類FunctionButton充當請求的傳送者,幫助文件處理類HelpHandler充當請求的接收者,在傳送者FunctionButtononClick()方法中將呼叫接收者HelpHandlerdisplay()方法。顯然,如果使用上述程式碼,將給系統帶來如下幾個問題:

       (1) 由於請求傳送者和請求接收者之間存在方法的直接呼叫,耦合度很高,更換請求接收者必須修改傳送者的原始碼,如果需要將請求接收者HelpHandler改為WindowHanlder(視窗處理類),則需要修改FunctionButton的原始碼,違背了“開閉原則”。

       (2) FunctionButton類在設計和實現時功能已被固定,如果增加一個新的請求接收者,如果不修改原有的FunctionButton類,則必須增加一個新的與FunctionButton功能類似的類,這將導致系統中類的個數急劇增加。由於請求接收者HelpHandlerWindowHanlder等類之間可能不存在任何關係,它們沒有共同的抽象層,因此也很難依據“依賴倒轉原則”來設計FunctionButton

       (3) 使用者無法按照自己的需要來設定某個功能鍵的功能,一個功能鍵類的功能一旦固定,在不修改原始碼的情況下無法更換其功能,系統缺乏靈活性。

       不難得知,所有這些問題的產生都是因為請求傳送者FunctionButton類和請求接收者HelpHandlerWindowHanlder等類之間存在直接耦合關係,如何降低請求傳送者和接收者之間的耦合度,讓相同的傳送者可以對應不同的接收者?這是Sunny軟體公司開發人員在設計“功能鍵設定”模組時不得不考慮的問題。命令模式正為解決這類問題而誕生,此時,如果我們使用命令模式,可以在一定程度上解決上述問題(注:命令模式無法解決類的個數增加的問題),下面就讓我們正式進入命令模式的學習,看看命令模式到底如何實現請求傳送者和接收者解耦。

2 命令模式概述

       在軟體開發中,我們經常需要向某些物件傳送請求(呼叫其中的某個或某些方法),但是並不知道請求的接收者是誰,也不知道被請求的操作是哪個,此時,我們特別希望能夠以一種鬆耦合的方式來設計軟體,使得請求傳送者與請求接收者能夠消除彼此之間的耦合,讓物件之間的呼叫關係更加靈活,可以靈活地指定請求接收者以及被請求的操作。命令模式為此類問題提供了一個較為完美的解決方案。

       命令模式可以將請求傳送者和接收者完全解耦,傳送者與接收者之間沒有直接引用關係,傳送請求的物件只需要知道如何傳送請求,而不必知道如何完成請求

       命令模式定義如下:

命令模式(Command Pattern):將一個請求封裝為一個物件,從而讓我們可用不同的請求對客戶進行引數化;對請求排隊或者記錄請求日誌,以及支援可撤銷的操作。命令模式是一種物件行為型模式,其別名為動作(Action)模式或事務(Transaction)模式。

命令模式的定義比較複雜,提到了很多術語,例如“用不同的請求對客戶進行引數化”、“對請求排隊”,“記錄請求日誌”、“支援可撤銷操作”等,在後面我們將對這些術語進行一一講解。

       命令模式的核心在於引入了命令類,通過命令類來降低傳送者和接收者的耦合度,請求傳送者只需指定一個命令物件,再通過命令物件來呼叫請求接收者的處理方法,其結構如圖3所示:

圖3命令模式結構圖

       在命令模式結構圖中包含如下幾個角色:

       ● Command(抽象命令類):抽象命令類一般是一個抽象類或介面,在其中聲明瞭用於執行請求的execute()等方法,通過這些方法可以呼叫請求接收者的相關操作。

       ● ConcreteCommand(具體命令類):具體命令類是抽象命令類的子類,實現了在抽象命令類中宣告的方法,它對應具體的接收者物件,將接收者物件的動作繫結其中。在實現execute()方法時,將呼叫接收者物件的相關操作(Action)

       ● Invoker(呼叫者):呼叫者即請求傳送者,它通過命令物件來執行請求。一個呼叫者並不需要在設計時確定其接收者,因此它只與抽象命令類之間存在關聯關係。在程式執行時可以將一個具體命令物件注入其中,再呼叫具體命令物件的execute()方法,從而實現間接呼叫請求接收者的相關操作。

       ● Receiver(接收者):接收者執行與請求相關的操作,它具體實現對請求的業務處理。

       命令模式的本質是對請求進行封裝,一個請求對應於一個命令,將發出命令的責任和執行命令的責任分割開每一個命令都是一個操作:請求的一方發出請求要求執行一個操作;接收的一方收到請求,並執行相應的操作。命令模式允許請求的一方和接收的一方獨立開來,使得請求的一方不必知道接收請求的一方的介面,更不必知道請求如何被接收、操作是否被執行、何時被執行,以及是怎麼被執行的

       命令模式的關鍵在於引入了抽象命令類,請求傳送者針對抽象命令類程式設計,只有實現了抽象命令類的具體命令才與請求接收者相關聯。在最簡單的抽象命令類中只包含了一個抽象的execute()方法,每個具體命令類將一個Receiver型別的物件作為一個例項變數進行儲存,從而具體指定一個請求的接收者,不同的具體命令類提供了execute()方法的不同實現,並呼叫不同接收者的請求處理方法。

        典型的抽象命令類程式碼如下所示:

abstract class Command {
	public abstract void execute();
}

對於請求傳送者即呼叫者而言,將針對抽象命令類進行程式設計,可以通過構造注入或者設值注入的方式在執行時傳入具體命令類物件並在業務方法中呼叫命令物件的execute()方法,其典型程式碼如下所示:

class Invoker {
	private Command command;
	
    //構造注入
	public Invoker(Command command) {
		this.command = command;
	}
	
    //設值注入
	public void setCommand(Command command) {
		this.command = command;
	}
	
	//業務方法,用於呼叫命令類的execute()方法
	public void call() {
		command.execute();
	}
}

具體命令類繼承了抽象命令類,它與請求接收者相關聯,實現了在抽象命令類中宣告的execute()方法,並在實現時呼叫接收者的請求響應方法action(),其典型程式碼如下所示:

class ConcreteCommand extends Command {
	private Receiver receiver; //維持一個對請求接收者物件的引用

	public void execute() {
		receiver.action(); //呼叫請求接收者的業務處理方法action()
	}
}

       請求接收者Receiver類具體實現對請求的業務處理,它提供了action()方法,用於執行與請求相關的操作,其典型程式碼如下所示:

class Receiver {
	public void action() {
		//具體操作
	}
}

疑問

思考

一個請求傳送者能否對應多個請求接收者?如何實現?