23、设计模式:策略与命令模式解析

设计模式:策略与命令模式解析

在软件开发中,设计模式是解决常见问题的有效手段。本文将深入探讨策略模式和命令模式,介绍它们的概念、应用场景以及在实际项目中的实现方法。

策略模式

策略模式允许客户端(上下文)类使用相关算法的变体。以自定义 UITextField 的输入验证器为例,不同的验证器可以改变自定义 UITextField 的核心功能。

策略模式与装饰器模式有些相似,但二者存在明显区别。装饰器模式从外部扩展对象的行为,就像给对象换了一层“皮肤”;而策略模式将不同的策略封装在对象内部,改变的是对象的“核心”。

在实际应用中,若 textField 对象为 CustomTextField ,可通过在接口构建器中使用引用连接来构建相关对象。若将类的某些属性声明为 IBOutlet ,就可以像处理其他按钮一样,将 InputValidator 实例分配给 *TextField

命令模式

命令模式借鉴了战场上将军将指令封装在密封信封中交给士兵执行的思想。在面向对象设计中,将指令封装为不同的命令对象,这些对象可以在不同时间被不同客户端传递和重用。

命令模式的概念

命令对象封装了对目标执行指令的信息,客户端或调用者无需了解目标的具体细节,就能对其执行操作。通过将请求封装为对象,客户端可以对请求进行参数化、排队、记录,还能支持撤销操作。命令对象将一个或多个操作与特定的接收者绑定在一起,从而解耦了操作对象和执行操作的接收者。

命令模式的结构如下:
- 客户端创建 ConcreteCommand 对象并设置其接收者。
- 调用者请求通用命令(实际上是 ConcreteCommand )来执行请求。
- Command 是调用者已知的通用接口(协议)。
- ConcreteCommand 作为接收者和操作之间的中间人。
- 接收者可以是执行与命令对象对应的实际操作的任何对象。

命令模式的应用场景

当遇到以下情况时,可以考虑使用命令模式:
- 希望应用程序支持撤销/重做功能。
- 希望将操作参数化为对象,以执行操作并使用不同的命令对象替换回调。
- 希望在不同时间指定、排队和执行请求。
- 希望记录更改,以便在系统故障时重新应用。
- 希望系统支持事务,将事务封装为数据更改集,这些事务可以建模为命令对象。

虽然可以直接执行方法,但随着程序规模的增大,管理和重用对象会变得非常困难,而命令模式可以有效解决这些问题。

命令模式在 Cocoa Touch 框架中的应用

Cocoa Touch 框架对命令模式进行了适配,其中 NSInvocation NSUndoManager 和目标 - 动作机制是典型的应用。由于目标 - 动作机制在许多初级 iOS 编程书籍中已有介绍,这里主要介绍 NSInvocation NSUndoManager

  • NSInvocation 对象 NSInvocation 对象类似于 ConcreteCommand 类,它封装了在运行时将执行消息转发给接收者所需的信息,如目标对象、方法选择器和方法参数。创建 NSInvocation 对象时,需要提供 NSMethodSignature 对象,该对象包含方法调用的参数和返回值类型。
    以下是使用 NSInvocation 实现命令模式的流程:
graph LR
    A[客户端] --> B[创建 NSInvocation 对象]
    B --> C[设置接收者和操作选择器]
    C --> D[设置调用者(如 UIButton)]
    D --> E[调用者调用 NSInvocation 对象的 invoke 方法]
    E --> F[NSInvocation 对象调用目标的选择器]
  • NSUndoManager :自 iOS 3.0 引入以来, NSUndoManager 可用于 iOS 应用开发。它是一个通用的撤销栈管理类,功能强大且用途广泛。

NSUndoManager 管理自己的撤销和重做栈,撤销栈将所有已调用的操作作为对象进行维护。它可以通过调用从撤销栈中弹出的操作对象来“反转”撤销操作(即重做)。注册的撤销操作可以是反转客户端之前调用的操作。

