如何采用命令模式实现"撤销/恢复"

本文介绍了如何采用命令模式来实现撤销/恢复功能,详细阐述了设计模式的概念、原则和分类,特别是命令模式的定义、角色、优缺点及适用场景。通过示例代码展示了命令模式在人物移动操作中的应用,同时提出了宏命令的概念,并讨论了命令模式在应对大量命令类和数据存储优化方面的挑战。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言:现在大部分优秀的编辑器都带有 "撤销/恢复"功能。这个功能就是相当于传说中的”后悔药“,方便大家随时切换到以前的某一个点。


为了寻找“后悔药”,我们也开始了该功能的探索之旅。本文主要考虑的方法是采用命令模式实现该功能的思路!

 

mweb


xcode



美图

一、设计模式的一些术语


1、设计模式定义


设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。


使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。


设计模式对一类问题提供了相应的解决方案,所以使用上与问题场景紧密结合。


2、设计原则


  • 单一职责原则

一个类只做一件事,引起它变化的原因只有一个。

尽量避免修改一个功能时,影响太多其他的东西,这个原则的划分细粒度是一个难点,只能通过工作经验的积累才能过更好的应用

 

  • 开闭原则

即对扩展开放,对修改关闭

提供良好的可扩展性,维护性。比如:创建一个图形基类,再创建一个方形,圆形,如果要新增别的图形,只需要新增一个类就要,不影响其他现有的功能和代码

 

  • 里氏代换原则

即子类可以代替父类的全部功能。反过来就不行

比如写了一个鸟类,有会飞的功能,再写一个鸵鸟,集成这个鸟类,会飞的功能,就违反了这个原则

 

  • 依赖倒转原则

即高层代码不应该依赖于底层代码,而应该依赖于接口,即面向接口编程。

我们的pc电脑,在设计USB 模块的时候,应该是要遵循usb2.0,3.0的接口编程,而不是为具体的u 盘,或者厂家的u 盘做专门的设计

 

  • 接口隔离原则

使用多个隔离的接口,比使用单个接口要好。

降低耦合,方便维护,避免,集成后,执行很多不用的方法

 

  • 合成/聚合复用原则

尽量使用对象组合,而不是继承来达到复用的目的。

降低耦合性,调高灵活性,可复用性

 

  • 迪米特法则

两个类之间不必彼此直接通信,那么这两个类不应该直接发生相互作用。

通过第三者来降低耦合度


设计模式就是实现了这些原则,从而达到了代码复用、增加可维护性的目的。


3、基本的设计模式
设计模式分为三种类型,共23种。


* 创建型模式:单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式。

* 结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。

* 行为型模式:模版方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式(Interpreter模式)、状态模式、策略模式、职责链模式(责任链模式)、访问者模式。


二、命令模式


1、定义


将一个请求封装为一个对象(即我们创建的Command对象),从而使你可用不同的请求对客户进行参数化; 对请求排队或记录请求日志,以及支持可撤销的操作。
  
2、UML 图


命令模式由以下角色组成:



—— 命令角色(Command):定义命令的接口,声明执行的方法。

 

——具体命令角色(Concrete Command):实现命令接口,是“虚”的实现;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。

 

——接收者角色(Receiver):负责具体实施和执行一个请求。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。

 

—— 请求者(调用者)角色(Invoker):负责调用命令对象执行请求。

 

—— 客户角色(Client):创建一个具体命令对象并设定该命令对象的接收者。

   

3、基本代码


    
@protocol ICommand <NSObject>

/**
执行操作
**/
- (void)execute;


@end
//具体命令
@interface ConcreteCommand : NSObject <ICommand>

- (instancetype)initWithReceiver:(Receiver *)receiver;

@end

@interface ConcreteCommand()

@property (nonatomic ,strong) Receiver *receiver;

@end

@implementation ConcreteCommand

- (instancetype)initWithReceiver:(Receiver *)receiver
{
if(self = [super init])
{
_receiver = receiver;
}

return self;
}

- (void)execute
{
[_receiver action];
}

@end
//接受者
@interface Receiver : NSObject

- (void)action;

