《iOS设计模式解析》
设计模式是面向对象编程中的概念,是一个对象或类的设计模板,用于解决特定领域经常发生的问题。
根据1994年版的《设计模式》一书,设计模式是对定制来解决特定场景下一般设计问题的类和互相通信的对象的描述。
简而言之,设计模式是为特定场景下的问题而定制的解决方案。特定场景指问题所在的重复出现的场景。问题指特定环境下想达成的目标。同样的问题在不同的环境下会有不同的限制和挑战。定制的解决方案是指在特定环境下克服了问题的限制条件而达成目标的一种设计。
设计模式是经时间证明为有效的,对特定面向对象设计问题主要方面的一种抽象,体现了面向对象的重要思想。
本书涉及21种设计模式,根据其实际主题或应用领域分为以下8个功能部分:对象创建、接口适应、对象去耦、抽象集合、行为扩展、算法封装、性能与对象访问以及对象状态。
一、对象创建
1. 原型模式(Phototype)
1.1 定义
原型模式:使用原型实例制定创建对象的种类,并通过复制这个原型创建新的对象。
1.2 何时使用原型模式
在以下情形,会考虑使用原型模式
- 需要创见的对象应独立于其类型与创建方式;
- 需要实例化的类是在运行时决定的;
- 不想要与产品层次相对应的工厂层次;
- 不同类的实例间的差异仅是状态的若干组合。因此复制相应数量的原型比手工实例化更加方便;
- 类不容易创建,比如每个组件可把其他组件作为子节点的组合对象。复制已有的组合对象并对副本进行修改会更加容易。
此模式的最低限度是生成对象的真实副本,以用作同一环境下其他相关事务的基础(原型)。
1.3 应用举例
为Mark聚合体实现复制方法(遵守NSCopying协议,实现copy方法)
2. 工厂方法(Factory Method)
2.1 定义
工厂方法:定义创建对象的接口,让子类决定实例化哪一个类。工厂方法使得一个类的实例化延迟到子类。
2.2 何时使用工厂方法
在以下情形,自然会想到使用工厂方法模式:
- 编译时无法准确预期要创建的对象的类;
- 类想让其子类决定在运行时创建什么;
- 类有若干辅助类为其子类,而你想将返回哪个子类这一信息局部或。
使用这一模式的最低限度是,工厂方法能给予类在变更返回哪一种对象这一点上更多的灵活性。使用这一架构的一个常见例子是Cocoa Touch框架(或一般的Cocoa)中的NSNumber。尽管可以使用常见的alloc init两步法创建NSNumber实例,但这没什么用,除非使用预先定义的类工厂方法来创建有意义的实例。例如,[NSNumber numberWithBool:YES]消息会得到NSNumber的子类NSCFBoolean的一个实例,这个实例包含传给类工厂方法的布尔值。工厂方法模式对框架设计者特别有用。
2.3 应用举例
在TouchPainter生成不同画布,特定CanvasView的实例由在CanvasViewGenerator抽象类中定义的工厂方法canvasViewWithFrame:aFrame来创建。有两个CanvasViewGenerator的子类,各自通过重载父类的canvasViewWithFrame方法负责创建特定CanvasView的实例。
3. 抽象工厂(Abstract Factory)
3.1 定义
抽象工厂:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
3.2 何时使用抽象工厂
- 通过对象组合创建抽象产品
- 创建多系列产品
- 必须修改父类的接口才能支持新的产品
3.3 应用举例
在TouchPainter应用程序中,冠名合作公司的名称和标志。在加品牌的过程中,也有可能涉及其他UI元素,如让用户返回主应用程序的主画面按钮,以及画布上的工具条等多处变动需要加到框架中,此时需要用抽象工厂模式。如果有多个类共有相同的行为,但实际实现不同,则可能需要某种抽象类型作为其父类被继承。抽象类型定义所有相关具体类将共有的共同行为。
软件设计的黄金法则:变动需要抽象。
每个产品有自身的产品体系,并由各个具体工厂中的工厂方法支持。产品由两个不同的品牌工厂——SierraBrandingFactory和AcmeBrandingFactory来“生产”。它们各自重载抽象BrandingFactory类中定义的工厂方法brandedView、brandedMainButton和brandedToolbar,并根据工厂设计所针对的品牌返回具体产品。
超类的类方法factory是返回具体BrandingFactory的正确版本的工厂方法。其子类不应重载这个方法(尽管子类能够这么做)。factory方法根据当前的编译配置返回一个具体品牌工厂的实例。在BrandingFactory中的这些工厂方法(brandedView、brandedMainButton、brandedToolbar)的默认实现返回抽象产品UIView、UIButton和UIToolbar的实例,不带任何品牌细节。每个品牌所要求的一切细节将由实际的具体品牌工厂在其重载的工厂方法中生产。
4. 生成器模式(Builder)
4.1 定义
生成器模式:将一个复杂对象的构建与它的表现分离,使得同样的构建过程可以创建不同的表现。
4.2 何时使用生成器模式
在以下情形,自然会想到使用生成器模式。
- 需要创建涉及各种部件的复杂对象。创建对象的算法应该独立于部件的装配方式。常见例子是构建组合对象。
- 构建过程需要以不同的方式(例如,部件或表现的不同组合)构建对象。
4.3 应用举例
构建游戏中的角色,假定有两种类型的角色——敌人和游戏者。两种角色有一些共同的基本特征,如力量、耐力、智力、敏捷和攻击力。每一种特征都影响角色的防御与攻击能力。特征与防御或攻击因子成正比或反比关系。
此时,用CharacterBuilder类定义角色通用属性、设置特征方法,用StandardCharacterBuilder类继承Character定义特征属性与攻击、防御的计算关系,用ChasingGame中的两种方法设置各种特征值,最终生成两种角色。
5. 单例模式(Singleton)
5.1 定义
单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
5.2 何时使用生成器模式
在以下情形,应该考虑使用单例模式:
- 类只能有一个实例,而且必须从一个为人熟知的访问点对其进行访问,比如工厂方法。
- 这个唯一的实例只能通过子类化进行扩展,而且扩展的对象不会破坏客户端代码。
5.3 应用举例
UIApplication类、UIAccelerometer类、NSFileManager类
二、接口适配
6. 适配器模式(Adapter)
6.1 定义
适配器模式:将一个类的接口转换成客户希望的另外一个接口,适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
6.2 何时使用适配器模式
在以下情形,自然会想到使用这一模式。
- 已有类的接口与需求不匹配。
- 想要一个可复用的类,该类能够同可能带有不兼容接口的其他类协作。
- 需要适配一个类的几个不同子类,可是让每一个子类去子类化一个类适配器又不现实。那么可以使用对象适配器(也叫委托)来适配其父类的接口。
6.3 应用举例
在调色板与画布之间使用适配器模式实现颜色和线宽的适配。
7. 桥接(Bridge)
7.1 定义
桥接模式:将抽象部分与它的实现部分分离,使它们都可以独立地变化。
7.2 何时使用桥接模式
在以下情形,自然会想到使用这一模式:
- 不想在抽象与其实现之间形成固定的绑定关系(这样就能在运行时切换实现);
- 抽象及其实现都应可以通过子类化独立进行扩展;
- 对抽象的实现进行修改不应影响客户端代码;
- 如果每个实现需要额外的子类以细化抽象,则说明有必要把它们分成两个部分;
- 想在带有不同抽象接口的多个对象之间共享一个实现。
7.3 应用举例
创建iOS版虚拟仿真器,用桥接模式构建仿真器,让它支持Game Boy和Game Gear等多种便携游戏平台。
ConsoleController和ConsoleEmulator分别是虚拟控制器和虚拟仿真器的抽象类。两个类有不同的接口。在ConsoleController中封装一个对ConsoleEmulator的引用,是联系两者唯一方式。因此,ConsoleController的实例可以在一个抽象层次上使用ConsoleEmulator的实例。这就形成了两个不同的类ConsoleController与ConsoleEmulator之间的桥接。ConsoleEmulator为其子类定义了接口,用于处理针对特定控制台OS的底层指令。ConsoleController有个相对底层的方法,向桥接的另一端发送基本命令类型。ConsoleController的setCommand:command方法接受一个预先定义好的命令类型参数,并通过loadInstructionsForCommand:command消息把它传递给内嵌的ConsoleEmulator引用。最后,它向这个引用发送一个executeInstructions消息,在仿真器中执行任何已加载的指令。
8.外观(Facade)
8.1 定义
外观模式:为系统中的一组接口提供一个统一的接口,外观定义一个高层接口,让子系统更易于使用。
8.2 何时使用外观模式
在以下两种常见的情形下,会考虑使用这一模式。
- 子系统正逐渐变得复杂。应用模式的过程中演化出许多类。可以使用外观为这些子系统类提供一个较简单的接口。
- 可以使用外观对子系统进行分层。每个子系统级别有一个外观作为入口点。让它们通过其外观进行通信,可以简化它们的依赖关系。
8.3 应用举例
出租车的例子。
整个出租车服务作为一个封闭系统,包括一名出租车司机、一辆车和一台计价器。同系统交互的唯一途径是通过CabDriver中定义的接口driveToLocation:x。一旦乘客向出租车发出driveToLocation:x消息,CabDriver就会收到这个消息。司机需要操作两个子系统——Taximeter和Car。CabDriver先会启动(start)Taximeter,让它开始计价,然后司机对汽车会松刹车、换挡、踩油门,把车开走。直到到达了地点x,CabDriver会松油门、踩刹车、停止Taximeter,结束行程。一切都发生于发给CabDriver的一个简单的driveToLocation:x命令之中。无论这两个子系统有多么复杂,它们隐藏于乘客的实现之外。因此CabDriver是在为出租车子系统中的其他复杂接口提供一个简化的接口。CabDriver像“外观”一样,处于乘客与出租车子系统之间。
三、对象去耦
9. 中介者(Mediator)
9.1 定义
中介者模式:用一个对象来封装一系列对象的交互方式。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变他们之间的交互。
9.2 何时使用中介者模式
在以下情形,自然会考虑使用这一模式:
- 对象间的交互虽定义明确然而非常复杂,导致一组对象彼此互相依赖而且难以理解;
- 因为对象引用了许多其他对象并与其通讯,导致对象难以复用;
- 想要定制一个分布在多个类中的逻辑或行为,又不想生成太多子类。
9.3 应用举例
使用CoordinatingController集中管理UI,它的实例维护UI要素与视图控制器之间UI流程的逻辑,CoordinatingController引用了其他Controller,完成Controller之间的跳转。用户会点击按钮,触发视图迁移的请求,然后CoordinatingController将根据按钮的标签处理视图迁移。
10. 观察者(Observer)
10.1 定义
观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
10.2 何时使用观察者模式
在以下情形,自然会考虑使用这一模式。
- 有两种抽象类型相互依赖。将它们封装在各自的对象中,就可以对它们单独进行改变和复用。
- 对一个对象的改变需要同时改变其他对象,而不知道具体有多少对象有待改变。
- 一个对象必须通知其他对象,而它又不需知道其他对象是什么。
10.3 应用举例
观察者模式的实现方式有两种:通知(NSNotificationCenter)和键-值观察(KVO)
1. 通知(NSNotificationCenter)
对象在内部数据改变之后,能够把通知投递到通知中心,使消息能够广播给其他正在观察的对象,然后这些对象可作出适当的响应。对象可以像下面这样构造一个通知,然后投递到通知中心:
NSNotification *notification = [NSNotification notificationWithName:@"data changes" object:self];
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter postNotification:notification];
任何要订阅这个通知的对象,首先要为自己进行注册。如下面的代码段所示:
[notificationCenter addObserver:self
selector:@selector(update:)
name:@"data changes"
object:subject];
2. 键-值观察(KVO)
Cocoa提供了一种称为键-值观察的机制,对象可以通过它得到其他对象特定属性的变更通知。代码如下:
[scribble addObserver:self
forKeyPath:@"mark"
options:NSKeyValueObservingOptionInitial
context:nil];
观察者需要重载observeValueForKeyPath:ofObject:change:context:;以接收来自主题变更的回调并处理任何返回的值。
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([object isKindOfClass:[Scribble class]] && [keyPath isEqualToString:@"mark"])
{
id <Mark> mark = [change objectForKey:NSKeyValueChangeNewKey];
[canvasView_ setMark:mark];
[canvasView_ setNeedsDisplay];
}
}
用观察者实现TouchPainter中更新CanvasView上的线条。
四、抽象集合
11. 组合模式(Composite)
11.1 定义
组合模式:将对象组合成树形结构以表示“部分-整体”的层次结构。组合使得用户对单个对象和组合对象的使用具有一致性。
11.2 何时使用组合模式
在以下情形,自然会想到使用这一模式:
- 想获得对象抽象的树形表示(部分-整体层次结构);
- 想让客户端统一处理组合结构中的所有对象。
11.3 应用举例
在TouchPainter中,点、线是就是由遵守Mark协议的对象以组合的形式实现的。
在Cocoa Touch框架中的UIView就是以组合模式实现的。
12. 迭代器(Iterator)
12.1 定义
迭代器模式:提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。
12.2 何时使用迭代器模式
在以下情形,自然会想到使用迭代器模式:
- 需要访问组合对象的内容,而又不暴露其内部表示;
- 需要通过多种方式遍历对象;
- 需要提供一个统一的接口,用来遍历各种类型的组合对象。
12.3 应用举例
在Cocoa Touch框架中使用迭代器模式的案例
- NSEnumerator
使用NSEnumerator来枚举NSArray、NSDictionary和NSSet对象中的元素,一般通过objectEnumerator或keyEnumerator,来创建并返回响应的具体枚举器对象。客户端用返回的枚举器对象遍历集合中的元素,如下面的代码所示:
NSArray *anArray = ...;
NSEnumerator *itemEnumerator = [anArray objectEnumerator];
NSString *item;
while (item = [itemEnumerator nextObject])
{
// 对item作些处理
}
- 基于块的枚举
在iOS 4中,苹果公司在NSArray、NSDictionary和NSSet对象中引入了新方法,用于基于块的枚举。其中一个方法叫enumerateObjectsUsingBlock:(void (^) (id obj, NSUInteger idx, BOOL *stop))block。下面的代码段通过一个NSSArray对象演示了它是如何在代码中实现的。
NSArray *anArray = [NSArray arrayWithObjects@"This", @"is", @"a", @"test", nil];
NSString *string = @"test";
[anArray enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop)
{
if ([obj localizedCaseInsensitiveCompare:string] == NSOrderedSame)
{
// 对返回的obj做点别的事情
*stop = YES;
}
}];
- 快速枚举
for-in语法 - 内部枚举
NSSArray有个实例方法叫(void)makeObjectsPerformSelector:(SEL)aSelector,它允许客户端向数组中每个元素发送一个消息,让每个元素执行指定的aSelector。
五、行为扩展
13. 访问者(Visitor)
13.1 定义
访问者模式:表示一个作用于某对象结构中的各元素的操作,它让我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
13.2 何时使用访问者模式
在以下场景,可以使用访问者模式。
- 一个复杂的对象结构包含很多其他对象,它们有不同的接口(比如组合体),但是想对这些对象实施一些依赖于其具体类型的操作。
- 需要对一个组合结构中的对象进行很多不相关的操作,但是不想让这些操作“污染”这些对象的类。可以将相关的操作集中起来,定义在一个访问者中,并在需要在访问者中定义的操作时使用它。
- 定义复杂结构的类很少作修改,但经常需要向其添加新的操作。
13.3 应用举例
组合结构为各种叶节点和组合节点定义的上层抽象接口,声明了几个基本操作,任何一个叶节点或组合节点都可以执行这些操作。如果需要向组合体添加操作,则需要修改所有节点类的接口。如果经常这样,会造成巨大影响。因此更好的办法是向组合体使用访问者模式。访问者可以是相关操作的一个集合,这些操作可以通过组合体对象,根据每个节点的类型来执行。一旦对组合结构实现了访问者模式,通常就再也不需要修改组合体类的接口了。
对于组合结构来说,只需要声明并实现- (void)acceptMarkVisitor:(visitor *)visitor
方法,该方法的实现根据组合中类的不同可以是[visitor visitDot:self]
或[visitor visitVertex:self]
。- (void)visitDot:Dot
和- (void)visitVertex:Vertex
方法是由遵守Visitor协议的具体访问者对象实现的,通过该方法,将组合对象中的具体节点传递给visitor对象,以便visitor对象为组合对象的具体节点实现或扩展相应的功能。
在使用组合结构的类中,创建访问者对象,并为调用组合结构的- (void)acceptMarkVisitor:
方法。
在CanvasView中的示例代码如下
- (void)drawRect:(CGRect)rect
{
// 绘图代码
CGContextRef context = UIGraphicsGetCurrentContext();
// 创建renderer访问者
MarkRenderer *markRenderer = [[MarkRenderer alloc] initWithCGContext:context];
// 把renderer沿着mark组合结构传递
[_mark acceptMarkVisitor:markRenderer];
}
14. 装饰(Decorator)
14.1 定义
装饰模式:动态地给一个对象添加一些额外的职责,就扩展功能来说,装饰模式相比生成子类更为灵活。
14.2 何时使用装饰模式
在以下3种常见的情形下,应考虑使用这一模式。
- 想要在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责;
- 想要扩展一个类的行为,却做不到。类定义可能被隐藏,无法进行子类化,或者,对类的每个行为的扩展,为支持每种功能组合,将产生大量的子类。
- 对类的职责的扩展是可选的。
14.3 应用举例
为UIImage创建图像滤镜,有两种方式可以实现装饰模式:真正的子类和范畴。
- 通过真正的子类实现装饰
通过runtime中的消息转发实现,但是也绕不过范畴。 - 通过范畴实现装饰
在使用范畴的方式中,只需向UIImage类添加滤镜,构成范畴。
事实上,通过范畴对类进行扩展,本质上就是装饰模式。
15. 责任链(Chain of Responsibility)
15.1 定义
责任链模式:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间发生耦合。此模式将这些对象连成一条链,并沿着这条链传递请求。直到有一个对象处理它为止。
15.2 何时使用责任链模式
在以下情形,自然会考虑这一模式:
- 有多个对象可以处理请求,而处理程序只有在运行时才能确定;
- 向一组对象发出请求,而不想显式指定处理请求的特定处理程序。
15.3 应用举例
为RPG游戏中的人物实现各种防御道具。
假定我们要开发一个RPG游戏。里面的每个人物能够通过赚取点数来升级防御道具。防御道具可以是盾牌或盔甲。每种形式的防御只能应付一种特定的攻击。如果防御道具不认识一种进攻,它就把进攻的作用传给下一个会响应它的“实体”。
Avatar、MetalArmor和CrystalShield是AttackHandler的子类。AttackHandler定义了一个方法——handleAttack:attack,该方法的默认行为是,把攻击传给另一个AttackHandler的引用,即成员变量nextAttackHandler_。子类重载这个方法,对攻击提供实际的响应。如果AttackHandler不知道如何响应一个攻击,那么就使用[super handleAttack:attack]消息,把它转发给super,这样super中的默认实现就会把攻击沿着链传下去。
AttackHandler代码
AttackHandler.h代码段
#import "Attack.h"
@interface AttackHandler : NSObject
@property (nonatomic, strong) AttackHandler *nextAttackHandler;
- (void)handleAttack:(Attack *)attack;
@end
AttackHandler.m代码段
#import "AttackHandler.h"
@implementation AttackHandler
- (void)handleAttack:(Attack *)attack
{
[_nextAttackHandler handleAttack:attack];
}
Avatar的第一个防具MetalArmor,子类化AttackHandler并重载其handleAttack:方法,代码清单如下所示
MetalArmor代码
MetalArmor.h
#import "AttackHandler.h"
@interface MetalArmor : AttackHandler
// 重载的方法
- (void)handleAttack:(Attack *)attack;
@end
MetalArmor.m
#import "MetalArmor.h"
#import "SwordAttack.h"
@implementation MetalArmor
- (void)handleAttack:(Attack *)attack
{
if ([attack isKindOfClass:[SwordAttack class]])
{
// 攻击没有通过这个盔甲
NSLog(@"%@", @"No damage from a sword attack!");
}
else
{
NSLog(@"I don't know this attack:%@", [attack class]);
[super handleAttack:attack];
}
}
@end
类似的,CrystalShield也是一种AttackHandler类型。除了类名外,它的类声明几乎跟MetalArmor一样,这里就不重复了。
Avatar代码
Avatar.m
#import "Avatar.h"
@implementation Avatar
- (void)handleAttack:(Attack *)attack
{
// 当攻击到达这里时,我就被击中了
// 实际损伤的点数取决于攻击的类型
NSLog(@"Oh! I'm hit with a %@!", [attack class]);
}
@end
接下来,我们看一下如何应用责任链模式中的各项防具
管理人物和各种攻击的客户端代码
// 创建新的人物
AttackHandler *avatar = [[Avatar alloc] init];
// 让它穿上金属盔甲
AttackHandler *metalArmoredAvatar = [[MetalArmor alloc] init];
[metalArmoredAvatar setNextAttackHandler:avatar];
// 然后给金属盔甲中的人物增加一个水晶盾牌
AttackHandler *superAvatar = [[CrystalShield alloc] init];
[superAvatar setNextAttackHandler:metalArmor];
// 用剑攻击人物
Attack *swordAttack = [[SwordAttack alloc] init];
[superAvatar handleAttack:swordAttack];
// 用魔法火焰攻击人物
Attack *magicFireAttack = [[MagicFireAttack alloc] init];
[superAvatar handleAttack:magicFireAttack];
// 用闪电攻击人物
Attack *lightningAttack = [[LightningAttack alloc] init];
[superAvatar handleAttack:lightningAttack];
这个攻击处理程序有点像栈(即先进后出)。因为需要让Avatar是攻击的最后一站,所以它要最先创建。然后创建MetalArmor的实例,把Avatar作为它的下一个AttackHandler。它们被当做“增强”了的Avatar,MetalArmor是它通往真正的Avatar实例的第一道门。仅有MetalArmor还不够,还需要创建CrystalShield的实例,作为Avatar的另一种防御。我们使用MetalArmor形式的AttackHandler,作为CrystalShield实例的下一个攻击处理程序。此时,Avatar已是一个具有两种防御的“超级人物”。
六、算法封装
16. 模板方法(Template Method)
16.1定义
模板方法模式:定义一个操作中算法的骨架,而将一些步骤延迟到子类中。模板方法使子类可以重定义算法的某些特定步骤而不改变该方法的结构。
16.2 何时使用模板方法
在以下情形,应该考虑使用模板方法。
- 需要一次性实现算法的不变部分,并将可变的行为留给子类来实现。
- 子类的共同行为应该被提取出来放到公共类中,以避免代码重复。现有代码的差别应该被分离为新的操作。然后用一个调用这些新操作的模板方法来替换这些不同的代码。
- 需要控制子类的扩展。可以定义一个在特定点调用“钩子”(hook)操作的模板方法。子类可以通过对钩子操作的实现在这些点扩展功能。
钩子操作给出了默认行为,子类可对其扩展。默认行为通常什么都不做。子类可以重载这个方法,为模板算法提供附加的操作。
模板方法模式中的控制结构是倒转的,因为父类的模板方法调用其子类的操作,而不是子类调用父类的操作。这与“好莱坞法则”类似:别给我们打电话,我们会打给你。
模板方法会调用5种类型的操作:
- 对具体类或客户端类的具体操作;
- 对抽象类的具体操作;
- 抽象操作;
- 工厂方法(见第4章)
- 钩子操作(可选的抽象操作)
16.3 应用举例
制作三明治:
做简单的三明治的基本步骤应该像下面这样:
(1)准备面包(prepareBread);
(2)把面包放在盘子上(putBreadOnPlate);
(3)往面包上加肉(addMeat);
(4)加调味料(addCondiments);
(5)上餐(serve)。
可以定义一个叫make的模板方法,它调用上述各个步骤来制作真正的三明治。制作真正三明治的默认算法有些特定的操作没有实现,所以模板方法只是定义了制作三明治的一般方式。当具体的三明治子类重载了三明治的行为之后,客户端仅用make消息就能制作真正的三明治了。
抽象类AnySandwich.m代码如下:
#import "AnySandwich.h"
@implementation AnySandwich
- (void)make
{
[self prepareBread];
[self putBreadOnPlate];
[self addMeat];
[self addCondiments];
[self extraStep];
[self serve];
}
子类继承AnySandwich,并重载其他方法,并不重载-(void)make
方法。在重载完成后,调用- (void)make
方法,即可得到想要的定制化的结果。
extraStep
方法是为子类预留的额外附加步骤的方法,如果在标准步骤之外需要添加一些个性化的操作与设置,则可以重载该方法,并在其中实现。这个方法就是“钩子”
17. 策略(Strategy)
17.1 定义
策略模式:定义一系列算法,把它们一个个封装起来,并且使它们可相互替换,本模式使得算法可独立于使用它的客户而变化。
17.2 何时使用策略模式
在以下情形,自然会考虑使用这一模式。
- 一个类在其操作中使用多个条件语句来定义许多行为。我们可以把相关的条件分支移到它们自己的策略类中。
- 需要算法的各种变体。
- 需要避免把复杂的、与算法相关的数据结构暴露给客户端。
17.3 应用举例
在UITextField中应用验证策略
假设应用程序中需要有个UITextField以接受用户的输入,然后要在应用程序的处理中使用这个输入值。应用程序有个文字字段,只接受字幕,即a~z或A~Z,还有个字段只接受数值型的值,即0~9。为保证每个字段的输入有效,需要在用户结束文本框的编辑时做些验证。
可以把数据验证放到UITextField的委托方法textFieldDidEndEditing:之中。
如果不用策略模式,代码会写成清单中个样子。
- (void)textFieldDidEndEditing:(UITextField *)textField
{
if (textField == numericTextField)
{
// 验证 [textField text],保证其为数值型
}
else if (textField == alphaTextField)
{
// 验证[textField text],保证其值只包含字母
}
}
要是有更多不同类型的文本框,条件语句还会继续下去。如果能去掉这些条件语句,代码会更易管理,将来对代码的维护也会容易的多。
用策略模式实现上述需求
定义一个策略算法的抽象类InputValidator
其中包含一个方法- (BOOL)validateInput:(UITextField *)input error:(NSError **)error;
每个单独算法策略会继承InputValidator
类,并实现在- (BOOL)validateInput:(UITextField *)input error:(NSError **)error;
方法中用正则表达式实现它要验证的算法。
定义一个继承UITextField
类的CustomTextField
的类,CustomTextField
类有一个InputValidator
属性,用于绑定各种策略方法。该类也有一个- (BOOL)validate
方法,用于调用它的InputValidator
属性所对应的方法,进行算法验证。
CustomTextField
类代码如下:
CustomTextField.h
#import "InputValidator.h"
@interface CustomTextField : UITextField
@property (nonatomic, strong) InputValidator *inputValidator;
- (BOOL)validate;
@end
CustomTextField.m
#import "CustomTextField.h"
@implementation CustomTextField
- (BOOL)validate
{
NSError *error = nil;
// 调用inputValidator策略属性的验证方法进行验证,并将验证结果返回。
BOOL validationResult = [_inputValidator validateInput:self error:&error];
if (!validationResult)
{
UIAlertView *alertView = [[UIAlertView alloc]
initWithTitle:[error localizedDescription]
message:[error localizedFailureReason]
delegate:nil
cancelButtonTitle:NSLocalizedString(@"OK", @"")
otherButtonTitles:nil];
[alertView show];
}
return validationResult;
}
在StrategyViewController
类中创建两个CustomTextField
类的对象numericTextField
和alphaTextField
,分别将它们的InputValidator
属性设置为NumericInputValidator
类的对象和AlphaInputValidator
类的对象。
在StrategyViewController
类的- (void)textFieldDidEndEditing:(UITextField *)textField;
方法中调用CustomField
的- (void)validate
方法完成验证和策略调用。
StrategyViewController
类的代码如下
StrategyViewController.m:
@interface StrategyViewController()
@property (nonatomic, weak) IBOutlet CustomTextField *numericTextField;
@property (nonatomic, weak) IBOutlet CustomTextField *alphaTextField;
@end
@implementation StrategyViewController
- (void)textFieldDidEndEditing:(UITextField *)textField
{
if ([textField isKindOfClass:[CustomTextField class]])
{
[(CustomTextField *)textField validate];
}
}
@end
当一个文本框编辑结束,执行到textFieldDidEndEditing:时,它会检查textField是否为CustomTextField类。如果是,它就向textField发一条validate消息。根据不同的CustomTextField
类的对象绑定的InputValidate
属性,对文本输入框激活验证过程。可以看出,我们不再需要那些条件语句了。相反,我们使用一条简洁的多的语句,实现同样的数据验证。除了上面多了一条确保textField对象的类型是CustomField的额外检查之外,不应再有任何复杂的东西。
18. 命令(Command)
18.1 定义
命令模式:将请求封装为一个对象,从而可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。
18.2 何时使用命令模式
在下列情形,自然会考虑使用这一模式。
- 想让应用程序支持撤销与恢复;
- 想用对象参数化一个动作以执行操作,并用不同命令对象来代替回调函数;
- 想要在不同时刻对请求进行指定、排列和执行。
- 想记录修改日志,这样在系统故障时,这些修改可在后来重做一遍;
- 想让系统支持事务(transaction),事务封装了对数据的一系列修改。事务可以建模为命令对象。
命令模式允许封装在命令对象中的可执行指令。这使得在实现撤销和恢复基础设施的时候自然会想到这个模式。但这个模式的用途不只如此。命令对象的另一个为人熟知的应用是推迟调用器的执行。调用器可以是菜单项或按钮。使用命令对象连结不同对象之间的操作相当常见,比如,单击视图控制器中的按钮,可以执行一个命令对象,对另一个视图控制器进行某些操作。命令对象隐藏了与这些操作有关的所有细节。
18.3 应用举例
在Cocoa Touch框架中使用命令模式主要有NSInvocation对象和NSUndoManager对象。
在TouchPainter中实现撤销与恢复
- 使用NSUndoManager实现绘图与撤销绘图
(1)添加生成绘图与撤销绘图调用的方法
每次需要NSUndoManager注册撤销和恢复操作时,都需要一个新的NSInvocation对象。方便起见,我们添加一两个方法,生成NSInvocation对象的原型,我们就可以仅仅用某些Stroke和Dot对象修改几个参数,得到想要的NSInvocation对象。
生成NSInvocation对象的原型代码如下所示:
- (NSInvocation *)drawScribbleInvocation
{
NSMethodSignature *executeMethodSignature = [_scribble methodSignatureForSelector:@selector(addMark:shouldAddToPreviousMark:)];
NSInvocation *drawInvocation = [NSInvocation invocationWithMethodSignature:executeMethodSignature];
[drawInvocation setTarget:_scribble];
[drawInvocation setSelector:@selector(addMark:shouldAddToPreviousMark:)];
BOOL attachToPreviousMark = NO;
[drawInvocation setArgument:&attachToPreviousMark atIndex:3];
return drawInvocation;
}
- (NSInvocation *)undrawScribbleInvocation
{
NSMethodSignature *unexecuteMethodSignature = [_scribble methodSignatureForSelector:@selector(removeMark:)];
NSInvocation *undrawInvocation = [NSInvocation invocationWithMethodSignature:unexecuteMethodSignature];
[undrawInvocation setTarget:_scribble];
[undrawInvocation setSelector:@selector(removeMark:)];
return undrawInvocation;
}
在NSInvocation对象中收集的用户参数从序号2开始,因为第一个(序号0)是接收器,第二个是包含被调用选择器名字的_cmd。
(2)添加向NSUndoManager注册撤销与恢复的方法
我们介绍了如何生成用于线条与点的绘制与撤销的NSInvocation对象,但是还需要把它们注册到NSUndoManager,让这些调用能被撤销。向CanvasViewController的NSUndoManager注册撤销与恢复操作的方法的实现代码如下所示
- (void)executeInvocation:(NSInvocation *)invocation withUndoInvocation:(NSInvocation *)undoInvocation
{
[[self.undoManager prepareWithInvocaitonTarget:self] unexecuteInvocation:undoInvocation withRedoInvocation:invocation];
[invocation invoke];
}
- (void)unexecuteInvocation:(NSInvocation *)invocation WithRedoInvocation
{
[[self.undoManager prepareWithInvocationTarget:self] executeInvocation:redoInvocation withUndoInvocation:invocation];
[invocation invoke];
}
(3)为调用而修改触摸事件处理程序
现在要修改CanvasViewController中原来的触摸事件处理器,以创建调用对象,为撤销和恢复准备所有绘图动作。
对CanvasViewController中原来的触摸事件处理程序所作的修改,增加了NSUndoManager和基于NSInvocation的撤销和恢复操作
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
_startPoint = [[touches anObject] locationInView:_canvasView];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
CGPoint lastPoint = [[touches anyObject] previousLocationInView:_canvasView];
// 如果这是手指的拖动,就向涂鸦添加一个线条
if (CGPointEqualToPoint(lastPoint, _startPoint))
{
id <Mark> newStroke = [[Stroke alloc] init];
[newStroke setColor:_strokeColor];
[newStroke setSize:_strokeSize];
// 取得用于绘图的NSInvocation
// 并为绘图命令设置新的参数
NSInvocation *drawInvocation = [self drawScribbleInvocation];
[drawInvocation setArgument:&newStroke atIndex:2];
// 取得用于撤销绘图的NSInvocation,并未撤销命令设置新的参数
NSInvocation *undrawInvocation = [self undrawScribbleInvocation];
[undrawInvocation setArgument:&newStroke atIndex:2];
// 执行带有撤销命令的绘图命令
[self executeInvocation:drawInvocation withUndoInvocation:undrawInvocation];
}
// 把当前触摸作为顶点添加到临时线条
CGPoint thisPoint = [[touches anyObject] locationInView:_canvasView];
Vertex *vertex =[ [Vertex alloc] initWithLocation:thisPoint];
// 由于不需要撤销每个顶点,所以保留这条语句
[_scribble addMark:vertex shouldAddToPreviousMark:YES];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
CGPoint lastPoint = [[touches anyObject] previousLocationInView:_canvasView];
CGPoint thisPoint = [[touches anyObject] locationInView:_canvasView];
// 如果触摸从未移动(抬起之前一直在同一处)就向现有Stroke组合体添加一个点
// 否则就把它作为最后一个顶点添加到临时线条
if (CGPointEqualToPoint(lastPoint, thisPoint))
{
Dot *singleDot = [[Dot alloc] initWithLocation:thisPoint];
[singleDot setColor:_strokeColor];
[singleDot setSize:_strokeSize];
// 取得用于绘图的NSInvocation
// 并为绘图命令设置新的参数
NSInvocation *drawInvocation = [self drawScribbleInvocation];
[drawInvocation setArgument:&newStroke atIndex:2];
// 取得用于撤销绘图的NSInvocation,并未撤销命令设置新的参数
NSInvocation *undrawInvocation = [self undrawScribbleInvocation];
[undrawInvocation setArgument:&newStroke atIndex:2];
// 执行带有撤销命令的绘图命令
[self executeInvocation:drawInvocation withUndoInvocation:undrawInvocation];
}
// 在此重置起点
_startPoint = CGPointZero;
}
- 自制绘图与撤销绘图的基础设施
有一种较为简单而不易出错的方式。
我们使用两个栈,一个用于撤销,一个用于恢复,就像NSUndoManager那样。执行过的命令对象被压入撤销栈。栈顶的总是上一个执行过的命令。当应用程序需要撤销上一个操作的时候,它会从撤销栈弹出最后一个命令,然后执行撤销。完成撤销之后,应用程序会把这个命令压入恢复栈。所有命令对象都被撤销了之后,恢复栈就填满了所有的恢复命令对象。对于恢复也是类似的过程,只是方向相反。
命令对象只是从一个栈弹出压入另一个栈。
(1)命令基础设施的设计
首先,需要把“绘图”动作作成一个命令对象。它绘制某个图形之后,应用程序会把它压入撤销栈,等以后需要时就能撤销它所画的图形。我们把这个绘图用的命令叫做DrawScribbleCommand。
Command是DrawScribbleCommand的抽象父类。它声明了抽象操作execute
和undo
。Command还有一个具体属性userInfo,客户端可通过它向Command对象提供附加的参数。DrawScribbleCommand有一个Scribble对象的引用,可以向它添加或从它删除Mark对象。这里的CanvasViewController既是客户端又是调用者。
(2)命令类的实现
抽象Command类的代码如下所示
Command.h
@interface Command : NSObject
@proerpty (nonatomic, strong) NSDictionary *userInfo;
- (void)execute;
- (void)undo;
@end
Command.m
#import "Command.h"
@implementation
- (void)execute
{
// 应该抛出异常
}
- (void)undo
{
// 什么也不做
// 子类需要重载这个方法,执行实际的撤销
}
@end
Command的实现中没做什么事情,因为execute和undo方法都是抽象操作,其子类应该在其中加入一些动作。
DrawScribbleCommand类的代码实现
DrawScribbleCommand.h代码
#import "Command.h"
#import "Scribble.h"
@interface DrawScribbleCommand : Command
{
@private
Scribble *_scribble;
id <Mark> _mark;
BOOL _shouldAddToPreviousMark;
}
- (void)execute;
- (void)undo;
DrawScribbleCommand.m代码
#import "DrawScribbleCommand.h"
@implementation DrawScribbleCommand
- (void)exercute
{
if (!_userInfo) return;
_scribble = [_userInfo objectForKey:ScribbleObjectUserInfoKey];
_mark = [_userInfo objectForKey:MarkObjectUserInfoKey];
_shouldAddToPreviousMark = [(NSNumber *)[_userInfo objectForKey:AddToPreviousMarkUserInfoKey] boolValue];
[_scribble addMark:_mark shouldAddToPreviousMark:_shouldAddToPreviousMark];
}
- (void)undo
{
[_scribble removeMark:_mark];
}
重载的execute方法依赖userInfo属性中的信息。如果没有提供userInfo,方法就会退出。那么,究竟userInfo之中有什么对DrawScribbleCommand如此重要呢?userInfo字典含有3个关键元素,对于Scribble对象的使用至关重要。首先,是一个目标Scribble对象。没有它,其他的就都没用了。然后,是一个应该被加入到这个Scribble对象中的Mark实例。最后,是一个BOOL值,表示这个Mark实例应该如何附加到Scribble对象。
undo方法只是告诉保存的Scribble对象删除保存的Mark引用。
(3)为命令修改CanvasViewController
CanvasViewController中绘制涂鸦的命令对象的方法
- (void)executeCommand:(Command *)command prepareForUndo:(BOOL)prepareForUndo
{
if (prepareForUndo)
{
// 懒加载_undoStack
if (_undoStack == nil)
{
_undoStack = [[NSMutableArray alloc] initWithCapacity:_levelsOfUndo];
}
// 如果撤销栈满了,就丢掉栈底的元素
if ([_undoStack count] == _levelsOfUndo)
{
[_undoStack dropBottom];
}
// 把命令压入撤销栈
[_undoStack push:command];
}
// 调用命令对象的执行方法,把图像画上去
[command execute];
}
- (void)undoCommand
{
Command *command = [_undoStack pop];
// 调用命令对象的撤销方法,把图像删掉
[command undo];
// 把命令压入恢复栈
if (_redoStack == nil)
{
_redoStack = [[NSMutableArray alloc] initWithCapacity:_levelsOfUndo];
}
[_redoStack push:command];
}
- (void)redoCommand
{
Command *command = [_redoStack pop];
[command execute];
// 把命令压回到撤销栈
[_undoStack push:command];
}
// CanvasViewController中操作DrawScribbleCommand对象的触摸事件处理程序
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
_startPoint = [[touches anyObject] locationInView:_canvasView];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
CGPoint lastPoint = [[touches anyObject] previousLocationInView:_canvasView];
// 如果这是手指的拖动,就向涂鸦添加一条线
if (CGPointEqualToPoint(lastPoint, _startPoint))
{
id <Mark> newStroke = [[Stroke alloc] init];
[newStroke setColor:_strokeColor];
[newStroke setSize:_strokeSize];
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
_scribble, ScribbleObjectUserInfoKey,
newStroke, MarkObjectUserInfoKey,
[NSNumber numberWithBool:NO], AddToPreviousMarkUserInfoKey,nil];
DrawScribbleCommand *command = [[DrawScribbleCommand alloc] init];
[command setUserInfo:userInfo];
[self executeCommand:command prepareForUndo:YES];
}
// 把当前触摸作为顶点添加到临时线条
CGPoint thisPoint = [[touches anyObject] locationInView:_canvasView];
Vertex *vertex = [[Vertex alloc] initWithLocation:thisPoint];
[_scribble addMark:vertex shouldAddToPreviousMark:YES];
}
我们完成了在TouchPainter中实现撤销和恢复操作的例子,面向对象软件的设计之中经常能见到命令模式。
七、性能与对象访问
19. 享元(Flyweight)
19.1 定义
享元模式:运用共享技术有效地支持大量细粒度的对象。
19.2 何时使用享元模式
当满足以下所有条件时,自然会考虑使用这个模式:
- 应用程序使用很多对象;
- 在内存中保存对象会影响内存性能;
- 对象的多数特有状态(外在状态)可以放到外部而轻量化;
- 移除了外在状态之后,可以用较少的共享对象替代原来的那组对象;
- 应用程序不依赖于对象标识,因为共享对象不能提供唯一的标识。
19.3 应用举例
使用可共享的花朵池绘制几百个花朵图案,在屏幕上随机显示花朵图案。
我们的目标是用6个不同的实例,画很多随机尺寸和位置的花。如果为屏幕上所画的每朵花创建一个实例,程序会占用很多内存。我们的方案是使用享元模式来限制花朵实例的数量,让它不多于可选花朵类型的总数。
通过一个工厂模式,创建6类花朵的UIImageView,并且用一个NSMutableDictionary保存这6类花朵的UIImageView,当再次生成时,可以直接从NSMutableDictionary中取出对应的UIImageView,并将其加载到UIViewController中view的Array数组中,进行绘制。
20. 代理(Proxy)
20.1 定义
代理模式:为其他对象提供一种代理以控制对这个对象的访问。
有以下几种代理。
- 远程代理(remote proxy)为位于不同地址空间或网络上的对象提供本地代表。
- 虚拟代理(virtual proxy)根据需要创建重型对象。
- 保护代理(protection proxy)根据各种访问权限控制对原对象的访问。
- 智能引用代理(smart-reference proxy)通过对真正对象的引用进行计数来管理内存。也用于锁定真正对象,让其他对象不能对其进行修改。
代理模式的思想是使用一个基本上跟实体对象行为相同的代理。客户端可以“透明地”使用代理,即不必知悉所面对的只是一个代理而不是实体对象。当客户端请求某些创建的开销较大的功能时,代理将把请求转发给实体对象,准备好请求的功能并返回给客户端。客户端不知道幕后发生了什么。代理和实体对象同样拥有客户端要求的行为。
20.2 何时使用代理模式
在下列情形,自然会考虑使用这一模式。
- 需要一个远程代理,为位于不同地址空间或网络中的对象提供本地代表。
- 需要一个虚拟代理,来根据要求创建重型的对象。
- 需要一个保护代理,来根据不同访问权限控制对原对象的访问。
- 需要一个智能引用代理,通过对实体对象的引用进行计数来管理内存。也能用于锁定实体对象,让其他对象不能修改它。
20.3 应用举例
- 用虚拟代理懒加载图像
-
在Cocoa Touch框架中使用代理模式
Objective-C不支持多重继承。所以,如果代理对象不是Cocoa Touch框架中任何类的子类的话,可以考虑使用NSProxy作为占位或代替对象。
NSProxy是Cocoa框架中如NSObject一样的根类(root class)。NSProxy实现了NSObject协议,所以NSProxy对象实际上也是NSObject类型。NSProxy类是一个抽象基类,所以它没有自己的初始化方法。对NSProxy对象调用它不知如何响应的方法,将会抛出异常。
NSProxy的主要作用是,为其他对象(甚至还不存在的对象)的替身对象定义一个API。发给代理对象的消息会被转发给实体对象,或者,让代理加载实体对象或把代理自身变成实体对象。
NSProxy的存在只有一个目的——当代理。forwardInvocation:和methodSignatureForSelector:这两个实例方法对于整个代理过程至关重要(runtime的消息转发方法)。
当一个NSProxy子类的对象不能响应实体对象可能会有的方法时,Objective-C的运行库会向代理对象发送一个methodSignatureForSelector:消息,取得被转发的消息的正确方法签名。接下来,运行库会使用返回的方法签名,构造一个NSInvocation实例并使用forwardInvocation:消息把它发送给代理对象,让它把调用转发给其他对象。如果作为NSProxy子类的代理对象可以响应这个消息,那么forwardInvocaiton:方法就根本不会被调用。虽然Objective-C不支持多重继承,但是我们可以使用NSProxy的消息转发机制,来转发可由其他类的对象处理的任务,达成同样的目的。
八、对象状态
21. 备忘录(Memento)
21.1 定义
备忘录模式:在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
21.2 何时使用备忘录模式
当同时满足以下两个条件时,应当考虑使用这一模式:
- 需要保存一个对象(或某部分)在某一个时刻的状态,这样以后就可以恢复到先前的状态;
- 用于获取状态的接口会暴露实现的细节,需要将其隐藏起来。
21.3 应用举例
- NSKeyedUnarchiver实现备忘录模式
- 自己实现一套方法将对象数据转成NSData写入File中
- 实现对象的NSCopying协议中的
- (id)initWithCoder:(NSCoder *)coder
和- (void)encodeWithCoder:(NSCoder *)coder
方法。