24、命令模式与享元模式:设计模式的实践应用

命令模式与享元模式:设计模式的实践应用

1. 自制绘图/撤销绘图基础设施

在软件开发中,实现撤销和重做操作是一项常见需求。传统方法是通过遍历命令历史列表来执行撤销或重做操作,可使用索引跟踪列表中的当前命令对象,但这种方法较为复杂且容易出错。

更简单、不易出错的方法是使用两个栈,一个用于撤销(undo stack),另一个用于重做(redo stack),就像 NSUndoManager 那样。执行的命令对象会被压入撤销栈,栈顶始终是最后执行的命令。当应用需要撤销最后一次执行时,会从撤销栈中弹出最后一个命令并执行撤销操作,完成后将该命令压入重做栈。重做操作则相反。

graph LR
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    A(执行命令):::process --> B(压入撤销栈):::process
    B --> C{是否需要撤销?}:::process
    C -- 是 --> D(从撤销栈弹出):::process
    D --> E(执行撤销操作):::process
    E --> F(压入重做栈):::process
    C -- 否 --> G(继续执行新命令):::process
    F --> H{是否需要重做?}:::process
    H -- 是 --> I(从重做栈弹出):::process
    I --> J(执行重做操作):::process
    J --> K(压入撤销栈):::process
    H -- 否 --> G
2. 设计命令基础设施

以 TouchPainter 应用为例,当用户触摸画布时,会出现笔触或点。需要将“绘图”操作封装为命令对象,即 DrawScribbleCommand Command DrawScribbleCommand 的抽象父类,声明了 execute undo 抽象操作,还有一个 userInfo 属性,允许客户端为命令对象提供额外参数。

以下是相关代码实现:

// Command.h
@interface Command : NSObject
{
  @protected
  NSDictionary *userInfo_;
}

@property (nonatomic, retain) NSDictionary *userInfo;

- (void) execute;
- (void) undo;

@end

// Command.m
#import "Command.h"

@implementation Command
@synthesize userInfo=userInfo_;

- (void) execute
{
  // should throw an exception.
}

- (void) undo
{
  // do nothing
  // subclasses need to override this
  // method to perform actual undo.
}

- (void) dealloc
{
  [userInfo_ release];
  [super dealloc];
}

@end

// DrawScribbleCommand.h
#import "Command.h"
#import "Scribble.h"

@interface DrawScribbleCommand : Command
{
  @private
  Scribble *scribble_;
  id <Mark> mark_;
  BOOL shouldAddToPreviousMark_;
}

- (void) execute;
- (void) undo;

@end

// DrawScribbleCommand.m
#import "DrawScribbleCommand.h"

@implementation DrawScribbleCommand

- (void) execute
{
  if (!userInfo_) return;

  scribble_ = [userInfo_ objectForKey:ScribbleObjectUserInfoKey];
  mark_ = [userInfo_ objectForKey:MarkObjectUserInfoKey];
  shouldAddToPreviousMark_ = [(NSNumber *)[userInfo_  
                                           objectForKey:AddToPreviousMarkUserInfoKey]
                               boolValue];

  [scribble_ addMark:mark_ shouldAddToPreviousMark:shouldAddToPreviousMark_];

}

- (void) undo
{
  [scribble_ removeMark:mark_];
}

@end
3. 修改 CanvasViewController 以处理命令

CanvasViewController 中有几个方法用于管理绘图命令对象:
- executeCommand:prepareForUndo: :负责执行传入的命令对象,并将其压入撤销栈,同时维护撤销栈的大小。
- undoCommand :从撤销栈中弹出最后一个命令,执行撤销操作,然后将该命令压入重做栈。
- redoCommand :从重做栈中弹出最后一个命令,执行重做操作,然后将该命令压回撤销栈。

// CanvasViewController.m
#pragma mark -
#pragma mark Draw Scribble Command Methods

- (void) executeCommand:(Command *)command
         prepareForUndo:(BOOL)prepareForUndo
{
  if (prepareForUndo)
  {
    // lazy-load undoStack_
    if (undoStack_ == nil)
    {
      undoStack_ = [[NSMutableArray alloc] initWithCapacity:levelsOfUndo_];
    }

    // drop the bottom one if the  
    // undo stack is full
    if ([undoStack_ count] == levelsOfUndo_)
    {
      [undoStack_ dropBottom];
    }

    // push the command
    // to our undo stack
    [undoStack_ push:command];
  }

  [command execute];
}