注册撤销命令操作有两种方式:
- 简单撤销注册 :需要一个选择器来标识撤销操作,并将接收者作为参数。在进行任何更改之前,将状态传递给撤销管理器。当撤销操作被调用时,接收者将被重置为之前的状态或属性。
- 基于调用的注册 :涉及 NSInvocation 对象,可处理具有任意数量和类型参数的方法,适用于状态更改需要多个参数的情况。

在大多数情况下,包含或管理应用中其他对象的客户端对象拥有 NSUndoManager 实例。由于 NSUndoManager 不会保留其调用目标接收者,客户端对象需要确保推入 NSUndoManager 撤销栈的接收者的引用计数至少为 1,否则在执行撤销/重做操作时可能导致应用崩溃。

在 TouchPainter 应用中实现撤销/重做功能

为了让用户能够撤销和重做在屏幕上的绘图操作,我们将为 TouchPainter 应用设计和实现撤销/重做架构。下面将介绍使用 NSUndoManager 实现绘图/撤销绘图的方法。

  • 添加生成绘图/撤销绘图调用的方法 :每次需要 NSUndoManager 注册撤销/重做操作时,都需要一个新的 NSInvocation 对象。以下是生成绘图和撤销绘图调用的方法:
- (NSInvocation *) drawScribbleInvocation
{
  NSMethodSignature *executeMethodSignature = [scribble_  
                                               methodSignatureForSelector:
                                               @selector(addMark:
                                                         shouldAddToPreviousMark:)];
  NSInvocation *drawInvocation = [NSInvocation  
                                  invocationWithMethodSignature:
                                  executeMethodSignature];
  [drawInvocation setTarget:scribble_];
  [drawInvocation setSelector:@selector(addMark:shouldAddToPreviousMark:)];
  BOOL attachToPreviousMark = NO;
  [drawInvocation setArgument:&attachToPreviousMark atIndex:3];

  return drawInvocation;
}

- (NSInvocation *) undrawScribbleInvocation
{
  NSMethodSignature *unexecuteMethodSignature = [scribble_  
                                                 methodSignatureForSelector:
                                                 @selector(removeMark:)];
  NSInvocation *undrawInvocation = [NSInvocation  
                                    invocationWithMethodSignature:
                                    unexecuteMethodSignature];
  [undrawInvocation setTarget:scribble_];
  [undrawInvocation setSelector:@selector(removeMark:)];

  return undrawInvocation;
}

drawScribbleInvocation 方法生成一个用于向 scribble_ 添加 Mark 对象的 NSInvocation 对象, undrawScribbleInvocation 方法生成一个用于从 scribble_ 移除 Mark 对象的 NSInvocation 对象。

  • 添加在 NSUndoManager 中注册撤销/重做操作的方法 :以下是执行和撤销 NSInvocation 对象的方法:
#pragma mark Draw Scribble Command Methods

- (void) executeInvocation:(NSInvocation *)invocation  
        withUndoInvocation:(NSInvocation *)undoInvocation
{
  [invocation retainArguments];

  [[self.undoManager prepareWithInvocationTarget:self]  
   unexecuteInvocation:undoInvocation 
   withRedoInvocation:invocation];

  [invocation invoke];
}

- (void) unexecuteInvocation:(NSInvocation *)invocation  
          withRedoInvocation:(NSInvocation *)redoInvocation
{   
  [[self.undoManager prepareWithInvocationTarget:self]  
   executeInvocation:redoInvocation 
   withUndoInvocation:invocation];

  [invocation invoke];
}

executeInvocation:withUndoInvocation: 方法立即执行一个调用对象,并将另一个调用对象注册为撤销操作; unexecuteInvocation:withRedoInvocation: 方法使用第一个参数中的调用对象执行撤销操作,并将第二个参数注册为重做操作。

  • 修改触摸事件处理程序以进行调用 :修改 CanvasViewController 中的原始触摸事件处理程序,创建调用对象以准备所有绘图操作的撤销/重做。以下是修改后的触摸事件处理程序:
