Design Pattern:命令模式

命令模式是行爲型的設計模式,其核心思想很簡單:將一個請求封裝成一個對象,而且這個對象包含請求的全部信息(turns a request into a stand-alone object that contains all information about the request) 怎麼理解呢?command 命令,這個單詞的英文解釋是 an authoritative direction or instruction to do something,而請求 request 能夠簡單理解成方法調用 to do something,所以,命令模式的核心就是將動詞 to do something 轉成了名詞 command,封裝命令類。git

看一下命令模式的類圖:github

  • Client:命令的發起人
  • Invoker:命令的執行者(responsible for initiating request),並持有命令,通常由 Client 傳入一個命令
  • Command:抽象類,描述了命令執行的接口(declares just a single method for executing the command)
  • Concrete Command:實現請求(implement various kinds of requests),調用 Receiver 執行
  • Receiver:命令真正的執行者,處理命令執行的相關業務邏輯(contains some business logic)

總之,能夠將命令模式當作一個客人在餐廳點餐的過程:設計模式

  1. 你告訴服務員要點的菜。
  2. 服務員將你點的訂單交給廚師。
  3. 廚師作好菜以後將菜交給服務員。
  4. 最後服務員把菜遞給你。 你的命令(訂單)經過調用者(服務員)交給了命令的執行者(廚師),至於這道菜是怎麼作的,誰作的,你並不知道也不用關心,只須要發出命令。而對於餐廳,廚師只須要將菜作好,不用關心是誰點的菜,若是某個廚師請假也能夠換一個廚師作菜。

直接調用方法不就好了?爲何要將 request 封裝成 command 呢?由於直接調用 request 時須要知道 request 全部細節,當 request 較多時也難以管理,而抽象成 command 就能夠:網絡

  • 可使用不一樣的請求把客戶端參數化(parameterize methods with different requests)
  • 能夠將請求排隊或者延時(delay or queue a request’s execution)
  • 能夠提供命令的撤銷和恢復功能(support undoable operations)

真實的例子

NSInvocation

An NSInvocation object contains all the elements of an Objective-C message: a target, a selector, arguments, and the return value. Each of these elements can be set directly, and the return value is set automatically when the NSInvocation object is dispatched.異步

NSInvocation 是 iOS 中的系統類,基於命令模式,將 Objective-C 消息的全部信息封裝到此類中,下面是使用 NSInvocation 的例子:函數

NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setArgument:&parameters atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
NSInteger result = 0;
[invocation getReturnValue:&result];
複製代碼

經過使用 NSInvocation,能夠將方法的調用者與執行者隔離開,進行解耦(例如 CTMediator 和 JSPatch 都使用 NSInvocation 調用方法)。除了解耦,因爲消息的信息都被封裝到 NSInvocation 中,所以能夠進行消息分發,例如能夠修改 target 從而將消息轉發給其餘 target 或者修改 selector 從而將消息轉發給其餘 implemention。ui

YTKNetwork

我司的 YTKNetwork 網絡庫使用的也是命令模式:spa

  • Client:ViewController
  • Invoker:YTKNetworkAgent
  • Command:YTKBaseRequest
  • Concrete Command:Custom Request
  • Receiver:AFNetworking

將 API 請求封裝成 YTKBaseRequest 的命令以後,Client 並不關心真正執行的是誰,目前是 AFNetworking,若是進行更換或者大版本升級,對 Client 沒有影響。另外,能夠對 Request 的 URL 進行 URL Filter,統一修改 URL 的某些值(例如 Common Arguments 或 Device),也能夠對多個 Request 進行管理(不管是 ViewController 仍是 YTKNetworkAgent)。設計

應用場景

繪畫模塊(Undo+Redo )