- (void) undoCommand
{
  Command *command = [undoStack_ pop];  
  [command undo];

  // push the command to the redo stack
  if (redoStack_ == nil)
  {
    redoStack_ = [[NSMutableArray alloc] initWithCapacity:levelsOfUndo_];
  }

  [redoStack_ push:command];
}

- (void) redoCommand
{
  Command *command = [redoStack_ pop];  
  [command execute];

  // push the command back to the undo stack
  [undoStack_ push:command];
}
4. 连接绘图命令与实际绘图事件

通过 CanvasViewController 中的触摸事件处理程序,将 DrawScribbleCommand 对象与实际绘图事件连接起来。在 touchesMoved touchesEnded 方法中,会创建 DrawScribbleCommand 对象,并将其传递给 executeCommand:prepareForUndo: 方法执行。

// CanvasViewController.m
#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];

    NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
                              scribble_, ScribbleObjectUserInfoKey,  
                              newStroke, MarkObjectUserInfoKey,  
                              [NSNumber numberWithBool:NO],  
                              AddToPreviousMarkUserInfoKey, nil];
    DrawScribbleCommand *command = [[[DrawScribbleCommand alloc] init] autorelease];
    [command setUserInfo:userInfo];
    [self executeCommand:command prepareForUndo:YES];
  }

  // add the current touch as another vertex to the
  // temp stroke
  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 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];

    NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
                              scribble_, ScribbleObjectUserInfoKey,  
                              singleDot, MarkObjectUserInfoKey,  
                              [NSNumber numberWithBool:NO],  
                              AddToPreviousMarkUserInfoKey, nil];
    DrawScribbleCommand *command = [[[DrawScribbleCommand alloc] init] autorelease];
    [command setUserInfo:userInfo];
    [self executeCommand:command prepareForUndo:YES];

  }
  // 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;
}
5. 允许用户激活撤销/重做操作

在 TouchPainter 应用的主画布视图中,工具栏右侧有撤销和重做按钮。通过 onBarButtonHit: 方法捕获用户点击事件,根据按钮的标签判断是执行撤销还是重做操作。

// CanvasViewController.m
#pragma mark -
#pragma mark Toolbar button hit method

- (IBAction) onBarButtonHit:(id)button
{
  UIBarButtonItem *barButton = button;

  if ([barButton tag] == 4)
  {
    [self undoCommand];
  }
  else if ([barButton tag] == 5)
  {
    [self redoCommand];
  }
}
6. 命令模式的其他用途

命令模式不仅适用于实现撤销/重做基础设施,还可用于延迟执行调用者中的操作。调用者可以是菜单项或按钮,使用命令对象可以将不同对象之间的操作连接起来,隐藏操作的具体细节。

7. 享元模式简介

在面向对象软件设计中,共享公共对象有时不仅能节省资源,还能提高性能。享元模式专注于设计可共享的对象,其关键组件包括可共享的享元对象和维护这些对象的对象池。通常会使用工厂(或管理器)来维护对象池并返回合适的实例。

组件 描述
可共享的享元对象 包含不用于标识对象的内在信息
对象池 存储可共享的享元对象
工厂/管理器 维护对象池并返回合适的实例
8. 享元对象的特点

享元对象的“轻量级”并非指其大小,而是指通过共享节省的大量空间。对象的部分(或大部分)唯一状态(外在状态)可以被提取出来并在其他地方管理,其余部分则进行共享。通过精心设计,可显著节省内存,在 iOS 开发中,节省内存意味着提升整体性能。

9. 享元模式的类图结构

Flyweight 是两个具体享元类 ConcreteFlyweight1 ConcreteFlyweight2 的父接口(协议)。每个具体享元类维护自己的 intrinsicState Flyweight 声明了 operation:extrinsicState 方法,由具体享元类实现。客户端通过提供 extrinsicState 让享元对象完成任务。

classDiagram
    class Flyweight {
        +operation(extrinsicState)
    }
    class ConcreteFlyweight1 {
        -intrinsicState
        +operation(extrinsicState)
    }
    class ConcreteFlyweight2 {
        -intrinsicState
        +operation(extrinsicState)
    }
    class FlyweightFactory {
        +getFlyweight(type)
    }
    Flyweight <|.. ConcreteFlyweight1
    Flyweight <|.. ConcreteFlyweight2
    FlyweightFactory --> Flyweight

综上所述,命令模式和享元模式在软件开发中都有重要应用。命令模式通过封装操作实现撤销和重做功能,提高了软件的可维护性和灵活性;享元模式通过共享对象节省资源,提升了软件的性能。开发者可根据具体需求合理运用这些设计模式,优化软件设计。