#pragma mark -
#pragma mark Touch Event Handlers

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
  startPoint_ = [[touches anyObject] locationInView:canvasView_];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
  CGPoint lastPoint = [[touches anyObject] previousLocationInView:canvasView_];

  // add a new stroke to scribble
  // if this is indeed a drag from
  // a finger
  if (CGPointEqualToPoint(lastPoint, startPoint_))
  {
    id <Mark> newStroke = [[[Stroke alloc] init] autorelease];
    [newStroke setColor:strokeColor_];
    [newStroke setSize:strokeSize_];

    [scribble_ addMark:newStroke shouldAddToPreviousMark:NO];

    // retrieve a new NSInvocation for drawing and
    // set new arguments for the draw command
    NSInvocation *drawInvocation = [self drawScribbleInvocation];
    [drawInvocation setArgument:&newStroke atIndex:2];

    // retrieve a new NSInvocation for undrawing and
    // set a new argument for the undraw command
    NSInvocation *undrawInvocation = [self undrawScribbleInvocation];
    [undrawInvocation setArgument:&newStroke atIndex:2];

    // execute the draw command with the undraw command
    [self executeInvocation:drawInvocation withUndoInvocation:undrawInvocation];
  }

  // add the current touch as another vertex to the
  // temp stroke
  CGPoint thisPoint = [[touches anyObject] locationInView:canvasView_];
  Vertex *vertex = [[[Vertex alloc]  
                     initWithLocation:thisPoint]  
                    autorelease];

  // we don't need to undo every vertex
  // so we are keeping this
  [scribble_ addMark:vertex shouldAddToPreviousMark:YES];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
  CGPoint lastPoint = [[touches anyObject] previousLocationInView:canvasView_];
  CGPoint thisPoint = [[touches anyObject] locationInView:canvasView_];

  // if the touch never moves (stays at the same spot until lifted now)
  // just add a dot to an existing stroke composite
  // otherwise add it to the temp stroke as the last vertex
  if (CGPointEqualToPoint(lastPoint, thisPoint))
  {
    Dot *singleDot = [[[Dot alloc]  
                       initWithLocation:thisPoint]  
                      autorelease];
    [singleDot setColor:strokeColor_];
    [singleDot setSize:strokeSize_];

    [scribble_ addMark:singleDot shouldAddToPreviousMark:NO];

    // retrieve a new NSInvocation for drawing and
    // set new arguments for the draw command
    NSInvocation *drawInvocation = [self drawScribbleInvocation];
    [drawInvocation setArgument:&singleDot atIndex:2];

    // retrieve a new NSInvocation for undrawing and
    // set a new argument for the undraw command
    NSInvocation *undrawInvocation = [self undrawScribbleInvocation];
    [undrawInvocation setArgument:&singleDot atIndex:2];

    // execute the draw command with the undraw command
    [self executeInvocation:drawInvocation withUndoInvocation:undrawInvocation];
  }

  // reset the start point here
  startPoint_ = CGPointZero;

  // if this is the last point of stroke
  // don't bother to draw it as the user
  // won't tell the difference
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
  // reset the start point here
  startPoint_ = CGPointZero;
}

touchesMoved: 方法中,当检测到新的笔画开始时,创建 Stroke 对象并生成绘图和撤销绘图的 NSInvocation 对象,然后执行绘图操作并注册撤销操作。在 touchesEnded: 方法中,当绘制点时,执行相同的操作。

通过以上步骤,我们借助 NSUndoManager 实现了撤销/重做基础设施。后续还可以探讨如何从头开始构建自己的撤销/重做系统。

设计模式:策略与命令模式解析

从零构建撤销/重做系统

除了使用 NSUndoManager 实现撤销/重做功能外,我们还可以从头开始构建自己的撤销/重做系统。下面将逐步介绍构建过程。

设计思路

