看图识模式
前提&场景
我们有若干个军团,军团有若干功能:1.陆地攻击目标 2.轰炸目标 3.保护目标 4.从目标撤退等。
将军可以下达指令给军团,调用军团的功能。
需求
在一场battle中,将军运筹帷幄,给旗下的军团下达了一系列的命令:
攻击1号山地
攻击4号山地
保护3号山地
......
如果用类去实现,可以这样
将军直接调用军团A的attack:方法,同样也可以直接调用bomb、protect方法,这样并没有问题。
如果我们引入新的需求:
我们需要对将军下发的命令进行排队(下发多条命令时需要按照优先级执行)、记录执行命令、撤销命令、传递命令、重复执行等相关操作。
如果还是按照上面直接调用来做,基本上很难实现,有以下几个主要问题:
-
不同军种攻击方法不同,需要知道每个类或方法的细节才能调用。导致彼此拥有高耦合度。
- 将军下命令攻击某个标的,本质上他并不关心由那个军种该如何攻击的这种细节。
-
对命令进行操作(撤销、记录等)将非常复杂
- 直接调用的后果就是不能直接找到某次调用,再对这次操作进行操作。通常需要额外的环境存放这些操作相关的参数、顺序、执行者等,当程序变大时,基本上难以维护。
-
命令不可传递和复用
- 如果需要多个军团攻击目标A,需要调用每个军团攻击方法。某个时间重复攻击,任然需要再次调用,不能复用。
如何设计呢?
这里主要解决2个问题
-
需要把命令封装起来,这样我们就可以对这个封装的对象进行种种操作了
比如将军下的命令是一封信,信里上标明执行的等级,攻击的目标等参数信息。 当若干封信来的时候我们可以根据等级排列,执行完后还可以把信交给其它军团再次执行。。。
-
调用者和执行者解耦
将军(调用者)封装一个命令对象,哪个军团接到这个命令对象就执行这个命令,调用者不用关心执行的细节,只需关注命令本身即可。
如下图:
模式定义
定义
命令模式: 将请求封装为一个对象,从而可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。
静态类图
命令模式包含如下角色:
- 客户端(Client)角色: 创建一个具体命令(ConcreteCommand)对象并确定其接收者。
- 命令(Command)角色:声明了一个给所有具体命令类的抽象接口。
- 具体命令(ConcreteCommand)角色:定义一个接收者和行为之间的弱耦合;实现execute()方法,负责调用接收者的相应操作。execute()方法通常叫做执行方法。
- 请求者(Invoker)角色:负责调用命令对象执行请求,相关的方法叫做行动方法。
- 接收者(Receiver)角色:负责具体实施和执行一个请求。任何一个类都可以成为接收者,实施和执行请求的方法叫做行动方法。
分析
命令模式的本质是对命令进行封装,将发出命令的责任和执行命令的责任分割开。
- 每一个命令都是一个操作:请求的一方发出请求,要求执行一个操作;接收的一方收到请求,并执行操作。
- 命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否被执行、何时被执行,以及是怎么被执行的。
- 命令模式使请求本身成为一个对象,这个对象和其他对象一样可以被存储和传递。
- 命令模式的关键在于引入了抽象命令接口,且发送者针对抽象命令接口编程,只有实现了抽象命令接口的具体命令才能与接收者相关联。
动机
在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用命令模式来进行设计,使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活。
命令模式可以对发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。这就是命令模式的模式动机。
Command模式是最让我疑惑的一个模式。它实际上不是个很具体,规定很多的模式,正是这个灵活性,让人容易产生confuse。
代码
根据上面的场景来进行code实现;
接收者
接受者是命令模式中去执行具体操作的对象,这里指的具体军团
@interface Legion : NSObject
@property (nonatomic, copy) NSString *name;
- (instancetype)initWithName: (NSString *)name;
// 攻击
- (void)attack:(NSString *)target;
// 轰炸
- (void)bomb:(NSString *)target;
// 保护
- (void)protect:(NSString *)target;
// 撤回
- (void)recall:(NSString *)target;
@end
@implementation Legion
- (instancetype)initWithName: (NSString *)name
{
self = [super init];
if (self) {
_name = name;
}
return self;
}
- (void)attack:(NSString *)target
{
NSLog(@"军团: %@--------攻击: %@", self.name, target);
}
- (void)bomb:(NSString *)target
{
NSLog(@"军团: %@--------轰炸: %@", self.name, target);
}
- (void)protect:(NSString *)target
{
NSLog(@"军团: %@--------保护: %@", self.name, target);
}
- (void)recall:(NSString *)target
{
NSLog(@"军团: %@--------撤回: %@", self.name, target);
}
@end
抽象命令Command
命令对象中暴露了指定的调用execute方法,命令的调用者和接收者针对抽象的命令类编写,从而达到解耦合的目的。
发起命令的对象完全不知道具体实现对象是谁,也不知道如何实现。
#import "Legion.h"
@interface Command : NSObject
@property (nonatomic, assign) NSInteger level;
@property (nonatomic, strong) Legion *legion;
@property (nonatomic, copy) NSString *target;
- (instancetype)initWithReciver:(Legion *)legion target:(NSString *)target;
- (void)attack;
- (void)recall;
@end
@implementation Command
- (instancetype)initWithReciver:(Legion *)legion target:(NSString *)target
{
self = [super init];
if (self) {
_legion = legion;
_target = target;
}
return self;
}
- (void)attack
{
}
- (void)recall
{
}
@end
具体命令
陆地攻击命令 LandAttackCommand
#import "Command.h"
@interface LandAttackCommand : Command
- (void)attack;
- (void)recall;
@end
@implementation LandAttackCommand
- (void)attack
{
[self.legion attack:self.target];
}
- (void)recall
{
[self.legion recall:self.target];
}
@end
空袭命令 AirAttackCommand
#import "Command.h"
@interface AirAttackCommand : Command
- (void)attack;
- (void)recall;
@end
@implementation AirAttackCommand
- (void)attack
{
[self.legion bomb:self.target];
}
- (void)recall
{
[self.legion recall:self.target];
}
@end
调用者&客户端
这里我们可以写一个调用者类,用这个类负责命令的创建和调用,还有对命令的管理(比如排序、日志记录等等)。
这里我就不单独写了,具体业务中可以依据具体情况去编写。
下面的调用例子把客户端和调用者放在一起不做具体区分。
Legion *legionA = [[Legion alloc] initWithName:@"军团A"];
Command *landComand1 = [[LandAttackCommand alloc] initWithReciver:legionA target:@"4号高地"];
landComand1.level = 100;
Command *landComand2 = [[LandAttackCommand alloc] initWithReciver:legionA target:@"5号高地"];
landComand2.level = 200;
Command *airAttackCommand1 = [[AirAttackCommand alloc] initWithReciver:legionA target:@"4号高地"];
airAttackCommand1.level = 50;
Command *airAttackCommand2 = [[AirAttackCommand alloc] initWithReciver:legionA target:@"5号高地"];
airAttackCommand2.level = 500;
NSArray *commandList = @[landComand1, landComand2, airAttackCommand1, airAttackCommand2];
for (Command *comand in commandList) {
[comand attack];
}
// 命令传递
Legion *legionB = [[Legion alloc] initWithName:@"军团B"];
landComand1.legion = legionB;
[landComand1 attack];
// 撤销
[landComand1 recall];
// 可以根据level等级对所有的对命令进行排序、记录等操作...
结果
由此可见,我们对请求行为进行了封装,从而可以对这个请求对象(命令)进行一系列的操作。而且还化解了调用者和接收者之间的耦合关系。
特征
使用环境
在以下情况下可以使用命令模式:
- 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
- 系统需要在不同的时间指定请求、将请求排队和执行请求。
- 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
- 系统需要将一组操作组合在一起,即支持宏命令
优点&缺点
优点
命令模式的优点
- 降低系统的耦合度。
- 新的命令可以很容易地加入到系统中。
- 可以比较容易地设计一个命令队列和宏命令(组合命令)。
- 可以方便地实现对请求的Undo和Redo。
缺点
命令模式的缺点
- 使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个命令都需要设计一个具体命令类,因此某些系统可能需要大量具体命令类,这将影响命令模式的使用。