设计模式 ─── 算法封装

算法封装,通过封装和拓展对象的算法来改变对象的行为。
涉及到算法封装的有以下三种设计模式:
① 模板方法(封装共用行为)
② 策略(封装策略)
③ 命令(封装命令)

一. 模板方法

模板方法模式是面向对象设计中一种非常简单的设计模式(我们常用的基类就用到了这种模式)。其基本思想是在抽象类的一个方法中定义“标准”算法。在这个方法中调用的基本操作应由子类重载实现。这个方法称为“模板”,因为方法定义的算法缺少一些特有的操作(特有的操作需要子类去实现)。
AbstractClass不完整地定义一些方法与算法,留出一些操作未做定义。AbstractClass调用templateMethod时,方法中未定义的空白部分,由ConcreteClass重载primitiveOperation1(或2)来填补。模板方法中的控制结构流程是父类的模板方法调用其子类的操作,而不是子类调用父类的操作。

模板方法.png

举个例子🌰:模板方法其实像一个“食谱”,它定义了做的步骤,具体步骤让子类(也就是事物自己决定)。以做三明治为例,AnySandwich是一个抽象类,相当于食谱,定义了制作三明治的一般流程make,具体的三明治(比如鸡蛋火腿三明治)继承它的子类,会实现详细的步骤。

三明治的制作.png

另外也可以在步骤中定义一个额外的步骤,让一些特殊的子类去实现。这就是我们所说的钩子,这个对子类来说是可选的。

- (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可以使用相同的接口访问算法的各种变体。之前提过,装饰是改变对象的“外表”,策略的话则是改变对象的“内容”。

策略模式.png

举个例子🌰:将UITextField的验证算法封装成策略类,让其能随时切换验证算法,也可以在多个UITextField时,让各个控件执行自己的验证算法。

UITextField的策略模式.png

另外,在MVC模式中,控制器决定视图对模型进行显示的时机和内容,视图本身知道如何绘图,但需要控制器告诉它要显示的内容。同一视图如果与不同的控制器合作,数据的输出格式可能一样,但数据的类型和格式可能随着不同控制器的不同输出而不同。所以,这边把控制器当做是视图的策略。

在以下情况,需要考虑策略模式:

  • 一个类在其操作中使用多个条件语句来定义许多行为。我们可以把相关的条件分支移到它们自己的策略类中。(这个就只是算法把封装成类,没有context)
  • 需要算法的各种变体。(封装算法,方便切换)
  • 需要避免把复杂的、与算法相关的数据结构暴露给客户端。(抽离算法,免得类太复杂)

三. 命令

命令模式简单理解就是封装一个命令(命令其实也算是算法),封装的算法主要用于推迟命令对象的执行。把指令封装在各种命令对象中,命令对象可以被传递并且在指定时刻被不同的客户端复用。
命令对象封装了如何对目标执行指令的信息,因此客户端或调用者不必了解目标的任何细节,却仍可以对它执行任何已有的操作。通过把请求封装成对象,客户端可以把它参数化并置入队列或日志中,也能够支持可撤销的操作。命令对象将一个或多个动作绑定到特定的接收器。命令模式消除了作为对象的动作和执行它的接收器之间的绑定。
如下类图,Client(客户端)创建ConcreteCommand对象并设定其receiver(接收器),内部实际上是让receiver去执行某个动作。Invoker(祈求者)要求命令ConcreteCommand(继承于通用接口Command)执行,内部用数组保存命令,然后让命令执行,这样可以起到保存撤销的功能。所以,ConcreteCommand起Receiver和对它的操作action之间的中间人的作用。Receiver是可以随着ConcreteCommand对象实施的相应请求而执行实际操作的任何对象。

命令模式.png

举个例子🌰:以通过按钮让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实例去调用接收器的方法。

NSInvocation.png

命令模式允许封装在命令对象中的可执行指令,使得实现撤销和恢复变得简单。另外,像开头所说的,命令模式还有一个令人熟知的应用时推迟调用器的执行(这个其实不太理解)。调用器可以是菜单项或按钮,使用命令对象连接不同对象之间的操作(比如点击按钮,执行命令对象,对视图控制器进行某种操作),命令对象隐藏了与这些操作有关的所有细节。另外,特定的命令需要修改或者添加新的命令类,也不会影响到其他组件。

在以下情况,需要考虑命令模式:

  • 想让应用程序支持撤销与恢复。
  • 想让对象参数化一个动作以执行操作,并用不同命令对象来代替回调函数。
  • 想要在不同时刻对请求进行指定、排列和执行。(至少有个对象来管理请求)
  • 想记录修改日志,这样再系统故障时,这些修改可在后来重做一遍。
  • 想让系统支持事务,事务封装了对数据的一系列修改,事务可以建模为命令对象。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容