要构建自己的撤销/重做系统,核心是维护两个栈:撤销栈和重做栈。当用户执行一个操作时,将该操作的逆操作压入撤销栈;当用户执行撤销操作时,从撤销栈中弹出操作并执行,同时将该操作的逆操作压入重做栈;当用户执行重做操作时,从重做栈中弹出操作并执行,同时将该操作的逆操作压入撤销栈。

实现步骤
  1. 定义操作类 :首先,我们需要定义表示操作的类。以绘图应用为例,操作可以是添加或移除一个 Mark 对象。
@interface DrawingOperation : NSObject
@property (nonatomic, strong) id <Mark> mark;
@property (nonatomic, assign) BOOL isAddOperation;
- (instancetype)initWithMark:(id <Mark>)mark isAddOperation:(BOOL)isAddOperation;
@end

@implementation DrawingOperation
- (instancetype)initWithMark:(id <Mark>)mark isAddOperation:(BOOL)isAddOperation {
    self = [super init];
    if (self) {
        _mark = mark;
        _isAddOperation = isAddOperation;
    }
    return self;
}
@end
  1. 实现撤销/重做栈管理类 :创建一个管理撤销和重做栈的类。
@interface CustomUndoManager : NSObject
@property (nonatomic, strong) NSMutableArray *undoStack;
@property (nonatomic, strong) NSMutableArray *redoStack;
- (void)addOperation:(DrawingOperation *)operation;
- (void)undo;
- (void)redo;
@end

@implementation CustomUndoManager
- (instancetype)init {
    self = [super init];
    if (self) {
        _undoStack = [NSMutableArray array];
        _redoStack = [NSMutableArray array];
    }
    return self;
}

- (void)addOperation:(DrawingOperation *)operation {
    [self.undoStack addObject:operation];
    [self.redoStack removeAllObjects]; // 执行新操作后,清空重做栈
}

- (void)undo {
    if (self.undoStack.count > 0) {
        DrawingOperation *operation = [self.undoStack lastObject];
        [self.undoStack removeLastObject];

        // 执行逆操作
        if (operation.isAddOperation) {
            // 移除 Mark
            [scribble_ removeMark:operation.mark];
        } else {
            // 添加 Mark
            [scribble_ addMark:operation.mark shouldAddToPreviousMark:NO];
        }

        // 创建逆操作并压入重做栈
        DrawingOperation *reverseOperation = [[DrawingOperation alloc] initWithMark:operation.mark isAddOperation:!operation.isAddOperation];
        [self.redoStack addObject:reverseOperation];
    }
}

- (void)redo {
    if (self.redoStack.count > 0) {
        DrawingOperation *operation = [self.redoStack lastObject];
        [self.redoStack removeLastObject];

        // 执行操作
        if (operation.isAddOperation) {
            // 添加 Mark
            [scribble_ addMark:operation.mark shouldAddToPreviousMark:NO];
        } else {
            // 移除 Mark
            [scribble_ removeMark:operation.mark];
        }

        // 创建逆操作并压入撤销栈
        DrawingOperation *reverseOperation = [[DrawingOperation alloc] initWithMark:operation.mark isAddOperation:!operation.isAddOperation];
        [self.undoStack addObject:reverseOperation];
    }
}
@end
  1. 修改触摸事件处理程序 :在触摸事件处理程序中使用自定义的撤销/重做管理器。
@interface CanvasViewController ()
@property (nonatomic, strong) CustomUndoManager *customUndoManager;
@end

