命令模式,把请求封装成一个对象,可以记录请求日志,可以进行撤销操作;可以撤销请求这个是命令模式的核心能力了:
秉承着不给demo的文章不是好程序员的思想:DEMO 密码: ppsd ,如果失效请留言,看到后会更新;如果文章中有不对的地方,请指出,不胜感激;如果想对设计模式有一个系统的学习,也可以留言,一起交流.;设计模式类的文章会不定期更新,我也在学习ing...
最近自己也在看《Objective-C编程之道iOS设计模式解析》,算是设计模式方面非常牛逼的一本书了,忘记上传到demo里面了,需要这本书的可以留言,只是电子版,网上也有资源,我没有去找.
敲黑板:如果真的想学习设计模式,当然或者其他的,只看永远没用,敲
才是程序员进步的开始!!!
命令模式的四大角色:
- 接收者--->处理具体的命令的逻辑代码,具体的实现都在这里;
- 命令接口,也就是我们OC中的协议;
- 具体命令--->就是协议的实现;
- 请求者Invoker,或者说是命令管理器manager--->负责管理所有的命令;
以俄罗斯方块为例,有具体命令:左,右,变形,向下;然后我们可以加入对命令的撤销操作,全部撤销等;
- 首先我们创建一个命令接口:
#ifndef TMCommandProtocol_h
#define TMCommandProtocol_h
//俄罗斯方块命令协议(标准)->接口
@protocol TMCommandProtocol<NSObject>
//操作->具体实现方法
-(void)execute;
@end
#endif
- 然后是具体的命令,这里创建三个:左,右,变形,具体的命令,要实现协议方法,调用接收者中的具体的指令,所以它需要遵从协议,同时持有接收者对象;
#import <Foundation/Foundation.h>
#import "TMCommandProtocol.h"
#import "TetrisMachine.h" //这个就是接收者
//实现协议->持有接收者引用
@interface TMLeftCommand : NSObject<TMCommandProtocol>
- (instancetype)init:(TetrisMachine*)tm;
@end
#import "TMLeftCommand.h"
@interface TMLeftCommand()
@property(nonatomic, strong) TetrisMachine* tm;
@end
@implementation TMLeftCommand
- (instancetype)init:(TetrisMachine*)tm{
self = [super init];
if (self) {
self.tm = tm;
}
return self;
}
-(void)execute{
[self.tm toLeft];
}
@end
🤣🤣其他两个类的实现也是一幕幕一样样的,只是在execute方法中调用的分别是 toRight,和toTransform方法而已
- 然后就是接收者,具体的实现类:
#import <Foundation/Foundation.h>
@interface TetrisMachine : NSObject
-(void)toLeft;
-(void)toRight;
-(void)toTransform;
@end
#import "TetrisMachine.h"
@implementation TetrisMachine
-(void)toLeft{
NSLog(@"向左..");
}
-(void)toRight{
NSLog(@"向右..");
}
-(void)toTransform{
NSLog(@"变形..");
}
@end
4.我们的请求者,也就是命令管理者,它持有所有的具体命令的实例对象和接收者的实例对象,同时对外提供接口.
#import <Foundation/Foundation.h>
#import "TetrisMachine.h"
#import "TMLeftCommand.h"
#import "TMRightCommand.h"
#import "TMTransformCommand.h"
@interface TetrisMachineManager : NSObject
- (instancetype)init:(TetrisMachine*)tm left:(TMLeftCommand*)left right:(TMRightCommand*)right transform:(TMTransformCommand*)transform;
-(void)toLeft;
-(void)toRight;
-(void)toTransform;
-(void)undo;
-(void)undoAll;
@end
#import "TetrisMachineManager.h"
@interface TetrisMachineManager()
//父类引用指向子类实例对象(面向对象编程)->架构设计中以后经常看到->后面讲解的内容都将面向协议
@property(nonatomic, strong) NSMutableArray* commands;
@property(nonatomic, strong) TetrisMachine* tm;
@property(nonatomic, strong) TMLeftCommand* left;
@property(nonatomic, strong) TMRightCommand* right;
@property(nonatomic, strong) TMTransformCommand* transform;
@end
@implementation TetrisMachineManager
- (instancetype)init:(TetrisMachine*)tm left:(TMLeftCommand*)left right:(TMRightCommand*)right transform:(TMTransformCommand*)transform{
self = [super init];
if (self) {
self.commands = [[NSMutableArray alloc] init];
self.tm = tm;
self.left = left;
self.right = right;
self.transform = transform;
}
return self;
}
-(void)toLeft{
//回调命令
[self.left execute];
//保存命令
//保存的对象中,包含 (具体的命令和 接收者对象),这就是一个完整的可执行的命令;
[self.commands addObject:[[TMLeftCommand alloc] init:self.tm] ];
}
-(void)toRight{
[self.right execute];
[self.commands addObject:[[TMRightCommand alloc] init:self.tm] ];
}
-(void)toTransform{
[self.transform execute];
[self.commands addObject:[[TMTransformCommand alloc] init:self.tm] ];
}
-(void)undo{
if (self.commands.count > 0) {
NSLog(@"撤销");
//撤销
[[self.commands lastObject] execute];
//移除
[self.commands removeLastObject];
}
}
//撤销所有
-(void)undoAll{
NSLog(@"撤销所有");
//这里不能使用具体的类,要用抽象类才可以
for (id<TMCommandProtocol> command in self.commands) {
[command execute];
}
[self.commands removeAllObjects];
}
@end
这里分析一下:如果抛开对命令的存储,撤销操作,我们要怎么实现这个功能?其实非常的简单,我们只需要一个接收者的实例对象,直接调用方法就可以了,比如要向右,一行代码足以:
[self.tm toRight];
,我们只需要一个接收者的类就可以了.
但是要想撤销一个命令呢?我们这样写却做不到,所以才会创建具体的命令,让命令持有接收者的实例对象,同时在代理方法中调用具体命令的具体实现,最后把这个具体命令的实例对象保存在了array中.这就是命令模式的大致原理了.
最后VC中的调用就很简单了
TMLeftCommand* left = [[TMLeftCommand alloc] init:tm];
TMRightCommand* right = [[TMRightCommand alloc] init:tm];
TMTransformCommand* transform = [[TMTransformCommand alloc] init:tm];
//创建请求者(童鞋们课后扩展功能,指定撤回某一个步骤)
TetrisMachineManager* manager = [[TetrisMachineManager alloc] init:tm left:left right:right transform:(TMTransformCommand *)transform];
[manager toLeft];
[manager toRight];
[manager toTransform];
问题:
到这里,不知道大家对以上代码有没有什么疑问,比如[[self.commandArray lastObject] execute_new]
这行代码,为什么array的元素可以调用到execute_new方法 ?
以下是个人的理解:
- 首先,我们的execute_new方法来自于协议TMCommandProtocol ;
- CommandCenter_new类遵从了协议,所以 CommandCenter_new的实例对象 可以调用 execute_new方法;
- CommandManager类中导入了CommandCenter_new文件,意味着在CommandManager类中,是可以创建一个
CommandCenter_new的实例对象的,这也就意味着 如果CommandManager类有一个id类型的对象,那么这个对象就有可能是CommandCenter_new的实例对象,当然,只是有可能; - 而id类型的对象,系统对其有可能调用的所有方法都不报错,当然,运行时可能崩溃,这个要看运行时id被转换成了什么具体的实例对象了;
- 系统的
@interface NSMutableArray<ObjectType> : NSArray<ObjectType>
,是一个泛型;id是指向泛型类型的引用的,所以这里可以调用到;
这里可以再引申一个问题:看代码
@interface TestCounter : NSObject
- (TestCounter *)count;
@end
//实现,省略了
int main(int argc,char *argv[])
{
@autoreleasepool{
[(id)[TestCounter new] count];
}
return 0;
}
这里,本来直接使用[TestCounter new] count]; 是没毛病的,肯定可以调用到我们自己的 count方法;
但是这里我们加了一个强转--->(id),意味着这个时候是一个id类型的对象调用了count方法,
会报错:Multiple methods named 'count' found with mismatched result, parameter type or attributes
编译器会找到多个count方法,比如 nsarray中的等等,且我们的count方法返回值类型void和查找到的count返回值类型还不匹配.
而如果我们把count的返回值改成: - (NSUInteger)count;则不会报错,因为Foundation框架的所有count方法的返回值都是一个NSUInteger类型,编译器找到的签名自然都是一样的;同时我们会发现,我们的count方法会被执行,并不会调用到系统的count方法;
问题来源: https://blog.csdn.net/zhangao0086/article/details/44131091
优化1.动态指令
以上 是命令模式的基本写法,但是我们会发现,如果我们的具体命令很多的情况下,会造成大量的冗余代码,这不利于我们的开发,所以提出优化方案:
1.我希望命令类只有一个;
2.数组中保存了指令对象,我希望可以把具体的指令也保存到数组中,也就是说把具体执行命令的代码块全部保存到数组中,所以首先我们会想到block.下面是优化步骤:
1.干掉上面的 TMLeftCommand,TMRightCommand,TMTransformCommand,重新创建一个新的命令类,这个命令已然是持有接收者对象,遵从协议,实现协议方法;同时现在需要一个block,这个block用于执行具体的命令;
#import <Foundation/Foundation.h>
#import "TMCommandProtocol.h"
#import "TetrisMachine.h" //接收者
//定义一个block
typedef void (^DynamicBlock)(TetrisMachine *tm);
@interface DymainCommand : NSObject<TMCommandProtocol>
-(instancetype)initWithReciever:(TetrisMachine *)tm block:(DynamicBlock)block;
+(id<TMCommandProtocol>)createWithReciever:(TetrisMachine *)tm block:(DynamicBlock)block;
/*
id<TMCommandProtocol> 这里也是一个泛型,id在这里只存储TMCommandProtocol 类型
为什么这样写,因为看起来牛逼 😎😎
*/
@end
#import "DymainCommand.h"
@interface DymainCommand ()
@property (nonatomic,strong) TetrisMachine *tm;
@property (nonatomic,copy) DynamicBlock block;
@end
@implementation DymainCommand
- (instancetype)initWithReciever:(TetrisMachine *)tm block:(DynamicBlock)block
{
self = [super init];
if (self) {
self.tm = tm;
self.block = block;
}
return self;
}
//实现协议方法
-(void)execute {
self.block(self.tm);
}
//创建对象时,由于初始化参数比较复杂,所以在内部进行处理,外部不需要持有一个实例对象,配置参数等
+(id<TMCommandProtocol>)createWithReciever:(TetrisMachine *)tm block:(DynamicBlock)block{
//🚫这里需要创建对象,而不是使用self去调用,因为这里根本没有当前类的实例对象存在🚫
return [[DymainCommand alloc] initWithReciever:tm block:block];
}
//这样写也没毛病,因为这里返回的实例对象只有一种,如果可能有多种, 使用id👆
+(DymainCommand *)createNewWithReciever:(TetrisMachine *)tm block:(DynamicBlock)block{
return [[DymainCommand alloc] initWithReciever:tm block:block];
}
然后我们修改我们的请求者:
#import <Foundation/Foundation.h>
#import "TetrisMachine.h"
@interface DymainManager : NSObject
- (instancetype)init:(TetrisMachine*)tm;
-(void)toLeft;
-(void)toRight;
-(void)toTransform;
-(void)undo;
-(void)undoAll;
@end
#import "DymainManager.h"
#import "DymainCommand.h"
@interface DymainManager ()
@property (nonatomic,strong) TetrisMachine* machine;
@property (nonatomic,strong) NSMutableArray *commandArray; //存储操作过的指令
@end
@implementation DymainManager
- (instancetype)init:(TetrisMachine *)tm
{
self = [super init];
if (self) {
self.machine = tm;
}
return self;
}
-(NSMutableArray *)commandArray {
if (!_commandArray) {
_commandArray = [NSMutableArray arrayWithCapacity:10];
}
return _commandArray;
}
-(void)toLeft {
[self.machine toLeft]; //这个是直接 调用 接收者 的方法
}
@end
如果不保存指令,其实就是上面这样了,但是现在我们需要在调用-(void)toLeft ;方法时把这个完整的命令同时保存在数组中,所以,现在要想办法保存具体指令了;
分析: 保存的是一个DymainCommand的实例对象,所以需要创建一个实例对象
[DymainCommand createWithReciever:self.machine block:^(TetrisMachine *tm) {
}]
然后,我们调用toLeft指令,就要实现 block内 [self.machine toLeft]方法调用;
调用 toRight指令,就要实现 block内 [self.machine toRight]方法调用;说白了就是要实现一个动态指令,所以代码更改如下:
-(void)toLeft {
[self addCommad:@"toLeft"];
//这个是 传递参数 创建block 并保存 具体的命令(DymainCommand的实例对象) 到数组中,这个保存,就是为了能够进行撤销操作存在的
[self.machine toLeft]; //这个是直接 调用 接收者 的方法
}
-(void)addCommad:(NSString *)methodName{
// SEL sel = NSSelectorFromString(methodName);
[self.commandArray addObject:[DymainCommand createWithReciever:self.machine block:^(TetrisMachine *tm) {
NSLog(@"Block调用,撤销操作");
if (!tm) {
NSLog(@"block传参错误!!!😡😡");
return ;
}
SEL sel = NSSelectorFromString(methodName);
( ( void(*)(id,SEL) )[tm methodForSelector:sel] )(tm,sel);
}]];
}
这里有一个警告,使用tm调用,抛出内存问题,具体原因在命令模式第一节已经提过了,这篇文章也有一个完整的解答: 内存问题
优化2.复合型命令
就是,如果有几个命令需要同时调用,可以进行统一的配置,然后对外提供一个方法即可
创建复合型命令类,这里添加了一个删除三个操作的方法:
#import <Foundation/Foundation.h>
#import "TMCommandProtocol.h"
@interface WrapperCommand : NSObject<TMCommandProtocol>
-(instancetype)initWithCommandArray:(NSArray *)comArray;
-(void)oneTwoThreeCommandUndo;
@end
#import "WrapperCommand.h"
@interface WrapperCommand ()
@property (nonatomic,strong) NSArray *comArray;
@end
@implementation WrapperCommand
-(instancetype)initWithCommandArray:(NSArray *)comArray{
if (self = [super init]) {
self.comArray = comArray;
}
return self;
}
-(void)execute_new{
if (self.comArray.count<=0) return;
for (id<TMCommandProtocol> command in self.comArray) {
[command execute_new];
}
}
-(void)oneTwoThreeCommandUndo{
if (self.comArray.count>=3) {
for (int i=0; i<3; i++) {
id<TMCommandProtocol> command = self.comArray[i];
[command execute_new];
}
}else{
NSLog(@"命令不足三个");
}
}
而manager类同之前一样,只是添加了一个可以删除三个方法:
-(void)gotoUndoThree{
if (self.commandArray.count<3) return;
NSLog(@"撤销3个操作--->");
//复合命令调用
WrapperCommand* command = [[WrapperCommand alloc] initWithCommandArray:self.commandArray];
[command oneTwoThreeCommandUndo];
//删除数组数据,省略...
}
完整的调用可以看demo中的sp2文件
优化3.万能命令
系统NSUndoManager的实现,也叫做万能命令;其实上面的代码中,我们的接收者是写死的,现在我想动态的设置接收者去处理不同的事情,所以需要使用到泛型去处理:
下面的新的command类
#import <Foundation/Foundation.h>
#import "TMCommandProtocol.h"
/**
优化第三步->系统NSUndoManager实现,万能命令
接收者之前写死了
T:表示任意类型标记(表示符)->Type类型含义->T(ObjectType)
*/
@interface GenericsCommand<T> : NSObject<TMCommandProtocol>
-(instancetype)initWithTet:(T)tet CommandBlock:(void(^)(T))block;
+(id<TMCommandProtocol>)initWithTet:(T)tet CommandBlock:(void(^)(T))block;
@end
#import "GenericsCommand.h"
@interface GenericsCommand<T> ()
@property (nonatomic,strong) T tet;
@property (nonatomic,copy) void(^block)(T);
@end
@implementation GenericsCommand
/*
泛型声明用的T,但是实现时,系统会自动转存 id
*/
-(instancetype)initWithTet:(id)tet CommandBlock:(void (^)(id))block{
self = [super init];
if (self) {
self.tet = tet;
self.block = block;
}
return self;
}
+(id<TMCommandProtocol>)initWithTet:(id)tet CommandBlock:(void (^)(id))block{
return [[GenericsCommand alloc] initWithTet:tet CommandBlock:block];
}
-(void)execute_new{
self.block(self.tet);
}
@end
这样在调用时就可以传入不同的接收者了,manager类核心代码如下:
-(void)addCommandToArrayWithMethodName:(NSString *)methodName{
[self.commandArray addObject:[GenericsCommand initWithTet:self.tet CommandBlock:^(TetrisMachine *tet) {
SEL sel = NSSelectorFromString(methodName);
((void(*)(id,SEL)) [tet methodForSelector:sel])(tet,sel);
}]];
}
-(void)addCommandToArrayWithMethodName_sec:(NSString *)methodName{
[self.commandArray addObject:[GenericsCommand initWithTet:self.sec_tet CommandBlock:^(TetrisMachine *tet) {
SEL sel = NSSelectorFromString(methodName);
((void(*)(id,SEL)) [tet methodForSelector:sel])(tet,sel);
}]];
}
看起来,不就是一个id类型嘛,什么泛型搞得这么麻烦.是的,这里确实是一样的;当然,泛型有泛型的优势,泛型的一些基础知识可以看看这里:泛型
优化4.存储命令对象更改为存储block代码块:
manager类中的主要更改
typedef void(^commandBlock)(TetrisMachine *tet);
-(void)addCommandToArrayWithMethodName:(NSString *)methodName{
//这里存储的是block代码块了,不在是command的实例对象;
[self.commandArray addObject:^(TetrisMachine *tet){
SEL sel = NSSelectorFromString(methodName);
((void(*)(id,SEL)) [tet methodForSelector:sel])(tet,sel);
}];
}
-(void)gotoUndo{
if (self.commandArray.count>0) {
NSLog(@"撤销最后一步------->");
// [[self.commandArray lastObject] execute_new];
commandBlock block = [self.commandArray lastObject];
block(self.tet);
[self.commandArray removeLastObject];
}
}
-(void)gotoAllUndo{
if (self.commandArray.count<=0) return;
NSLog(@"撤销所有操作--->");
//本应该倒序,懒~~~
for (commandBlock block in self.commandArray) {
block(self.tet);
}
[self.commandArray removeAllObjects];
}
我们把之前存储命令的实例对象改成了存储block代码块了,好处是我们不再需要命令类了.
注:上面的撤销操作是调用的上一步的方法,这里只是为了方便说明问题才这样做,后面会更新一个实例说明具体应该如何操作(比如商品的增加和减少,其实在增加5个时,保存减少5个的操作,在减少时亦如此,每个操作都应该有个与之对应的相反的操作可以调用保存).