设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的;设计模式使代码编制真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
包含如下设计模式的概念和实际使用:
- 原型模式
- 工厂方法模式
- 抽象工厂模式
- 生成器模式
- 单例模式
- 适配器模式
- 桥接模式
- 外观模式
- 中介者模式
- 观察者模式
- 组合模式
- 迭代器模式
- 访问者模式
- 装饰模式
- 责任链模式
- 模板方法模式
- 策略模式
- 命令模式
参考书籍《Objective-C 编程之道》
:Processon 绘制 UML
针对接口编程,而不是针对实现编程
- 只要对象符合客户端所要求的接口,客户端就不必在意所使用对象的确切类型。
- 客户端只知道定义接口的协议或者抽象类,因此客户端对对象的类一无所知。
定义具有相同接口的类群很重要,因为多态是基于接口的。Objective-C有一种类似的东西叫做协议,协议是对象之间的一种合约,但本身不能实例化对象。实现协议或者从抽象类继承,使得对象共享相同的接口。有几点好处:
协议并不定义任何实现,而只申明方法,以确定符合协议的类的行为。因此协议只定义了抽象行为的“接口”。实现协议的类定义这些方法的实现,以执行真正的操作。变更协议可能会破坏实现该协议的类,可以使用 @optional 指令,将协议的部分方法变更为“可选的”。
客户端使用由协议所定义类型的对象,有个 Mark 协议,可以这样引用
id <Mark> = thisMark;
抽象基类通过生成一些其他子类可以共享的默认行为,抽象基类与通常的类相似,只是预留了一些可以或应该由子类重写的行为。
如果Mark
被申明为抽象基类,语法是这样
Mark *thisMark;
对象创建
1. 原型模式:
1.1 原型模式是一种非常简单的设计模式,客户端知道抽象 Prototype 类。在运行时,抽象 Prototype 子类的任何对象都可以按客户端的意愿被复制,因此,无需手工创建就可以制造同一个类型的多个实例。
1.2 在Objective-C中这样实现,在Prototype类(基类)中定义复制自身的接口,在实际子类中(ConcretePrototype1、ConcretePrototype2...)实现该方法
// Prototype.h 文件
// 在抽象基类(Prototype)中定义复制自身的接口
// Subclassing
- (Prototype *)clone;
- (void)print;
// Prototype.m 文件
// 基类方法,什么也不做
- (Prototype *)clone {
Prototype *typeSelf = [[Prototype alloc] init];
return typeSelf;
}
- (void)print {
NSLog(@"This is = %@ class = [%@]",self,[self class]);
}
// 在ConcretePrototype1类中重写父类clone方法
- (Prototype *)clone {
ConcretePrototype1 *typeSelf = [[ConcretePrototype1 alloc] init];
return typeSelf;
}
// 在客户端这样创建对象
Prototype *type1 = [[ConcretePrototype1 alloc] init];
[type1 print];
Prototype *newType1 = [type1 clone];
[newType1 print];
1.3 何时实用原型模式:
- 需要创建的对象应独立于其他类型与创建方式
- 要实例化的类是在运行时决定的
- 不想要与产品层次相对应的工厂层次
- 不同类的实例间的差异仅是状态的若干组合。因此复制相应数量的原型比手工实例化更加方便
- 类不容易创建,比如每个组件可把其他组件作为子节点的组合对象,复制已有的组合对象并对副本进行修改会更加容易
2. 工厂方法
2.1 工厂方法模式:定义创建对象的接口,让子类决定实例化那一个类,工厂方法使得一个类的实例化延迟到其子类。
2.2 举个例子:对象工厂与生产有形产品的真实工厂类似,例如,制鞋厂生产鞋,手机工厂生产手机。比如你让工厂给你生产些产品。你给它们发送一个 “生产产品的消息”。制鞋厂和手机厂都按照相同的“生产产品”的协议,启动其生产线。过程结束后,每个厂家都返回所生产的特定类型的产品。我们把“生产”这个有魔力的词称作为工厂方法,因为它是命令生产者(工厂)得到想要产品的方法。
2.3 在Objective-C中可以这样写:抽象的 Product(产品)定义了工厂方法创建的对象的接口。ConcreteProduct 实现了 Product 接口。Creator 定义了返回 Product 对象的工厂方法。它也可以为工厂方法定义一个默认实现,返回默认 ConcreteProduct 对象。Creator 的其他操作可以调用此工厂方法创建 Product 对象。ConcreteCreator 是 Creator 的子类。 它重写了工厂方法,以返回 ConcreteProduct 的实例。
// 在Creator中定义一个接口
- (Product *)factoryMethod;
// 返回一个抽象产品
- (Product *)factoryMethod {
Product *product = [[Product alloc] init];
return product;
}
// 在ConcreteCreator中返回实际产品
- (Product *)factoryMethod {
ConcreteProduct *product = [[ConcreteProduct alloc] init];
return product;
}
// 在客户端这样创建产品
Product *product = [ConcreteCreator factoryMethod];
// 得到ConcreteCreator对象了
2.4 如下情形,会考虑使用工厂方法
- 编译时无法准确预期要创建的对象的类
- 类想让其子类决定在运行时创建什么
- 类有若干辅助类为其子类,而你想将返回哪个子类这一信息局部化
使用这一模式的一个常见例子是 Cocoa Touch 框架中的 NSNumber。尽管可以使用常见的 alloc init 两步创建 NSNumber 实例,但这没有什么用。除非使用预先定义的类工厂方法来创建有意义的实例。例如 [NSNumber numberWithBool:YES] 消息会得到 NSNumber 的子类 NSCFBoolean 的一个实例。
3. 抽象工厂
- 通过对象组合创建抽象产品
- 创建多系列产品
- 必须修改父类的接口才能支持新的产品
3.1 抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类或其创建的细节。客户端与从工厂得到的具体对象之间没有耦合
4. 生成器
4.1 生成器模式:将一个复杂对象的构建与它的表现分离,使得同样的构建过程可以创建不同的表现
4.2 Build 是一个抽象接口,申明了一个 buildPart 方法,该 builder 方法由 ConcreteBuilder 实现,以构造实际产品(Product)。ConcreteBuilder 有个 getResult 方法,向客户端返回构造完毕的 Product,Director 定义了一个 construct 方法,命令 Builder 的实例去 buildPart。 Director和Builder形成一种聚合关系。这意味着 Builder 是一个组成部分,与 Director 结合。以使整个模式运转,但同时,Director 并不负责 Builder 的生存期。
4.3 何时实用生成器模式:
- 构建复杂对象
- 以多个步骤构建对象
- 以多种方式构建对象
- 在构建过程的最后一步返回产品
- 专注一个特定产品
5. 单例
5.1 单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。也是最简单的一种模式。
5.2 在Objective-C中可以通过GCD的方式来实现
+ (Singleton *)sharedInstance {
static Singleton *_instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 默认实现
_instance = [[Singleton alloc] initWithSingleton];
// 支持子类化
_instance = [[super allocWithZone:NULL] initWithSingleton];
});
return _instance;
}
5.3 何时使用单例模式:
- 访问共享对象,比如:文件系统、控制类、管理类、配置信息类。在Cocoa Touch中,
UIApplication
通过单例类设计
接口适配
6. 适配器
6.1 适配器模式:将一个类的接口转换成客户端希望的另外一个接口。适配器模式使得由于接口不兼容而不能在一起工作的那些类可以一起工作。用于连接两种不同种类的对象,使其毫无问题的协同工作,有时称为包装器。包括类适配器和对象适配器。
6.2 包含:类适配器和对象适配器。代理(Delegate)模式属于对象适配器
类适配器
- 只针对单一具体的 Adaptee 类,把 Adaptee 适配到 Target
- 易于重载 Adaptee 的行为,因为是通过直接的子类化进行的适配
- 只有一个 Adapter 对象,无须额外的指针间接访问 Adaptee
对象适配器
- 可以适配多个 Adaptee 及其子类
- 难以重载 Adaptee 的行为,需要借助于子类的对象而不是 Adaptee 本身
- 需要额外的指针以间接访问 Adaptee 并适配其行为
6.3 使用场景
- 已有类的接口与需求不匹配。
- 想要一个可复用的类,该类能够同可能带有不兼容接口的其他类协作。
- 需要适配一个类的几个不同子类,可是让每一个子类去子类化一个类适配器又不现实,那么可以使用对象适配器(也叫委托)来适配其父类的接口。
6.4 用Objective-C的块实现适配器模式。在我们的项目中,有一个视图,它允许用户改变在CanvasView中绘图的颜色和线宽。视图中有3个滑动条,用于改变线色的颜色份量。这一操作涉及几个组件---CanvasViewController、PaletteViewController及其滑动条。我们需要一个更好的方案,让我们能够把颜色变更的部分复用到应用程序的其他地方。
从图中可看到,定义了一个叫RGBValuesProvider的块,签名为CGFloat ^(CGFloat *red, CGFloat *green, CGFloat *blue)。块字面量应该符合块签名,并提供某些处理的实现。
Command.h 代码清单
@interface Command : NSObject
{
@protected
// 其他一些私有成员变量
}
// 其他属性...
- (void)execute;
@end
SetStrokeColorCommand.h 代码清单
#import "Command.h"
typedef void(^RGBValuesProvider)(CGFloat *red, CGFloat *green, CGFloat *blue);
@interface SetStrokeColorCommand : Command
@property (nonatomic, copy) RGBValuesProvider valuesProvider;
- (void)execute;
@end
SetStrokeColorCommand.m 代码清单
- (void)execute {
CGFloat redValue = 0.0;
CGFloat greenValue = 0.0;
CGFloat blueValue = 0.0;
// 从块中取得 RGB 值
if (_valuesProvider != nil) {
_valuesProvider(&redValue,&greenValue,&blueValue);
}
// 根据 RGB 值创建一个颜色对象
UIColor *color = [UIColor colorWithRed:redValue green:greenValue blue:blueValue alpha:1.0];
// 把它赋值给当前 canvasViewController
CanvasViewController *vc = [[CoordinatingController sharedInstance] canvasVC];
vc.view.backgroundColor = color;
}
CommandSlider.h 代码清单
#import "Command.h"
@interface CommandSlider : UISlider
{
@protected
Command *_command;
}
@property (nonatomic, strong) Command *command;
@end
PaletteViewController.m 中的 viewDidLoad方法
SetStrokeColorCommand *colorCommand = [[SetStrokeColorCommand alloc] init];
colorCommand.valuesProvider = ^(CGFloat *red, CGFloat *green, CGFloat *blue) {
*red = [_redSlider value];
*green = [_greenSlider value];
*blue = [_blueSlider value];
};
PaletteViewController.m 用于所有CommandSlider实例的valueChange:事件处理器
#pragma mark -
#pragma mark Slider evnet handler
- (void)updateColor:(CommandSlider *)slider {
[[slider command] execute];
}
7. 桥接
7.1 桥接模式的目的是把抽象层次结构从其实现中分离出来,使其能够独立变更。抽象层定义了供客户端使用的上层的抽象接口。实现类的引用被封装于抽象层的实例中时,桥接就形成了。
7.2 Abstraction是定义了供客户端使用的上层抽象接口的父接口。它有一个对Implementor实例的引用,Implement定义了实现类的接口。这个接口不必跟Abstraction的接口一致。Implement的接口提供基本的操作,而Abstraction的上层操作基于这些基本操作。当客户端向Abstraction的实例发送operation消息时,这个方法向imp发送operationImp消息。底下的实际ConcreteImplementator(A或B)将做出相应并接受任务。
7.3 因此想要往系统中添加新的ConcreteImplementator时,所要做的只是为Implementor创建一个新的实现类,相应operationImp消息并在其中执行任何具体的操作。不过,这对Abstraction方面不会有任何影响。同样,如果想修改Abstraction的接口或者创建更细化的Abstraction类,也能做到不影响桥接的另一头。
7.4 何时实用桥接模式
- 不想在抽象与其实现之间形成固定的绑定关系(这样就能在运行时切换实现)
- 抽象及其实现都应可以通过子类化独立进行扩展
- 对抽象的实现进行修改不应影响客户端代码
- 如果每个实现需要额外的子类以细化抽象,则说明有必要把它们分成两个部分
- 想在带有不同抽象接口的多个对象之间共享一个实现。
7.5 创建iOS版虚拟仿真器
7.6 ConsoleController和ConsoleEmulator分别是虚拟控制器和仿真器的抽象类。两个类有不同的接口。在ConsoleController中封装一个对ConsoleEmulator的引用,是联系两者的唯一方式。因此ConsoleController的实例可以在一个抽象层上使用ConsoleEmulator的实例。这就形成了两个不同的类ConsoleController和ConsoleEmulator之间的桥接。ConsoleEmulator为其子类定义了接口,用于处理针对特定控制台OS的底层指令。ConsoleController的setCommand:command消息把它传递给内嵌的ConsoleEmulator引用。最后,它向这个引用发送一个executeInstructions消息,在仿真器中执行任何已加载的指令。
7.7 ConsoleController类层次结构代表对ConsoleEmulator类层次结构的任何“实现”的一种抽象。抽象类层次结构提供了一层抽象,形成一个对任何兼容ConsoleEmulator的虚拟控制层。具体的ConsoleController只能通过在父类中定义的底层setCommand:方法,与桥接的另一端的仿真器进行交流。在这种配置中这个方法不应被子类重载,因为这是一个让父类与细化的控制器之间进行通讯的接口。如果在仿真器的一侧发生变更,对左侧的任何控制器都将毫无影响,反之亦然。
代码清单 ConsoleCommands.h
typedef enum {
kConsoleCommandUp,
kConsoleCommandDown,
kConsoleCommandLeft,
kConsoleCommandRight,
kConsoleCommandSelect,
kConsoleCommandStart,
kConsoleCommandAction,
kConsoleCommandAction2,
} ConsoleCommand;
上、下、左、右、选择、开始、动作1和动作2作为通用命令,定义一组enum。要是将来想扩展命令列表,以支持更复杂的模拟器,都不会破坏任何一边的设计。
代码清单 ConsoleEmulator.h
#import "ConsoleCommands.h"
@interface ConsoleEmulator : NSObject
- (void)loadInstructionsForCommand:(ConsoleCommand)command;
- (void)executeInstructions;
// 其他行为和属性
@end
代码清单 GameBoyEmulator.h
@interface GameBoyEmulator : ConsoleEmulator
// 从抽象类重载的行为
- (void)loadInstructionsForCommand:(ConsoleCommand)command;
- (void)executeInstructions;
@end
GameBoyEmulator和GameGearEmulator都是ConsoleEmulator的子类。他们重载抽象方法,并提供自己平台的特定行为。
代码清单 ConsoleController.h
#import "ConsoleEmulator.h"
@interface ConsoleController : NSObject
@property (nonatomic, strong) ConsoleEmulator *emulator;
- (void)setCommand:(ConsoleCommand)command;
// 其他行为和属性
@end
ConsoleController是整个虚拟控制器类层次的起点,它以emulator_保持着ConsoleEmulator实例的一个内部引用。它也定义了一个setCommand:command方法,供其子类用预先定义的命令类型输入命令。
代码清单 ConsoleController.m
- (void)setCommand:(ConsoleCommand)command {
[_emulator loadInstructionsForCommand:command];
[_emulator executeInstructions];
}
到此,虚拟控制器与仿真器的基本桥接就完成了。现在要开始创建第一个虚拟控制器TouchConsoleController,以形成多点触摸屏与隐藏在视图之后具体仿真器之间的接口。
代码清单 TouchConsoleController.h
@interface TouchConsoleController : ConsoleController
- (void)up;
- (void)down;
- (void)left;
- (void)right;
- (void)select;
- (void)start;
- (void)action1;
- (void)action2;
@end
代码清单 TouchConsoleController.m
@implementation TouchConsoleController
- (void)up {
[super setCommand:kConsoleCommandUp];
}
- (void)down {
[super setCommand:kConsoleCommandDown];
}
- (void)left {
[super setCommand:kConsoleCommandLeft];
}
- (void)right {
[super setCommand:kConsoleCommandRight];
}
- (void)select {
[super setCommand:kConsoleCommandSelect];
}
- (void)start {
[super setCommand:kConsoleCommandStart];
}
- (void)action1 {
[super setCommand:kConsoleCommandAction];
}
- (void)action2 {
[super setCommand:kConsoleCommandAction2];
}
@end
客户端代码 viewDidLoad方法
// ConsoleEmulator *emulator = [[GameBoyEmulator alloc] init];
ConsoleEmulator *emulator = [[GameGearEmulator alloc] init];
TouchConsoleController *control = [[TouchConsoleController alloc] init];
control.emulator = emulator;
[control up]; // left、right、action1.....等命令
8. 外观
8.1 外观模式:为子系统中一组不同的接口提供统一的接口。外观定义了上层接口,通过降低复杂度和隐藏子系统间的通信及依存关系,让子系统更易于使用。
8.2 可以看到整个出租车服务作为一个封闭系统,包括出租车司机、一辆车和一台计价器。同系统交互的唯一途径是通过CabDriver中定义的接口driveToLocation:x。一旦乘客向出租车司机发出driveToLocation:消息,CabDriver就会收到这个消息。司机需要操作两个子系统---Taximeter和Car。CabDriver先会启动(start)Taximeter,让它开始计价,然后司机对汽车会松刹车(releaseBrakes)、换挡(changeGears)...停止(stop),Taximeter,结束行程。一切都发生于发送CabDriver的一个简单的driveToLocation:x命令之中。无论这两个子系统有多么复杂,他们隐藏于乘客的视线之外。因此CabDriver是在为出租车子系统的其他复杂接口提供一个简化的接口。
代码清单 Car.h
@interface Car : NSObject
- (void)releaseBrakes;
- (void)changeGears;
- (void)pressAccelerator;
- (void)pressBrakes;
- (void)releaseAccelerator;
- (void)stop;
@end
代码清单 Taximeter.h
@interface Taximeter : NSObject
- (void)start;
- (void)stop;
@end
代码清单 CabDriver.h
@interface CabDriver : NSObject
- (void)driveToLocation:(CGPoint)x;
@end
代码清单 CabDriver.m
- (void)driveToLocation:(CGPoint)x {
// ...
// 启动计价器
Taximeter *meter = [[Taximeter alloc] init];
[meter start];
// 操作车辆,直到抵达位置 x
Car *car = [[Car alloc] init];
[car releaseBrakes];
[car changeGears];
[car pressAccelerator];
// ......
// 当到达了位置 x,就停下车和计价器
[car releaseAccelerator];
[car pressBrakes];
[meter stop];
}
代码清单,客户端代码 viewDidLoad方法
CabDriver *driver = [[CabDriver alloc] init];
[driver driveToLocation:CGPointMake(100.0, 100.0)];
8.3 使用场景
- 子系统正逐渐变得复杂,应用模式的过程中演化出许多类。可以使用外观为这些子系统类提供一个简单的接口。
- 可以使用外观对子系统进行分层,每个子系统级别有一个外观作为入口点。让它们通过其外观进行通信,可以简化它们的依存关系。
对象去耦
9. 中介者
9.1 中介者模式是用一个对象来封装一些列对象的交互方式。中介者使各对象不需要显示地相互引用,从而使其耦合松散,而且可以独立的改变它们之间的交互。
9.2 抽象的 Mediator 定义了用于同 Colleague 交互的一般行为,典型的同事(colleague)是以明确定义的方式进行相互通信的对象,并且彼此紧密依存,ConcreteMediator 为 ConcreteColleague定义了更加具体的行为。如果应用程序只需要一个中介者,有时抽象 Mediator 可以省略。
9.3 使用场景
- 对象间的交互虽定义明确然而非常复杂,导致一组对象彼此相互依赖而且难以理解。
- 因为对象引用了许多其他对象并与其通讯,导致对象难以复用
- 想要定制一个分布在多个类中的逻辑与行为,又不想生成太多子类
10. 观察者
10.1 观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更行。
10.1 观察者模式是一种发布-订阅模型。Observer从Subject订阅通知。ConcreteObserver实现抽象Observer并重载update方法。一旦Subject的实例需要通知Observer任何新的变更,Subject会发送update消息并通知存储内部列表中所有注册的Observer。在ConcreteObserver的update方法的实例实现中,Subject内部状态可被取得并在以后进行处理。
10.2 Subject提供注册与取消注册的方法,任何实现了Observer协议而且想要处理update消息的对象,都可以进行注册或取消注册。当Subject的实例发生变更时,它会想自己发送notify消息。notify方法里有个算法,定义了如何向已注册的观察者广播update消息。
10.3 何时使用观察者模式
- 有两种抽象类型相互依赖。将它们封装在各自的对象中,就可以对它们单独进行改变和复用。
- 对一个对象的改变需要同时改变其他对象,而不知道具体有多少对象有待改变
- 一个对象必须通知其他对象,而它又不需要知道其他对象是什么
10.4 在Cocoa Touch框架中使用观察者模式:通知和键-值观察
10.4.1 通知
Cocoa Touch框架使用NSNotificationCenter和NSNotification对象实现了一对多的发布-订阅模型。它们允许主题与观察者以一种松耦合的方式通信。两者在通信时对另一方无需多少了解。
主题要通知其他对象时,需要创建一个可通过全局的名字来识别的通知对象,然后把它投递到通知中心。通知中心查明特定的观察者,然后通过消息把通知发送给它们。
一旦创建了通知,就用它作为[notificationCenter postNotification:notification]消息调用的参数,投递到通知中心。通过向NSNotificationCenter类发送defaultCenter消息,可以得到NSNotificationCenter实例的引用。每个进程只有一个默认的通知中心,所以默认的NSNotificationCenter是个单例对象。defaultCenter是返回应用程序中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];
// 删除通知
[notificationCenter removeObserver:self name:@"data changes" object:nil];
10.4.2 键-值观察
是一种通知机制,它使对象能够在其他对象的属性发生更改时获得通知。在观察对象和被观察对象之间建立了联系。当被观察对象属性的值发生改变时,会想观察者发送一对一的通知。
这一机制基于NSKeyValueObserving非正式协议,Cocoa通过这个协议为所有遵守协议的对象提供了一种自动化的属性观察能力。要实现自动观察,参与键-值观察(Key-Value Observing,KVO)的对象需要符合键-值编码(KVC)的要求,并且需要符合KVC的存储方法。KVC基于有关非正式协议,通过存储对象属性实现自动观察。也可以使用NSKeyValueObserving的方法和相关范畴来实现手动的观察者通知。
// 监听Person实例对象(person)的name属性变化
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil]
// 然后实现该方法就可以得到属性的变更通知
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"keyPath = %@",keyPath);
NSLog(@"change = %@",change);
}
10.4 通知和键-值观察的主要却别
通知 | 键-值观察 |
---|---|
一个中心对象为所有观察者提供变更通知 | 被观察的对象直接向观察者发送通知 |
主要从广义上关注程序事件 | 绑定于特定对象属性的值 |
抽象集合
11. 组合
11.1 组合模式:将对象组合成树形结构以表示"部分-整体"的层次结构。组合使得用户对单个对象和组合对象的使用具有一致性
基类接口是定义了Leaf类和Composite类的共同操作的Component
每个节点代表一个叶节点或组合体节点。Leaf节点与Composite节点的主要区别在于,Leaf节点不包含同类型的子节点,而Composite则包含。Composite包含同一类型的子节点。由于Leaf类与Composite类有同样的接口,任何对Component类型的操作也能安全地应用到Leaf和Composite。客户端就不需要根据确切类型的is-else语句。
Composite需要方法来管理子节点,比如add:component和remove:component。因为Leaf和Composize有共同的接口,这些方法必须也是接口的一部分。而向Leaf对象发送组合体操作消息则没有意义,也不起作用,只有默认的实现。
11.2 使用场景
- 想获得对象抽象的树形表示(部分-整体层次结构)
- 想让客户端统一处理组合结构中的所有对象
11.3 在Cocoa Touch框架中使用组合模式
Mark协议是Dot、Vertex、Stroke类型的基类型,这样它们具有相同的接口。Dot的实例可以画在视图上,而Stroke的子节点Vertext对象只用来帮助在同一线条中把线连接起来。
Vertex只实现了location属性。Dot子类化Vertext并增加color与size属性,因为Vertex不需要color和size而Dot需要。在运行时aStroke可以包含aDot或aVertex对象。因此Stroke对象既可以是各种Mark的父节点,也可以是由Vertex对象构成的真正的线条组合,作为一个整体绘制在屏幕上。
代码清单 Mark.h
@protocol Mark <NSObject>
@property (nonatomic, strong) UIColor *color;
@property (nonatomic, assign) CGFloat size;
@property (nonatomic, assign) CGPoint location;
@property (nonatomic, readonly) NSUInteger count; // 子节点的个数
@property (nonatomic, readonly) id<Mark>lastChild;
- (id)copy;
- (void)addMark:(id<Mark>)mark;
- (void)removeMark:(id<Mark>)mark;
- (id<Mark>)childMarkAtIndex:(NSUInteger)index;
- (void)drawWithContext:(CGContextRef)context;
@end
代码清单 Vertex.h
@interface Vertex : NSObject <Mark,NSCopying>
{
@protected
CGPoint location_;
}
@property (nonatomic, strong) UIColor *color;
@property (nonatomic, assign) CGFloat size;
@property (nonatomic, assign) CGPoint location;
@property (nonatomic, readonly) NSUInteger count; // 子节点的个数
@property (nonatomic, readonly) id<Mark>lastChild;
- (id)initWithLocation:(CGPoint)location;
- (void)addMark:(id<Mark>)mark;
- (void)removeMark:(id<Mark>)mark;
- (id<Mark>)childMarkAtIndex:(NSUInteger)index;
- (void)drawWithContext:(CGContextRef)context;
@end
代码清单 Vertex.m
@synthesize location = location_;
@dynamic color,size;
- (id)initWithLocation:(CGPoint)location {
if (self = [super init]) {
[self setLocation:location];
}
return self;
}
// 默认属性什么也不做
- (void)setColor:(UIColor *)color {}
- (UIColor *)color {return nil;}
- (void)setSize:(CGFloat)size {}
- (CGFloat)size {return 0.0;}
// Mark 操作什么也不做
- (void)addMark:(id<Mark>)mark {}
- (void)removeMark:(id<Mark>)mark {}
- (id<Mark>)childMarkAtIndex:(NSUInteger)index {return nil;}
- (id<Mark>)lastChild {return nil;}
// 绘图,一个顶点
- (void)drawWithContext:(CGContextRef)context {
CGFloat x = self.location.x;
CGFloat y = self.location.y;
CGContextAddLineToPoint(context, x, y);
}
#pragma mark -
#pragma mark - NSCopying method
// 此方法需要实现,以支持备忘录
- (id)copyWithZone:(NSZone *)zone {
Vertex *vertexCopy = [[[self class] allocWithZone:zone] initWithLocation:location_];
return vertexCopy;
}
代码清单 Dot.h
@interface Dot : Vertex
{
@private
UIColor *color_;
CGFloat size_;
}
@property (nonatomic, strong) UIColor *color;
@property (nonatomic, assign) CGFloat size;
- (void)drawWithContext:(CGContextRef)context;
@end
代码清单 Dot.m
@synthesize size = size_, color = color_;
- (void)drawWithContext:(CGContextRef)context {
CGFloat x = self.location.x;
CGFloat y = self.location.y;
CGFloat frameSize = self.size;
CGRect frame = CGRectMake(x, y, frameSize, frameSize);
CGContextSetFillColorWithColor(context, [self color].CGColor);
CGContextFillEllipseInRect(context, frame);
}
#pragma mark -
#pragma mark - NSCopying method
- (id)copyWithZone:(NSZone *)zone {
Dot *dotCopy = [[[self class] allocWithZone:zone] initWithLocation:location_];
// 复制 color
[dotCopy setColor:[UIColor colorWithCGColor:[color_ CGColor]]];
// 复制 size
[dotCopy setSize:size_];
return dotCopy;
}
代码清单 Stroke.h
@interface Stroke : NSObject <Mark,NSCopying>
{
@private
UIColor *color_;
CGFloat size_;
NSMutableArray *children_;
}
@property (nonatomic, strong) UIColor *color;
@property (nonatomic, assign) CGFloat size;
@property (nonatomic, assign) CGPoint location;
@property (nonatomic, readonly) NSUInteger count;
@property (nonatomic, readonly) id<Mark>lastChild;
- (void)addMark:(id<Mark>)mark;
- (void)removeMark:(id<Mark>)mark;
- (id<Mark>)childMarkAtIndex:(NSUInteger)index;
- (void)drawWithContext:(CGContextRef)context;
@end
代码清单 Stroke.m
@implementation Stroke
@synthesize color = color_,size = size_;
@dynamic location;
- (id)init {
if (self = [super init]) {
children_ = [[NSMutableArray alloc] init];
}
return self;
}
- (void)setLocation:(CGPoint)location {
// 不做任何位置设定
}
- (CGPoint)location {
// 返回第一个节点的位置
if ([children_ count] > 0) {
id<Mark>child = children_[0];
return [child location];
}
// 否则,返回原点
return CGPointZero;
}
- (void)addMark:(id<Mark>)mark {
[children_ addObject:mark];
}
- (void)removeMark:(id<Mark>)mark {
// 如果 mark 在这一层,将其移除并返回
// 否则,让每个自己点去找它
if ([children_ containsObject:mark]) {
[children_ removeObject:mark];
}
else {
[children_ makeObjectsPerformSelector:@selector(removeMark:) withObject:mark];
}
}
- (id<Mark>)childMarkAtIndex:(NSUInteger)index {
if (index >= [children_ count]) {
return nil;
}
return children_[index];
}
// 返回最后子节点的便利方法
- (id<Mark>)lastChild {
return [children_ lastObject];
}
// 返回子节点个数
- (NSUInteger)count {
return [children_ count];
}
- (void)drawWithContext:(CGContextRef)context {
CGContextMoveToPoint(context, self.location.x, self.location.y);
for (id<Mark>mark in children_) {
[mark drawWithContext:context];
}
CGContextSetStrokeColorWithColor(context, [self color].CGColor);
}
#pragma mark -
#pragma mark - NSCopying method
- (id)copyWithZone:(NSZone *)zone {
Stroke *strokeCopy = [[[self class] allocWithZone:zone] init];
// 复制 color
[strokeCopy setColor:[UIColor colorWithCGColor:[color_ CGColor]]];
// 复制 size
[strokeCopy setSize:size_];
// 复制 children
for (id<Mark>child in children_) {
id<Mark>childCopy = [child copy];
[strokeCopy addMark:childCopy];
}
return strokeCopy;
}
Stroke用自己的children_作为NSMutableArray的实例,来保存Mark子节点。
代码清单,客户端构造Mark组合体
Dot *singleDot = [[Dot alloc] initWithLocation:thisPoint];
Vertex *vertex = [[Vertex alloc] initWithLocation:thisPoint];
Stroke *newStroke = [[Stroke alloc] init];
[newStroke addMark:singleDot];
[newStroke addMark:vertex];
Stroke *parentStroke = [[Stroke alloc] init];
[parentStroke addMark:newStroke];
[parentStroke addMark:singleDot];
单个Dot对象可被添加到parentStroke作为叶节点。parentStroke也可以接受组合体Stroke对象,组合体Stroke对象为了让绘图算法绘制相连的线,管理者自己的Vertex子节点。
12. 迭代器
12.1 迭代器提供了一种顺序访问聚合对象(集合)中元素的方法,而无需暴露结构的底层表示和细节。
12.2 List定义了修改集合以及返回集合中元素个数的方法。ListIterator保持一个对List对象的引用,以便迭代器遍历结构中的元素并将其返回。ListIterator定义了让客户端从迭代过程中访问下一项的方法。迭代器有个内部的index_变量,记录集合中的当前位置。
12.3 在Cocoa Touch框架中使用迭代器模式,通过"枚举器/枚举"改写了迭代器模式。
通过NSEnumerator来枚举NSArray、NSDictionary和NSSet对象中的元素。NSEnumerator本身是个抽象类。它依靠几个工厂方法,如:objectEnumerator或keyEnumerator,来创建并返回相应的具体枚举器对象。客户端用返回的枚举器对象遍历集合中的元素,
NSArray *array = @[@"one",@"two",@"three"];
NSEnumerator *itemEnumerator = [array objectEnumerator];
NSString *item;
while (item = [itemEnumerator nextObject]) {
// 对 item作处理
}
基于块的枚举,更加方便
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSString *item = obj;
if ([item isEqualToString:@"two"]) {
// 终止枚举
*stop = YES;
}
}];
12.2 何时实用迭代器模式
- 需要访问组合对象的内容,而又不暴露其内部表示
- 需要通过多种方式遍历组合对象
- 需要提供一个统一的接口,用来遍历各种类型的组合对象
行为扩展
13. 访问者
13.1 访问者模式:表示一个作用于某对象结构中的各元素的操作。它让我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
13.2 访问者模式涉及两个关键角色(或者组件):访问者和它访问的元素。元素可以是任何对象,但通常是"部分-整体"结构中的结点(组合模式)。部分-整体结构包含组合体与叶节点,或者任何其他复杂的对象结构。元素本身不仅限于这些种类的结构。访问者知道复杂结构中的每个元素,可以访问每个元素的结点,并根据元素的特征、属性或操作,执行任何操作。
13.3 Visitor协议声明了两个很像的 visit 方法,用于访问和处理各种Element类型的对象。ConcreteVisitor(1或2)实现这一协议及其抽象方法。 visit 的操作定义了针对特定Element类型的适当操作。Client创建ConcreteVisit(1或2)的对象,并将其传给一个Element对象结构。Element对象结构中有一个方法接受一般化的Visitor类型。继承Element的类中,所有acceptVisitor方法中的操作几乎一样,只有一条语句,让Visitor对象访问发起调用的具体Element对象。实际使用的 visit 消息,定义在每个具体Element类中,这是具体Element类之间的唯一不同之处。每当把acceptVisitor:消息传给Element结构,这个消息就会被转发给每个结点。在运行时确定Element对象的实际类型,再根据实际类型决定该调用哪个visit*方法
13.4 何时实用访问者模式
- 一个复杂的对象结构包含很多其他对象,它们有不同的接口(比如组合体),但是想对这些对象实施一些依赖于其具体类型的操作。
- 需要对一个组合结构中的对象进行很多不相关的操作,但是不想让这些操作“污染”这些对象的类。可以将相关的操作集中起来,定义在一个访问者类中,并在需要在访问者中定义的操作时使用它
- 定义复杂结构的类很少做修改,但经常需要向其添加新的操作。
14. 装饰
14.1 装饰模式:动态地给一个对象添加一些额外的职责。就扩展功能来说,装饰模式相比生成子类更为灵活。
14.2 标准的装饰模式包括一个抽象Component父类,它为其他具体组件(component)声明一些操作。抽象的Component类可被细化为另一个叫做Decorator的抽象类。Decorator包含了另一个Component的引用。ConcreteDecorator为其他Component或Decorator定义了几个扩展行为。并且会在自己的操作中执行内嵌的Component操作。
14.3 Component定义了一些抽象操作,其具体类将进行重载以实现自己特定的操作。Decorator是一个抽象类,它通过将一个Component或Decorator内签到Decorator对象,定义了扩展这个Component的实例的“装饰性”行为。默认的operation方法只是想内嵌的component发送一个消息。ConcreteDecoratorA和ConcreteDecoratorB重载父类的operation,通过super把自己增加的行为扩展给component的operation。如果只需要向component添加一种职责,那么就可以省掉抽象的Decorator类,让ConcreteDecorator直接把请求转发给component。
14.4 何时实用装饰模式
- 想要在不影响其他对象的情况下,以动态、透明的方法给单个对象添加职责
- 想要扩展一个类的行为,却做不到。类定义可能被隐藏,无法进行子类化;或者,对类的么个行为的扩展,为支持每种功能组合,将产生大量的子类
- 对类的职责的扩展是可选的
15. 责任链
15.1 责任链模式:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间发生耦合。此模式将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止
[站外图片上传中...(image-e9d563-1520217476674)]
15.2 责任链模式的主要思想是,对象引用了同一类型的另一个对象,形成一条链。链中的每个对象实现了同样的方法,处理对链中第一个对象发起的同一个请求。如果一个对象不知道如何处理请求,它就把请求传给下一个相应器(即successor)。
15.3 Handler是上层抽象类,定义了一个方法---handleRequest,处理它知道如何处理的请求对象。ConcreteHandler1和ConcreteHandler2实现了handleRequest方法,来处理它们认识的请求对象。Handler也有一个指向另一个同类型实例的引用,即successor。当调用Handler实例的handleRequest消息时,如果这个实例不知道如何处理请求,它会用同样的消息把请求转发给successor。如果successor可以处理,就行了;否则,他就把请求传给下一个successor(如果有的话)。这个过程会一直进行下去,直到请求被传到链中的最后一个successor。
15.4 在RPG游戏中使用责任链模式
Avatar、MetalArmor和CrystalShield是AttackHandler的子类。AttackHandler定义了一个方法---handleAttack:attack,该方法的默认行为是把攻击传给另一个AttackHandler的引用,即成员变量nextAttackHandler_。子类重载这个方法,对攻击提供实际的响应。如果AttackHandler不知道如何响应一个攻击,那么就使用[super handleAttack:attack]消息,把它转发给super,这样super中的默认实现就会把攻击沿着链传下去。
代码清单 AttackHandler.h
#import "Attack.h"
@interface AttackHandler : NSObject
{
@private
AttackHandler *nextAttackHandler_;
}
@property (nonatomic, strong) AttackHandler *nextAttackHandler;
- (void)handleAttack:(Attack *)attack;
@end
代码清单 AttackHandler.m
@implementation AttackHandler
@synthesize nextAttackHandler = nextAttackHandler_;
- (void)handleAttack:(Attack *)attack {
[nextAttackHandler_ handleAttack:attack];
}
@end
代码清单 MetalArmor.h
@interface MetalArmor : AttackHandler
// 重载的方法
- (void)handleAttack:(Attack *)attack;
@end
代码清单 MetalArmor.m
- (void)handleAttack:(Attack *)attack {
if ([attack isKindOfClass:[SwordAttack class]]) {
// 攻击没有通过这个盔甲
NSLog(@"No damage from a sword attack");
}
else {
NSLog(@"I don't know this attack: %@",[self class]);
[super handleAttack:attack];
}
}
代码清单 CrystalShield.m
- (void)handleAttack:(Attack *)attack {
if ([attack isKindOfClass:[MagicFireAttack class]]) {
// 攻击没有通过这个盾牌
NSLog(@"No damage from a magic fire attack");
}
else {
NSLog(@"I don't know this attack: %@",[self class]);
[super handleAttack:attack];
}
}
代码清单 Avatar.m
- (void)handleAttack:(Attack *)attack {
// 当攻击达到这里时,我就被击中了
// 实际损伤的点数取决于攻击的类型
NSLog(@"Oh! I'm hit with a %@",[attack class]);
}
代码清单,客户端代码
// 创建新的人物
AttackHandler *avatar = [[Avatar alloc] init];
// 让它穿上金属盔甲
AttackHandler *metalArmoredAvatar = [[MetalArmor alloc] init];
[metalArmoredAvatar setNextAttackHandler:avatar];
// 然后给金属盔甲中的人物添加一个水晶盾牌
CrystalShield *superAvatar = [[CrystalShield alloc] init];
[superAvatar setNextAttackHandler:metalArmoredAvatar];
// ... 其他行动
// 用剑去攻击人物
Attack *swordAttack = [[SwordAttack alloc] init];
[superAvatar handleAttack:swordAttack];
// 然后用魔法火焰攻击人物
Attack *magicFireAttack = [[MagicFireAttack alloc] init];
[superAvatar handleAttack:magicFireAttack];
// 现在用闪电进行新的攻击
LightningAttack *lightningAttack = [[LightningAttack alloc] init];
[superAvatar handleAttack:lightningAttack];
// ... 进一步的行动
// 客户端代码的输出
I don't know this attack: CrystalShield
No damage from a sword attack
No damage from a magic fire
I don't know this attack: CrystalShield
I don't know this attack: MetalArmor
Oh! I'm hit with a LightningAttack
金属盔甲为人物挡住了剑的攻击,因为有水晶盾牌,魔法火焰攻击也没有伤到人物。但是第三次的闪电攻击,盔甲和盾牌都不知道如何应付,最后,打出来消息:Oh! I'm hit with a LightningAttack,表示因闪电攻击而受到损伤。
15.5 何时使用责任链模式
- 有多个对象可以处理请求,而处理程序只有在运行时才能确定
- 向一组对象发出请求,而不想显示指定处理请求的特定处理程序
算法封装
16. 模板方法
16.1 模板方法模式:定义一个操作中算法的骨架,而将一些步骤延迟到子类中。模板方法使子类可以重定义算法的某些特定步骤而不改变该算法的结构。
模板方法模式是面向对象软件设计中一种非常简单的设计模式,其基本思想是在抽象类的一个方法中定义“标准”算法。在这个方法中调用的基本操作应由子类重载予以实现。这个方法称为模板方法。因为方法定义的算法缺少一些特有的操作。
AbstractClass 不完整地定义了一些方法与算法,留出一些操作未作定义。AbstractClass 调用的templateMethod 时,方法中未定义的空白部分,由 ConcreteClass重载 primitiveOperation1(或2)来填补
说明: 钩子操作给出默认行为,子类可对其扩展。默认行为通常什么都不做。子类可以重载这个方法,为模板方法提供附加的操作。
16.2 在Cocoa Touch框架中使用模板方法,
利用模板方法制作三明治。包含基本步骤:准备面包、把面包放在盘子上、往面包上加肉、加调味料、上餐;可以定义一个叫 make 的模板方法,它调用上述哥哥步骤来制作真正的三明治。制作真正三明治的默认算法有些特定的操作没有实现,所以模板方法知识定义了制作三明治的一般方法。当具体三明治子类重载了三明治的行为之后,客户端仅用 make 消息就能制作真正的三明治了
代码清单 AnySandwich.h
@interface AnySandwich : NSObject
- (void)make;
// 制作三明治的步骤
- (void)prepareBread;
- (void)putBreadOnPlate;
- (void)addMeat;
- (void)addCondiments;
// hook
- (void)extraStep;
- (void)serve;
@end
代码清单 AnySandwich.m
- (void)make {
[self prepareBread];
[self putBreadOnPlate];
[self addMeat];
[self addCondiments];
// hook
[self extraStep];
[self serve];
}
- (void)putBreadOnPlate {
// 做任何三明治都要先把面包放在盘子上
}
// hook
- (void)extraStep {
}
- (void)serve {
// 任何三明治做好了都要上餐
}
#pragma mark -
#pragma mark - Details will be handled by subClasses
- (void)prepareBread {
// 要保证子类重载这个方法
}
- (void)addMeat {
// 要保证子类重载这个方法
}
- (void)addCondiments {
// 要保证子类重载这个方法
}
代码清单 ReubenSandwich.h
@interface ReubenSandwich : AnySandwich
- (void)prepareBread;
- (void)addMeat;
- (void)addCondiments;
// hook
- (void)extraStep;
// 鲁宾三明治的特有操作
- (void)cutRyeBread;
- (void)addCornBeef;
- (void)addSauerkraut;
- (void)addThousandIslandDressing;
- (void)addSwissCheese;
- (void)grillit;
@end
代码清单 ReubenSandwich.m
- (void)prepareBread {
[self cutRyeBread];
}
- (void)addMeat {
[self addCornBeef];
}
- (void)addCondiments {
[self addSauerkraut];
[self addThousandIslandDressing];
[self addSwissCheese];
}
// hook
- (void)extraStep {
[self grillit];
}
#pragma mark -
#pragma mark - ReubenSandwich Specific methods
- (void)cutRyeBread {
// 鲁宾三明治需要两片黑麦面包
}
- (void)addCornBeef {
// ...... 加大量腌牛肉
}
- (void)addSauerkraut {
// ...... 还有德国酸菜
}
- (void)addThousandIslandDressing {
// ...... 别忘了千岛酱
}
- (void)addSwissCheese {
// ...... 还有上等瑞士奶酪
}
- (void)grillit {
// 最后要把它烤一下
}
ReubenSandwich是AnySandwich的子类。制作鲁宾三明治有其特有的步骤和配料。鲁宾三明治的面包需要黑麦面包,肉需要腌牛肉,还需要德国酸菜,调味料需要千岛酱和瑞士奶酪。虽然奶酪不能算调味料,但这么做可以简化制作三明治的一般步骤,因为不是所有三明治都有奶酪。
代码清单 Humburger.h
@interface Humburger : AnySandwich
- (void)prepareBread;
- (void)addMeat;
- (void)addCondiments;
// 汉堡包的特有方法
- (void)getBurgerBun;
- (void)addKetchup;
- (void)addMustard;
- (void)addBeefPatty;
- (void)addCheese;
- (void)addPickles;
@end
代码清单 Humburger.m
- (void)prepareBread {
[self getBurgerBun];
}
- (void)addMeat {
[self addBeefPatty];
}
- (void)addCondiments {
[self addKetchup];
[self addMustard];
[self addPickles];
}
#pragma mark -
#pragma mark - Humburger Specific Methods
- (void)getBurgerBun {
// 汉堡包需要小圆面包
}
- (void)addKetchup {
// 先要放番茄酱,才能加其他材料
}
- (void)addMustard {
// 然后加点儿芥末酱
}
- (void)addBeefPatty {
// 汉堡包的主料是一片牛肉饼
}
- (void)addCheese {
// 假定汉堡包都有奶酪
}
- (void)addPickles {
// 最后加点儿腌黄瓜
}
Humburger也是AnySandwich的子类,它也有自己的制作细节。汉堡包的面包需要小圆面包,肉需要牛肉饼,调味料需要番茄酱、芥末酱、腌黄瓜和奶酪。
16.2 何时实用模板方法
- 需要一次性实现算法的不变部分,并将可变的行为留给子类来实现
- 子类的共同行为应该被提取出来放到公共类中,以避免代码重复。现有代码的差别应该被分离为新的操作。然后用一个调用这些新操作的模板方法来替换这些不同的代码
- 需要控制子类的扩展。可以定义一个在特定点调用 “钩子”(hook)操作的模板方法,子类可以通过对钩子操作的实现在这些点扩展功能。
17. 策略
17.1 策略模式:定义一些列算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化
策略模式中的一个关键角色是策略类,它为所有支持的或相关的算法申明了一个共同接口。另外,还有使用策略接口来实现相关算法的具体策略类。场景(context)类的对象配置有一个具体策略对象的实例,场景对象使用策略接口调用由具体策略类定义的算法。
17.2 何时使用策略模式
- 一个类在其操作中使用多个条件语句来定义许多行为。我们可以把相关的条件分之移到它们自己的策略类中
- 需要算法的各种变体
- 需要避免把复杂的、与算法相关的数据结构暴露给客户端
18. 命令
18.1 命令模式:将请求封装为一个对象,从而可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。
18.2 何时使用命令模式
- 想让应用程序支持撤销与恢复
- 想用对象参数化一个动作以执行操作,并用不同命令对象来代替回调函数
- 想要在不同时刻对请求进行指定、排列和执行
- 想记录修改日志,这样在系统故障时,这些修改可在后来重做一遍
- 想让系统支持事务,事务封装了对数据的一些列修改。事务可以建模为命令对象