命令模式与享元模式:设计模式的实践应用
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. 总结
命令模式和享元模式是两种不同但都非常实用的设计模式。命令模式通过将操作封装在命令对象中,实现了操作的执行、撤销和重做,使得系统的可维护性和灵活性得到提升。而享元模式则通过共享对象,减少了内存的使用,提高了系统的性能。
在实际开发中,我们可以根据具体的需求选择合适的设计模式。如果需要实现撤销/重做功能或者将操作与调用者解耦,那么命令模式是一个不错的选择;如果面临大量相同或相似对象的情况,为了节省资源和提高性能,享元模式则更为合适。通过合理运用这些设计模式,我们可以打造出更加高效、灵活和可维护的软件系统。
超级会员免费看

被折叠的 条评论
为什么被折叠?



