02.设计模式之命令模式

命令模式,把请求封装成一个对象,可以记录请求日志,可以进行撤销操作;可以撤销请求这个是命令模式的核心能力了:

秉承着不给demo的文章不是好程序员的思想:DEMO 密码: ppsd ,如果失效请留言,看到后会更新;如果文章中有不对的地方,请指出,不胜感激;如果想对设计模式有一个系统的学习,也可以留言,一起交流.;设计模式类的文章会不定期更新,我也在学习ing...

最近自己也在看《Objective-C编程之道iOS设计模式解析》,算是设计模式方面非常牛逼的一本书了,忘记上传到demo里面了,需要这本书的可以留言,只是电子版,网上也有资源,我没有去找.

敲黑板:如果真的想学习设计模式,当然或者其他的,只看永远没用,才是程序员进步的开始!!!

命令模式的四大角色:

  1. 接收者--->处理具体的命令的逻辑代码,具体的实现都在这里;
  2. 命令接口,也就是我们OC中的协议;
  3. 具体命令--->就是协议的实现;
  4. 请求者Invoker,或者说是命令管理器manager--->负责管理所有的命令;

以俄罗斯方块为例,有具体命令:左,右,变形,向下;然后我们可以加入对命令的撤销操作,全部撤销等;

  1. 首先我们创建一个命令接口:
#ifndef TMCommandProtocol_h
#define TMCommandProtocol_h

//俄罗斯方块命令协议(标准)->接口
@protocol TMCommandProtocol<NSObject>

//操作->具体实现方法
-(void)execute;

@end

#endif
  1. 然后是具体的命令,这里创建三个:左,右,变形,具体的命令,要实现协议方法,调用接收者中的具体的指令,所以它需要遵从协议,同时持有接收者对象;
#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方法而已
  1. 然后就是接收者,具体的实现类:
#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方法 ?
以下是个人的理解:

  1. 首先,我们的execute_new方法来自于协议TMCommandProtocol ;
  2. CommandCenter_new类遵从了协议,所以 CommandCenter_new的实例对象 可以调用 execute_new方法;
  3. CommandManager类中导入了CommandCenter_new文件,意味着在CommandManager类中,是可以创建一个
    CommandCenter_new的实例对象的,这也就意味着 如果CommandManager类有一个id类型的对象,那么这个对象就有可能是CommandCenter_new的实例对象,当然,只是有可能;
  4. 而id类型的对象,系统对其有可能调用的所有方法都不报错,当然,运行时可能崩溃,这个要看运行时id被转换成了什么具体的实例对象了;
  5. 系统的@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个的操作,在减少时亦如此,每个操作都应该有个与之对应的相反的操作可以调用保存).

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。