命令模式是行爲型的設計模式,其核心思想很簡單:將一個請求封裝成一個對象,而且這個對象包含請求的全部信息(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
總之,能夠將命令模式當作一個客人在餐廳點餐的過程:設計模式
直接調用方法不就好了?爲何要將 request 封裝成 command 呢?由於直接調用 request 時須要知道 request 全部細節,當 request 較多時也難以管理,而抽象成 command 就能夠:網絡
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:¶meters 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 網絡庫使用的也是命令模式:spa
將 API 請求封裝成 YTKBaseRequest 的命令以後,Client 並不關心真正執行的是誰,目前是 AFNetworking,若是進行更換或者大版本升級,對 Client 沒有影響。另外,能夠對 Request 的 URL 進行 URL Filter,統一修改 URL 的某些值(例如 Common Arguments 或 Device),也能夠對多個 Request 進行管理(不管是 ViewController 仍是 YTKNetworkAgent)。設計
應用內報 Bug 支持當前屏幕截圖後進行繪製,而且繪製能夠 Undo 和 Redo,那就很是適合命令模式:將每次繪製的動做抽象成 Action,Action 中包含了這次繪製的 Path 和 Color(實際上是 CAShapeLayer),用兩個隊列分別存儲 Undo 和 Redo 的 Action:code
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 在處理 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 天然而然的能找到。使用命令模式的優勢是:
參考:
Article by Joe Shang