@end

@implementation Receiver

- (void)action
{
NSLog(@"----%@---", NSStringFromSelector(_cmd));
}

@end
//触发者
@interface Invoker : NSObject

@property (nonatomic, strong)id<ICommand> command;

- (void)runCommand;
@end


@implementation Invoker

- (void)runCommand
{
[self.command execute];
}

@end
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.

//创建接收者
Receiver *receiver = [Receiver new];

//创建命令对象,设定它的接收者
id<ICommand> command = [[ConcreteCommand alloc] initWithReceiver:receiver];

//创建Invoker,把命令对象设置进去
Invoker *invoker = [Invoker new];
[invoker setCommand:command];

[invoker runCommand];

return YES;
}


4、优缺点


优点:

 

①降低系统的耦合度。由于请求者与接收者之间不存在直接引用,因此请求者与接收者之间实现完全解耦,相同的请求者可以对应不同的接收者,同样,相同的接收者也可以供不同的请求者使用,两者之间具有良好的独立性。

 

②新的命令可以很容易地加入到系统中。由于增加新的具体命令类不会影响到其他类,因此增加新的具体命令类很容易,无须修改原有系统源代码,甚至客户类代码,满足“开闭原则”的要求。

 

③可以比较容易地设计一个命令队列或宏命令(组合命令)。

 

④为请求的撤销(Undo)和恢复(Redo)操作提供了一种设计和实现方案。

    

缺点:


使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类,因此在某些系统中可能需要提供大量的具体命令类,这将影响命令模式的使用。

    

适用场景


在以下情况下可以考虑使用命令模式:

 

①系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。请求调用者无须知道接收者的存在,也无须知道接收者是谁,接收者也无须关心何时被调用。

 

②系统需要在不同的时间指定请求、将请求排队和执行请求。一个命令对象和请求的初始调用者可以有不同的生命期,换言之,最初的请求发出者可能已经不在了,而命令对象本身仍然是活动的,可以通过该命令对象去调用请求接收者,而无须关心请求调用者的存在性,可以通过请求日志文件等机制来具体实现。

 

③系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。

 

④系统需要将一组操作组合在一起形成宏命令。


5、Demo 人物移动


①UML图



②具体代码,详见DEMO


    
//Command 接口
@protocol ICommand <NSObject>

/**
执行操作
**/
- (void)execute;

@optional
/**
撤销操作
**/
- (void)undo;


@end

移动命令

@implementation MoveCommand

- (instancetype)initWithPeople:(id<IPeople>)people point:(CGPoint)point
{
if (self = [super init])
{
_people = people;
_oriPoint = people.point;
_movePoint = point;
}

return self;
}

/**
执行操作
**/
- (void)execute
{
if(_people)
{
[_people moveToPoint:_movePoint];

NSLog(@"---%@--%@ position x: %f y: %f name: %@",NSStringFromSelector(_cmd), @"MoveCommand", _movePoint.x, _movePoint.y, _people.name);
}

}

/**
撤销操作
**/
- (void)undo
{
if(_people)
{

[_people moveToPoint:_oriPoint];

NSLog(@"---%@--%@ position x: %f y: %f name: %@",NSStringFromSelector(_cmd), @"MoveCommand", _movePoint.x, _movePoint.y, _people.name);

}
}

@end

修改名称命令

@implementation NameCommand


- (instancetype)initWithPeople:(id<IPeople>)people name:(NSString *)name;
{
if (self = [super init])
{
_people = people;
_oriName = people.name;
_moveName = name;
}

return self;
}

/**
执行操作
**/
- (void)execute
{
if(_people)
{
[_people changeName:_moveName];

NSLog(@"---%@--%@ position x: %f y: %f name: %@",NSStringFromSelector(_cmd), @"NameCommand", _people.point.x, _people.point.y, _people.name);
}

}

/**
撤销操作
**/
- (void)undo
{
if(_people)
{

[_people changeName:_oriName];

NSLog(@"---%@--%@ position x: %f y: %f name: %@",NSStringFromSelector(_cmd), @"NameCommand", _people.point.x, _people.point.y, _people.name);

}
}