@implementation CanvasViewController
- (instancetype)init {
    self = [super init];
    if (self) {
        _customUndoManager = [[CustomUndoManager alloc] init];
    }
    return self;
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    CGPoint lastPoint = [[touches anyObject] previousLocationInView:canvasView_];
    if (CGPointEqualToPoint(lastPoint, startPoint_)) {
        id <Mark> newStroke = [[[Stroke alloc] init] autorelease];
        [newStroke setColor:strokeColor_];
        [newStroke setSize:strokeSize_];

        [scribble_ addMark:newStroke shouldAddToPreviousMark:NO];

        // 创建操作并添加到撤销栈
        DrawingOperation *operation = [[DrawingOperation alloc] initWithMark:newStroke isAddOperation:YES];
        [self.customUndoManager addOperation:operation];
    }

    CGPoint thisPoint = [[touches anyObject] locationInView:canvasView_];
    Vertex *vertex = [[[Vertex alloc] initWithLocation:thisPoint] autorelease];
    [scribble_ addMark:vertex shouldAddToPreviousMark:YES];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    CGPoint lastPoint = [[touches anyObject] previousLocationInView:canvasView_];
    CGPoint thisPoint = [[touches anyObject] locationInView:canvasView_];
    if (CGPointEqualToPoint(lastPoint, thisPoint)) {
        Dot *singleDot = [[[Dot alloc] initWithLocation:thisPoint] autorelease];
        [singleDot setColor:strokeColor_];
        [singleDot setSize:strokeSize_];

        [scribble_ addMark:singleDot shouldAddToPreviousMark:NO];

        // 创建操作并添加到撤销栈
        DrawingOperation *operation = [[DrawingOperation alloc] initWithMark:singleDot isAddOperation:YES];
        [self.customUndoManager addOperation:operation];
    }

    startPoint_ = CGPointZero;
}

- (void)undoAction {
    [self.customUndoManager undo];
}

- (void)redoAction {
    [self.customUndoManager redo];
}
@end
两种实现方式的对比
实现方式 优点 缺点
使用 NSUndoManager 1. 框架提供,无需自己实现底层逻辑,开发效率高。
2. 功能强大,支持多种注册方式。
1. 学习成本较高,需要了解 NSInvocation 等概念。
2. 灵活性相对较低,受框架限制。
自定义撤销/重做系统 1. 灵活性高,可以根据具体需求进行定制。
2. 代码结构清晰,易于理解。
1. 需要自己实现底层逻辑,开发工作量大。
2. 可能存在稳定性问题,需要进行充分测试。
允许用户激活撤销/重做功能

为了让用户能够方便地使用撤销/重做功能,我们需要在界面上提供相应的按钮。以下是在 CanvasViewController 中添加撤销和重做按钮的示例代码:

- (void)viewDidLoad {
    [super viewDidLoad];

    UIBarButtonItem *undoButton = [[UIBarButtonItem alloc] initWithTitle:@"撤销" style:UIBarButtonItemStylePlain target:self action:@selector(undoAction)];
    UIBarButtonItem *redoButton = [[UIBarButtonItem alloc] initWithTitle:@"重做" style:UIBarButtonItemStylePlain target:self action:@selector(redoAction)];

    self.navigationItem.leftBarButtonItems = @[undoButton, redoButton];
}
总结

本文深入探讨了策略模式和命令模式。策略模式允许客户端使用相关算法的变体,通过将不同的策略封装在对象内部,改变对象的核心功能。命令模式将请求封装为对象,实现了操作和接收者的解耦,支持撤销/重做、排队和记录等功能。

在实现撤销/重做功能方面,我们介绍了使用 NSUndoManager 和自定义撤销/重做系统两种方法。使用 NSUndoManager 可以借助框架的强大功能,提高开发效率;而自定义撤销/重做系统则具有更高的灵活性,可以根据具体需求进行定制。

通过合理运用这些设计模式和实现方法,可以提高软件的可维护性、可扩展性和用户体验。在实际开发中,应根据项目的具体需求选择合适的方法。

以下是使用 mermaid 绘制的自定义撤销/重做系统的工作流程图:

graph LR
    A[执行操作] --> B[创建操作对象]
    B --> C[将操作对象压入撤销栈]
    C --> D[清空重做栈]
    E[撤销操作] --> F[从撤销栈弹出操作对象]
    F --> G[执行逆操作]
    G --> H[将逆操作对象压入重做栈]
    I[重做操作] --> J[从重做栈弹出操作对象]
    J --> K[执行操作]
    K --> L[将逆操作对象压入撤销栈]

通过这个流程图,可以更直观地理解自定义撤销/重做系统的工作原理。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值