應用內報 Bug 支持當前屏幕截圖後進行繪製,而且繪製能夠 Undo 和 Redo,那就很是適合命令模式:將每次繪製的動做抽象成 Action,Action 中包含了這次繪製的 Path 和 Color(實際上是 CAShapeLayer),用兩個隊列分別存儲 Undo 和 Redo 的 Action:code

  • 每當 Undo 時,從 Undo 隊尾移除一項,Action 對應的 CAShapeLayer 從當前 Layer 中移除,並將此 Action 放入到 Redo 隊列中。
  • 每當 Redo 時,從 Redo 隊尾移除一項,將 Action 對應的 CAShapeLayer 加入到當前 Layer 中,並將 Action 放入到 Undo 隊列中。
  • 每當有新的繪製動做時,新建 Action,放入 Undo 隊列中,並將 Redo 隊列清空 。

如何讓彈框按順序彈出?

PM 曾提出需求,要求在啓動時的彈窗可以按照順序彈出,當一個彈窗關閉後再彈出一個,而不是一塊兒彈出。

[AlertUtils showAlertWithTitle:title message:message buttonCallback:^{
	// Do Something
}];
複製代碼

上面的代碼就是通常彈窗的使用方式,分析一下這個需求,問題核心是彈窗的結束是基於 UI 操做,是種異步操做,如何將異步的操做可以按照序列執行呢?注意,「方法 + 序列」是否是聽起來很熟悉?因此這個需求就能夠用命令模式來處理,將彈窗封裝成命令後在串行隊列中管理就好了。

具體作法是從 NSOperation 繼承一個 AlertOperation,在 runInMain 函數中執行的 AlertUtils 的 showAlert 操做,並在其 buttonCallback 中調用 NSOperation 的 finishOperation。而全部的 AlertOperation 都是在 Serial Operation Queue 中,當前一個 Operation 沒有變成 finished 時,後一個 Operation 是不會執行的,所以實現了 Alert 的按順序彈出。

重構 WebViewController

最先的 WebViewController 在處理 JS 回調的方法是用一堆 if/else if/else 語句:

- (void)jsCallback:(NSString *)name arguments:(NSDictionary *)arguments {
	if ([name isEqualToString:@「command1」]) {
		[self handleCommand1:name arguments:arguments];
	} else if ([name isEqualToString:@「command2」]) {
		[self handleCommand2:name arguments:arguments];
	} else if ([name isEqualToString:@「command3」]) {
		[self handleCommand3:name arguments:arguments];
	} else if ([name isEqualToString:@「command4」]) {
		[self handleCommand4:name arguments:arguments];
	}
}
複製代碼

這樣寫的問題是致使 WebViewController 愈來愈龐大,一堆業務邏輯耦合到 WebViewController 中(例如登陸通知,語音跟讀的回調等),維護性變差。另外,若是想配置 WebViewController 只支持某些或者不支持某些 JS 特定的回調的話,甚至根據頁面 URL 進行動態調整,也不是很乾淨。因而趁着 UIWebView 升級 WKWebView,作了一次重構:基於命令模式,將 JS 回調的處理抽離到一個個 Handler 中,JS 回調的名稱和參數也在 Handler 中維護,WebViewController 中再也不含有任何與 WebView 無關的業務邏輯,當 WebView 觸發了 JS 回調後,調用 Command Manager 這個 Invoker 去調用 Command。

- (void)registerCommands {
	[self.commandManager registerCommand:[Command1Handler new]];
	[self.commandManager registerCommand:[Command2Handler new]];
	[self.commandManager registerCommand:[Command3Handler new]];
	[self.commandManager registerCommand:[Command4Handler new]];
}

- (void)jsCallback:(NSString *)name arguments:(NSDictionary *)arguments {
	JSCommand *command = [JSCommand commandWithName:name arguments:arguments];
	[self.commandManager handleCommand:command];
}
複製代碼

總結

命令模式的核心在於將一個 to do something 的動做抽象成 command 對象,而不要太糾結於 Invoker 是誰,Client 在哪?一旦 command 接口抽象完,Client、Invoker、Receiver 天然而然的能找到。使用命令模式的優勢是:

  • 解耦:Client 與 Receiver 之間沒有任何依賴關係,調用者實現功能時只須要調用 Command 抽象類的 execute 方法便可,不須要知道究竟是哪一個接收者執行。
  • 可擴展性:Command 子類能夠很是容易的擴展,符合 SRP 和 OCP。

參考:

Article by Joe Shang

相關文章
相關標籤/搜索