算法封装,通过封装和拓展对象的算法来改变对象的行为。
涉及到算法封装的有以下三种设计模式:
① 模板方法(封装共用行为)
② 策略(封装策略)
③ 命令(封装命令)
一. 模板方法
模板方法模式是面向对象设计中一种非常简单的设计模式(我们常用的基类就用到了这种模式)。其基本思想是在抽象类的一个方法中定义“标准”算法。在这个方法中调用的基本操作应由子类重载实现。这个方法称为“模板”,因为方法定义的算法缺少一些特有的操作(特有的操作需要子类去实现)。
AbstractClass不完整地定义一些方法与算法,留出一些操作未做定义。AbstractClass调用templateMethod时,方法中未定义的空白部分,由ConcreteClass重载primitiveOperation1(或2)来填补。模板方法中的控制结构流程是父类的模板方法调用其子类的操作,而不是子类调用父类的操作。
举个例子🌰:模板方法其实像一个“食谱”,它定义了做的步骤,具体步骤让子类(也就是事物自己决定)。以做三明治为例,AnySandwich是一个抽象类,相当于食谱,定义了制作三明治的一般流程make
,具体的三明治(比如鸡蛋火腿三明治)继承它的子类,会实现详细的步骤。
另外也可以在步骤中定义一个额外的步骤,让一些特殊的子类去实现。这就是我们所说的钩子,这个对子类来说是可选的。
- (void) make
{
[self prepareBread];
[self putBreadOnPlate];
[self addMeat];
[self addCondiments];
// 额外的步骤(为空),让一些特殊的子类去实现
[self extraStep];
[self serve];
}
在Cocoa Touch框架中,模板方法也是很常见。通过抽出共同行为放入框架类中,有助于提高可扩展性与可复用性,而维持各种类(框架类和用户类)之间的松耦合。比如UIView类中的定制绘图-(id)drawRect:(CGRect)rect
。这个方法默认实现什么也不做,如果UIView的子类需要绘制自己的视图,就重载这个方法(钩子方法)。
在以下情形,需要考虑模板方法:
- 需要一次性实现算法的不变部分,并将可变的行为留给子类来实现。
- 子类的共同行为应该被提取出来放在公共类中,以避免代码重复。现有代码的差别应被分离为新的操作。然后用一个调用这些新操作的模板方法来替代这些不同的代码。
- 需要控制子类的扩展。可以定义一个在特定点调用“钩子”操作的模板方法。子类可以通过对钩子操作的实现在这些点拓展功能。(比如在primitiveOperation1和primitiveOperation2插入一个额外方法)
二. 策略
模板方法是封装共有行为,策略模式是封装对象中的算法(算法简单讲就是一段逻辑)。如果if-else里面是一堆不同的算法,就可以考虑将这些算法疯转成一个对象。
策略模式中的关键角色是策略类,它为所有支持的算法声明了一个共同接口,我们使用策略接口来实现相关算法的具体策略类。场景(Context)类的对象配置一个具体策略对象的实例,场景对象使用策略接口调用具体策略类定义的算法。ConcreteStrategyA和B、C都是同一层次的,可以相互替换。Context可以使用相同的接口访问算法的各种变体。之前提过,装饰是改变对象的“外表”,策略的话则是改变对象的“内容”。
举个例子🌰:将UITextField的验证算法封装成策略类,让其能随时切换验证算法,也可以在多个UITextField时,让各个控件执行自己的验证算法。
另外,在MVC模式中,控制器决定视图对模型进行显示的时机和内容,视图本身知道如何绘图,但需要控制器告诉它要显示的内容。同一视图如果与不同的控制器合作,数据的输出格式可能一样,但数据的类型和格式可能随着不同控制器的不同输出而不同。所以,这边把控制器当做是视图的策略。
在以下情况,需要考虑策略模式:
- 一个类在其操作中使用多个条件语句来定义许多行为。我们可以把相关的条件分支移到它们自己的策略类中。(这个就只是算法把封装成类,没有context)
- 需要算法的各种变体。(封装算法,方便切换)
- 需要避免把复杂的、与算法相关的数据结构暴露给客户端。(抽离算法,免得类太复杂)
三. 命令
命令模式简单理解就是封装一个命令(命令其实也算是算法),封装的算法主要用于推迟命令对象的执行。把指令封装在各种命令对象中,命令对象可以被传递并且在指定时刻被不同的客户端复用。
命令对象封装了如何对目标执行指令的信息,因此客户端或调用者不必了解目标的任何细节,却仍可以对它执行任何已有的操作。通过把请求封装成对象,客户端可以把它参数化并置入队列或日志中,也能够支持可撤销的操作。命令对象将一个或多个动作绑定到特定的接收器。命令模式消除了作为对象的动作和执行它的接收器之间的绑定。
如下类图,Client(客户端)创建ConcreteCommand对象并设定其receiver(接收器),内部实际上是让receiver去执行某个动作。Invoker(祈求者)要求命令ConcreteCommand(继承于通用接口Command)执行,内部用数组保存命令,然后让命令执行,这样可以起到保存撤销的功能。所以,ConcreteCommand起Receiver和对它的操作action之间的中间人的作用。Receiver是可以随着ConcreteCommand对象实施的相应请求而执行实际操作的任何对象。
举个例子🌰:以通过按钮让View变暗变亮为例。
如果不用命令模式,其实也简单就写两个方法(变亮和变暗)。相较知晓,使用命令模式会多出好几个类,关系也会变得复杂。但是,有以下优点:
①分离代码,解耦Button和View(充当中间人,类似于中介者)。Button只管发出请求,View只管接收请求,它们的关系让命令来处理。
②可以支持撤销和恢复,因为Invoker(认为是命令管理类)保存管理着所有命令。
文件目录如下:
在ViewController中,只需要在根据不同Button的点击生成并执行不同命令,
- (void)buttonsEvent:(UIButton *)button {
if (button.tag == kAddButtonTag) {
// 生成命令
MakeDarkerCommand *darkerCommand = [[MakeDarkerCommand alloc] initWithReceiver:self.reciver parameter:0.1];
// 执行命令
[[Invoker sharedInstance] addAndExecute:darkerCommand];
} else if (button.tag == kDelButtonTag) {
// 生成命令
MakeLighterCommand *lighterCommand = [[MakeLighterCommand alloc] initWithReceiver:self.reciver parameter:0.1];
// 执行命令
[[Invoker sharedInstance] addAndExecute:lighterCommand];
}
}
两种具体的命令类(MakeDarkerCommand和MakeLighterCommand)都是有公共接口(遵守协议),类中也主要是和receiver相关的代码(绑定receiver,叫receiver做事情)。
@interface MakeLighterCommand ()
@property (nonatomic, weak) Receiver *receiver;
@property (nonatomic) CGFloat parameter;
@end
@implementation MakeLighterCommand
// 命令的初始化(会注入receiver)
- (id)initWithReceiver:(Receiver*)receiver parameter:(CGFloat)parameter {
self = [super init];
if (self) {
_receiver = receiver;
_parameter = parameter;
}
return self;
}
// 命令的执行(本质就是receiver的执行)
- (void)execute {
[self.receiver makeViewLighter:self.parameter];
}
@end
那Receiver又是怎么样的呢?Receiver主要是包含或者是需要执行动作的类,比如在这里,Receiver包含了需要执行命令的(也就是要改变明暗)View,以及让View变亮变暗的方法。
@property (nonatomic, weak) UIView *colorView;
/**
* 让接收指令的view颜色变淡
*
* @param quantity 数量
*/
- (void)makeViewLighter:(CGFloat)quantity;
/**
* 让接收指令的view颜色变身
*
* @param quantity 数量
*/
- (void)makeViewDarker:(CGFloat)quantity;
Command类的执行主要是在Invoker中,Invoker是一个命令管理类,撤销恢复命令都会在这边执行。
@interface Invoker ()
@property (nonatomic, strong) NSMutableArray *commandQueue;
@end
@implementation Invoker
+ (instancetype)sharedInstance {
static Invoker *sharedInstanceValue = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
sharedInstanceValue = [[Invoker alloc] init];
sharedInstanceValue.commandQueue = [NSMutableArray array];
});
return sharedInstanceValue;
}
- (void)addAndExecute:(id <CommandProtocol>)command {
// 添加并执行
[self.commandQueue addObject:command];
[command execute];
}
@end
在Cocoa Touch框架中,NSInvocation、NSUndoManager和“目标-动作”机制都是框架中对这个模式的典型应用。NSInvocation对象(类似ConcreteCommand)封装运行时库以向转发器转发执行消息所需要的所有必要信息,如目标对象、方法选择器和方法参数,通过NSInvocation实例去调用接收器的方法。
命令模式允许封装在命令对象中的可执行指令,使得实现撤销和恢复变得简单。另外,像开头所说的,命令模式还有一个令人熟知的应用时推迟调用器的执行(这个其实不太理解)。调用器可以是菜单项或按钮,使用命令对象连接不同对象之间的操作(比如点击按钮,执行命令对象,对视图控制器进行某种操作),命令对象隐藏了与这些操作有关的所有细节。另外,特定的命令需要修改或者添加新的命令类,也不会影响到其他组件。
在以下情况,需要考虑命令模式:
- 想让应用程序支持撤销与恢复。
- 想让对象参数化一个动作以执行操作,并用不同命令对象来代替回调函数。
- 想要在不同时刻对请求进行指定、排列和执行。(至少有个对象来管理请求)
- 想记录修改日志,这样再系统故障时,这些修改可在后来重做一遍。
- 想让系统支持事务,事务封装了对数据的一系列修改,事务可以建模为命令对象。