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

相关文章
相关标签/搜索