一、概念
1、命令模式的动机
夏天撸串加冰啤酒简直爽翻天,我们可以通过向服务员点菜烤羊肉、烤鱼、冰啤酒等,然后厨师或者超市就会提供做好的食物给我们。这个时候消费者和厨师是分开的,消费者只管点菜,不用管哪个厨师或者厨师啥时候做。
在软件开发中,也存在很多与此类似的,将请求的发送者和接收者解耦,从而降低系统的耦合度,这种模式称之为命令模式。
2、命令模式的定义
命令模式(Command Pattern):将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式。
命令模式的本质是对请求进行封装,一个请求对应于一个命令,将发出命令的责任和执行命令的责任分割开。
3、命令模式的4个角色
1)Command(抽象命令类):抽象命令类一般是一个抽象类或接口,在其中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接收者的相关操作。
2)ConcreteCommand(具体命令类):具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中。在实现execute()方法时,将调用接收者对象的相关操作(Action)。
3)Invoker(调用者):调用者即请求发送者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类之间存在关联关系。在程序运行时可以将一个具体命令对象注入其中,再调用具体命令对象的execute()方法,从而实现间接调用请求接收者的相关操作。
4)Receiver(接收者):接收者执行与请求相关的操作,它具体实现对请求的业务处理。
4、结构图
二、示例
命令模式的关键在于引入了抽象命令类,请求发送者针对抽象命令类编程,只有实现了抽象命令类的具体命令才与请求接收者相关联。
1、常规命令模式
1)先创建一个Command类,有一个execute()方法,表示抽象命令类;
2)然后创建三个类FishCommand、MuttonCommand和BeerCommand,都继承自Command类,与接收者相关联,表示具体命令类;
3)然后创建一个Customer类,有一个Command实例,表示调用者;
4)最后创建两个类Cook和Supermarket,一个时负责做饭的,一个负责买现成的,表示接收者。
Command类:
// 抽象命令类
@interface Command : NSObject
- (void)execute;
@end
@implementation Command
- (void)execute {}
@end
FishCommand、MuttonCommand和BeerCommand类:
// FishCommand
@interface FishCommand : Command
@end
@interface FishCommand ()
@property (nonatomic, strong) Cook *cook;
@end
@implementation FishCommand
- (instancetype)init
{
self = [super init];
if (self) {
_cook = [Cook new];
}
return self;
}
- (void)execute {
[self.cook cookFoodWithDescription:@"烤鱼一条"];
}
@end
// MuttonCommand
@interface MuttonCommand : Command
@end
@interface MuttonCommand ()
@property (nonatomic, strong) Cook *cook;
@end
@implementation MuttonCommand
- (instancetype)init
{
self = [super init];
if (self) {
_cook = [Cook new];
}
return self;
}
- (void)execute {
[self.cook cookFoodWithDescription:@"烤羊肉串10串"];
}
@end
// BeerCommand
@interface BeerCommand : Command
@end
@interface BeerCommand ()
@property (nonatomic, strong) Supermarket *market;
@end
@implementation BeerCommand
- (instancetype)init
{
self = [super init];
if (self) {
_market = [Supermarket new];
}
return self;
}
- (void)execute {
[self.market supplyFoodWithDescription:@"啤酒一打"];
}
@end
Customer类:
// 调用者
@interface Customer : NSObject
@property (nonatomic, strong) Command *command;
- (instancetype)initWithCommand:(Command *)command;
- (void)needFood;
@end
@implementation Customer
- (instancetype)initWithCommand:(Command *)command {
self = [super init];
if (self) {
_command = command;
}
return self;
}
- (void)needFood {
[self.command execute];
}
@end
Cook和Supermarket类:接收者
// Cook
@interface Cook : NSObject
- (void)cookFoodWithDescription:(NSString *)des;
@end
@implementation Cook
- (void)cookFoodWithDescription:(NSString *)des {
NSLog(@"厨师做食物:%@", des);
}
@end
// Supermarket
@interface Supermarket : NSObject
- (void)supplyFoodWithDescription:(NSString *)des;
@end
@implementation Supermarket
- (void)supplyFoodWithDescription:(NSString *)des {
NSLog(@"超市购买食物:%@", des);
}
@end
运行代码:
- (void)viewDidLoad {
[super viewDidLoad];
// 一个个点菜
Command *fish = [FishCommand new];
Customer *customer = [[Customer alloc] initWithCommand:fish];
[customer needFood];
Command *mutton = [MuttonCommand new];
customer.command = mutton;
[customer needFood];
Command *beer = [BeerCommand new];
customer.command = beer;
[customer needFood];
}
打印结果:
厨师做食物:烤鱼一条
厨师做食物:烤羊肉串10串
超市购买食物:啤酒一打
2、命令队列
比如现在不是一个个点菜,而是用菜单一起点菜,那么可以创建一个菜单类来存放多个命令。
CommandQueue类:
// 命令队列
@interface CommandQueue : NSObject
- (void)addCommand:(Command *)command;
- (void)removeCommand:(Command *)command;
- (void)execute;
@end
@interface CommandQueue ()
@property (nonatomic, strong) NSMutableArray<Command *> *commands;
@end
@implementation CommandQueue
- (instancetype)init
{
self = [super init];
if (self) {
_commands = [NSMutableArray array];
}
return self;
}
- (void)addCommand:(Command *)command {
[self.commands addObject:command];
}
- (void)removeCommand:(Command *)command {
if ([self.commands containsObject:command]) {
[self.commands removeObject:command];
}
}
- (void)execute {
for (Command *command in self.commands) { //一起执行所有的命令
[command execute];
}
}
@end
那么只需要修改Customer类为:
@interface Customer : NSObject
@property (nonatomic, strong) CommandQueue *queue;
- (instancetype)initWithQueue:(CommandQueue *)queue;
- (void)needFood;
@end
@implementation Customer
- (instancetype)initWithQueue:(CommandQueue *)queue {
self = [super init];
if (self) {
_queue = queue;
}
return self;
}
- (void)needFood {
[self.queue execute];
}
@end
运行代码:
- (void)viewDidLoad {
[super viewDidLoad];
// 一起点菜
Command *fish = [FishCommand new];
Command *mutton = [MuttonCommand new];
Command *beer = [BeerCommand new];
CommandQueue *queue = [CommandQueue new];
[queue addCommand:fish];
[queue addCommand:mutton];
[queue addCommand:beer];
Customer *customer = [[Customer alloc] initWithQueue:queue];
[customer needFood];
}
打印结果:
厨师做食物:烤鱼一条
厨师做食物:烤羊肉串10串
超市购买食物:啤酒一打
三、总结
命令模式是一种使用频率非常高的设计模式,它可以将请求发送者与接收者解耦,请求发送者通过命令对象来间接引用请求接收者,使得系统具有更好的灵活性和可扩展性。
1、优点
1、降低系统的耦合度。由于请求者与接收者之间不存在直接引用,因此请求者与接收者之间实现完全解耦,相同的请求者可以对应不同的接收者,同样,相同的接收者也可以供不同的请求者使用,两者之间具有良好的独立性。
2、新的命令可以很容易地加入到系统中。由于增加新的具体命令类不会影响到其他类,因此增加新的具体命令类很容易,无须修改原有系统源代码,甚至客户类代码,满足“开闭原则”的要求。
3、可以比较容易地设计一个命令队列或宏命令(组合命令)。
4、为请求的撤销(Undo)和恢复(Redo)操作提供了一种设计和实现方案。
2、缺点
使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类,因此在某些系统中可能需要提供大量的具体命令类,这将影响命令模式的使用。
3、适用场景
1、系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。请求调用者无须知道接收者的存在,也无须知道接收者是谁,接收者也无须关心何时被调用。
2、系统需要在不同的时间指定请求、将请求排队和执行请求。一个命令对象和请求的初始调用者可以有不同的生命期,换言之,最初的请求发出者可能已经不在了,而命令对象本身仍然是活动的,可以通过该命令对象去调用请求接收者,而无须关心请求调用者的存在性,可以通过请求日志文件等机制来具体实现。
3、系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
4、系统需要将一组操作组合在一起形成宏命令。
4、iOS应用举例
在Cocoa Touch框架中,典型的应用有:
1)目标-动作(Target-Action):比如按钮添加各种点击事件。
2)NSInvocation:它封装了向接收者转发执行消息所需的必要信息,有目标对象、方法选择器和方法的参数。
3)NSUndoManager:通用的撤销栈的管理类,它的实例可以管理自己撤销和恢复栈。
Demo地址:iOS-Design-Patterns