命令模式与享元模式:设计模式的实践应用

10. 享元模式的运行机制

在运行时, FlyweightFactory 负责管理享元对象池。通过调用其工厂方法,根据不同的类型返回相应的具体享元对象。以下是一个简单的流程图展示其运行机制:

graph LR
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    A(客户端请求享元对象):::process --> B(FlyweightFactory):::process
    B --> C{对象池是否存在该类型对象?}:::process
    C -- 是 --> D(从对象池取出对象):::process
    C -- 否 --> E(创建新的享元对象):::process
    E --> F(将新对象放入对象池):::process
    D --> G(返回享元对象给客户端):::process
    F --> G
11. 享元模式的代码实现示例

以下是一个简单的享元模式代码实现示例,假设我们有一个展示花朵图像的应用,通过享元模式只使用六个独特的实例来显示数百个花朵图像。

// Flyweight.h
@protocol Flyweight <NSObject>
- (void)displayWithExtrinsicState:(NSDictionary *)extrinsicState;
@end

// ConcreteFlyweight.h
#import "Flyweight.h"

@interface ConcreteFlyweight : NSObject <Flyweight>
- (instancetype)initWithIntrinsicState:(NSDictionary *)intrinsicState;
@end

// ConcreteFlyweight.m
#import "ConcreteFlyweight.h"

@interface ConcreteFlyweight ()
@property (nonatomic, strong) NSDictionary *intrinsicState;
@end

@implementation ConcreteFlyweight

- (instancetype)initWithIntrinsicState:(NSDictionary *)intrinsicState {
    self = [super init];
    if (self) {
        self.intrinsicState = intrinsicState;
    }
    return self;
}

- (void)displayWithExtrinsicState:(NSDictionary *)extrinsicState {
    // 结合内在状态和外在状态进行显示操作
    NSLog(@"Displaying with intrinsic: %@ and extrinsic: %@", self.intrinsicState, extrinsicState);
}

@end

// FlyweightFactory.h
#import "Flyweight.h"

@interface FlyweightFactory : NSObject
- (id<Flyweight>)getFlyweightWithType:(NSString *)type;
@end

// FlyweightFactory.m
#import "FlyweightFactory.h"
#import "ConcreteFlyweight.h"

@interface FlyweightFactory ()
@property (nonatomic, strong) NSMutableDictionary *flyweightPool;
@end

@implementation FlyweightFactory

- (instancetype)init {
    self = [super init];
    if (self) {
        self.flyweightPool = [NSMutableDictionary dictionary];
    }
    return self;
}

- (id<Flyweight>)getFlyweightWithType:(NSString *)type {
    id<Flyweight> flyweight = self.flyweightPool[type];
    if (!flyweight) {
        // 假设这里根据类型创建不同的内在状态
        NSDictionary *intrinsicState = @{@"type": type};
        flyweight = [[ConcreteFlyweight alloc] initWithIntrinsicState:intrinsicState];
        self.flyweightPool[type] = flyweight;
    }
    return flyweight;
}

@end
12. 使用享元模式的步骤

使用享元模式可以按照以下步骤进行:
1. 定义享元接口 :如上述代码中的 Flyweight 协议,声明享元对象需要实现的方法。
2. 实现具体享元类 :如 ConcreteFlyweight 类,包含内在状态并实现享元接口的方法。
3. 创建享元工厂 :如 FlyweightFactory 类,负责维护对象池并返回享元对象。
4. 客户端请求享元对象 :客户端通过工厂获取享元对象,并提供外在状态。

13. 命令模式与享元模式的对比
模式 主要目的 关键组件 应用场景
命令模式 封装可执行指令,实现撤销/重做等操作 命令对象、调用者、接收者、客户端 需要撤销/重做操作、延迟执行操作的场景
享元模式 设计可共享对象,节省资源和提高性能 可共享的享元对象、对象池、工厂/管理器 需要大量相同或相似对象的场景
14. 总结

命令模式和享元模式是两种不同但都非常实用的设计模式。命令模式通过将操作封装在命令对象中,实现了操作的执行、撤销和重做,使得系统的可维护性和灵活性得到提升。而享元模式则通过共享对象,减少了内存的使用,提高了系统的性能。

在实际开发中,我们可以根据具体的需求选择合适的设计模式。如果需要实现撤销/重做功能或者将操作与调用者解耦,那么命令模式是一个不错的选择;如果面临大量相同或相似对象的情况,为了节省资源和提高性能,享元模式则更为合适。通过合理运用这些设计模式,我们可以打造出更加高效、灵活和可维护的软件系统。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值