如何採用命令模式實現"撤銷/恢復"
前言:現在大部分優秀的編輯器都帶有 "撤銷/恢復"功能。這個功能就是相當於傳說中的”後悔藥“,方便大家隨時切換到以前的某一個點。
為了尋找“後悔藥”,我們也開始了該功能的探索之旅。本文主要考慮的方法是採用命令模式實現該功能的思路!
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
通過上面的一些探討,大家是否發現了一些問題。
如果我們要增加新的命令類,就會出現大量的命令類爆發
Demo中的接受者類,運用的陣列儲存,恢復和撤銷的資料,是否有更加優化的方案
如果我們要記錄的命令操作非常多,資料儲存非常大,該如何解決?
敬請期待後續優化...
文/Mob開發者平臺 資深IOS開發工程師 趙義
- END -