命令执行
@implementation PeopleView

/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code
}
*/

- (CGPoint)point
{
return self.frame.origin;
}

- (NSString*)name
{
return self.text;
}

-(void)moveToPoint:(CGPoint)point
{
CGRect rect = self.frame;
rect.origin = point;

self.frame = rect;
}

-(void)changeName:(NSString *)name
{
self.text = name;
}

@end


触发者代码:
@interface GameManager()
{
}
//撤销数组
@property (nonatomic, strong)NSMutableArray *undoList;

//重做数组
@property (nonatomic, strong)NSMutableArray *redoList;

@end

@implementation GameManager

//+ (instancetype)sharedManager {
// static GameManager *sharedInstance = nil;
// static dispatch_once_t onceToken;
// dispatch_once(&onceToken, ^{
// sharedInstance = [[self alloc] init];
//
// });
// return sharedInstance;
//}

-(id)init
{
if (self = [super init])
{
_undoList = [NSMutableArray new];
_redoList = [NSMutableArray new];

}
return self;
}

-(void)push:(id<ICommand>)command
{
[_undoList addObject:command];
[_redoList removeAllObjects];

[command execute];
}

-(void)redoCommand
{
if(_redoList.lastObject)
{
id<ICommand> command = _redoList.lastObject;
[_redoList removeLastObject];
[_undoList addObject:command];
[command execute];
}
}

-(void)undoCommand
{
if(_undoList.lastObject)
{
id<ICommand> command = _undoList.lastObject;
[_undoList removeLastObject];
[_redoList addObject:command];

[command undo];
}
}

@end


//执行调用

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.

peoples = @[@"德华" ,@"judy", @"lady", @"power", @"天天", @"Stylite", @"David", @"约翰", @"乐乐"];

}



- (GameManager *)gameMananger
{
if (!_gameMananger)
{
_gameMananger = [[GameManager alloc] init];
}
return _gameMananger;
}


#pragma mark - 动作

- (IBAction)changePosition:(id)sender
{
int x = arc4random() % 200 + 30;
int y = arc4random() % 200 + 100;

MoveCommand *command = [[MoveCommand alloc] initWithPeople:self.peopleView point:CGPointMake(x, y)];
//存入
[self.gameMananger push:command];
}

- (IBAction)changeName:(id)sender
{
int x = arc4random() % peoples.count ;


NameCommand *command = [[NameCommand alloc] initWithPeople:self.peopleView name:peoples[x]];

//存入
[self.gameMananger push:command];
}

- (IBAction)undo:(id)sender
{
[self.gameMananger undoCommand];

}

- (IBAction)redo:(id)sender
{
[self.gameMananger redoCommand];

}


6、宏命令 DEMO


核心思路就使用了组合命令


    
//Command 接口
@protocol IMCCommand <NSObject>

/**
执行操作
**/
- (void)execute;

@end


//宏接口定义
@interface MobileMacroCommand : NSObject <IMacroCommand>

@end

@interface MobileMacroCommand()

@property (nonatomic, strong) NSMutableArray *commands;

@end

@implementation MobileMacroCommand

- (id)init
{
if (self = [super init])
{
_commands = [NSMutableArray new];
}
return self;
}

/**
执行操作
**/
- (void)execute
{
if(_commands.count > 0)
{
for (id<IMCCommand> command in _commands)
{
[command execute];
}
}

}

/**
* 添加命令
*/
- (void)addCommand:(id<IMCCommand>)command
{
[_commands addObject:command];
}

/**
* 移除命令
*/
- (void)removeCommand:(id<IMCCommand>)command
{
[_commands removeObject:command];

}

@end


通过上面的一些探讨,大家是否发现了一些问题。


  1. 如果我们要增加新的命令类,就会出现大量的命令类爆发

  2. Demo中的接受者类,运用的数组存储,恢复和撤销的数据,是否有更加优化的方案

  3. 如果我们要记录的命令操作非常多,数据储存非常大,该如何解决?


敬请期待后续优化...


文/Mob开发者平台 资深IOS开发工程师 赵义


-  END  -



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值