27、备忘录模式:Scribble对象的保存与恢复实现

备忘录模式:Scribble对象的保存与恢复实现

1. 恢复Scribble对象

在保存Scribble对象后,如何从文件系统中恢复它是一个关键问题。通常情况下,管理者会将之前保存的备忘录传递给发起者以恢复其先前的状态,但在Scribble的保存和加载策略中,我们采用了一种不将备忘录传递回发起者的方式。

当用户点击缩略图打开已保存的涂鸦时,调用该过程的对象可以是按钮或单独的命令对象。作为客户端的调用者会向ScribbleManager实例发送消息,提供必要信息以从存档(文件系统)中加载并返回一个Scribble对象。调用者并不知晓涂鸦的保存位置和方式。

ScribbleManager会从文件系统的预定义位置加载相应的NSData对象,然后将该数据传递给ScribbleMemento的类方法 mementoWithData:data 来创建一个ScribbleMemento实例。此时,我们不会将ScribbleMemento对象传递回创建它的原始Scribble对象,而是使用该ScribbleMemento对象创建一个全新的Scribble实例,因为我们假设原始Scribble对象在保存之前生命周期已经结束。

调用者获取到恢复状态的新Scribble实例后,会在应用程序中传递该实例以供进一步使用。例如,请求CanvasViewController用恢复的Scribble对象替换当前的Scribble对象(即打开已保存的涂鸦)。

下面是该过程的流程图:

graph LR
    A[用户点击缩略图] --> B[调用者向ScribbleManager发送消息]
    B --> C[ScribbleManager加载NSData对象]
    C --> D[ScribbleManager创建ScribbleMemento实例]
    D --> E[创建新的Scribble实例]
    E --> F[传递新Scribble实例给CanvasViewController]

2. 设计和实现ScribbleMemento

在备忘录模式中,备忘录对象应该为其发起者提供广泛的接口,而为其他对象(如管理者)提供狭窄的接口。广泛的接口提供了更多使用对象的选项和自由,通常在面向对象语言中声明为私有(操作或构造函数)和友元。在Objective - C中,由于所有内容都是公共的,我们可以使用类别(categories)来实现类似的效果。

我们为ScribbleMemento创建一个名为ScribbleMemento (Private)的类别,将私有(或友元)操作放在该类别声明中,这样这些操作仅对Scribble可用。ScribbleMemento的公共(狭窄)接口与ScribbleMemento (Private)类别中声明的内容分开。在Objective - C 2.0中,还可以使用匿名类别(扩展)来声明私有操作。

ScribbleMemento通过 mementoWithData:data data 方法提供其狭窄功能。 mementoWithData:data 方法允许其他类通过提供NSData对象来获取ScribbleMemento实例, data 方法则将ScribbleMemento对象以NSData形式返回。其他类不应看到私有类别中定义的狭窄接口。

以下是ScribbleMemento类的声明:

#import "Mark.h"

@interface ScribbleMemento : NSObject
{
  @private
  id <Mark> mark_;
  BOOL hasCompleteSnapshot_;
}

+ (ScribbleMemento *) mementoWithData:(NSData *)data;
- (NSData *) data;

@end

ScribbleMemento保存一个对私有Mark对象的引用作为Scribble的内部状态,还有一个BOOL类型的私有成员变量 hasCompleteSnapshot_ ,用于告知Scribble对象存储的Mark引用是完整快照还是只是其片段。

其扩展(私有类别)声明了仅应由Scribble对象使用的更广泛接口:

#import "Mark.h"
#import"ScribbleMemento.h"

@interface ScribbleMemento ()

- (id) initWithMark:(id <Mark>)aMark;

@property (nonatomic, copy) id <Mark> mark;
@property (nonatomic, assign) BOOL hasCompleteSnapshot;

@end

Scribble对象可以使用自己的内部Mark引用创建和初始化ScribbleMemento对象,并访问其 mark hasCompleteSnapshot 属性。

下面是ScribbleMemento的实现:

#import "ScribbleMemento.h"
#import "ScribbleMemento+Friend.h"

@implementation ScribbleMemento

@synthesize mark=mark_;
@synthesize hasCompleteSnapshot=hasCompleteSnapshot_;

- (NSData *) data
{
  NSData *data = [NSKeyedArchiver archivedDataWithRootObject:mark_];
  return data;
}

+ (ScribbleMemento *) mementoWithData:(NSData *)data
{
  // It raises an NSInvalidArchiveOperationException if data is not a valid archive
  id <Mark> retoredMark = (id <Mark>)[NSKeyedUnarchiver unarchiveObjectWithData:data];
  ScribbleMemento *memento = [[[ScribbleMemento alloc]  
                               initWithMark:retoredMark] autorelease];

  return memento;
}

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

#pragma mark -
#pragma mark Private methods

- (id) initWithMark:(id <Mark>)aMark
{
  if (self = [super init])
  {
    [self setMark:aMark];
  }

  return self;
}

@end

data 实例方法通过NSKeyedArchiver类获取自身的编码版本并返回, mementoWithData:(NSData *)data 类方法使用提供的NSData对象创建一个新的ScribbleMemento实例。

需要注意的是, mark 属性实际上是复制另一个Mark对象,而不仅仅是保留它。这是为了防止原始Mark对象在应用程序的其他部分被修改时,影响ScribbleMemento对象收集的状态。

3. 修改Scribble类

为了让Scribble和ScribbleMemento协同工作,我们需要对Scribble类进行一些修改。添加了另一个Mark引用 incrementalMark_ ,用于保存添加到 parentMark_ 的完整笔画或点的引用。

以下是修改后的Scribble类声明:

#import "Mark.h"
#import "ScribbleMemento.h"

@interface Scribble : NSObject
{
  @private
  id <Mark> parentMark_;
  id <Mark> incrementalMark_;
}

// methods for Mark management
- (void) addMark:(id <Mark>)aMark shouldAddToPreviousMark:(BOOL)shouldAddToPreviousMark;
- (void) removeMark:(id <Mark>)aMark;

// methods for memento
- (id) initWithMemento:(ScribbleMemento *)aMemento;
+ (Scribble *) scribbleWithMemento:(ScribbleMemento *)aMemento;
- (ScribbleMemento *) scribbleMemento;
- (ScribbleMemento *) scribbleMementoWithCompleteSnapshot:(BOOL)hasCompleteSnapshot;
- (void) attachStateFromMemento:(ScribbleMemento *)memento;

@end

Scribble类还包含了一些专门用于与ScribbleMemento对象配合使用的方法。

以下是修改后的Scribble类实现:

#import "ScribbleMemento+Friend.h"
#import "Scribble.h"
#import "Stroke.h"

// A private category for Scribble
// that contains a mark property available
// only to its objects
@interface Scribble ()

@property (nonatomic, retain) id <Mark> mark;

@end


@implementation Scribble

@synthesize mark=parentMark_;

- (id) init
{
  if (self = [super init])
  {
    // the parent should be a composite
    // object (i.e., Stroke)
    parentMark_ = [[Stroke alloc] init];
  }

  return self;
}

#pragma mark -
#pragma mark Methods for Mark management

- (void) addMark:(id <Mark>)aMark shouldAddToPreviousMark:(BOOL)shouldAddToPreviousMark
{
  // manual KVO invocation
  [self willChangeValueForKey:@"mark"];

  // if the flag is set to YES
  // then add this aMark to the  
  // *PREVIOUS*Mark as part of an
  // aggregate.
  // Based on our design, it's supposed
  // to be the last child of the main
  if (shouldAddToPreviousMark)
  {
    [[parentMark_ lastChild] addMark:aMark];
  }
  // otherwise attach it to the parent
  else  
  {
    [parentMark_ addMark:aMark];
    incrementalMark_ = aMark;
  }

  // manual KVO invocation
  [self didChangeValueForKey:@"mark"];
}

- (void) removeMark:(id <Mark>)aMark
{   
  // do nothing if aMark is the parent
  if (aMark == parentMark_) return;

  // manual KVO invocation
  [self willChangeValueForKey:@"mark"];

  [parentMark_ removeMark:aMark];

  // we don't need to keep the  
  // incrementalMark_ reference
  // as it's just removed in the parent
  if (aMark == incrementalMark_)
  {
    incrementalMark_ = nil;
  }

  // manual KVO invocation
  [self didChangeValueForKey:@"mark"];
}

#pragma mark -
#pragma mark Methods for memento

- (id) initWithMemento:(ScribbleMemento*)aMemento
{
  if (self = [super init])
  {
    if ([aMemento hasCompleteSnapshot])
    {
      [self setMark:[aMemento mark]];
    }
    else  
    {
      // if the memento contains only
      // incremental mark, then we need to
      // create a parent Stroke object to  
      // hold it
      parentMark_ = [[Stroke alloc] init];
      [self attachStateFromMemento:aMemento];
    }
  }

  return self;
}

- (void) attachStateFromMemento:(ScribbleMemento *)memento
{
  // attach any mark from a memento object
  // to the main parent
  [self addMark:[memento mark] shouldAddToPreviousMark:NO];
}

- (ScribbleMemento *) scribbleMementoWithCompleteSnapshot:(BOOL)hasCompleteSnapshot
{
  id <Mark> mementoMark = incrementalMark_;

  // if the resulting memento asks
  // for a complete snapshot, then  
  // set it with parentMark_
  if (hasCompleteSnapshot)
  {
    mementoMark = parentMark_;
  }
  // but if incrementalMark_
  // is nil then we can't do anything
  // but bail out
  else if (mementoMark == nil)
  {
    return nil;
  }

  ScribbleMemento *memento = [[[ScribbleMemento alloc]  
                               initWithMark:mementoMark] autorelease];
  [memento setHasCompleteSnapshot:hasCompleteSnapshot];

  return memento;
}

- (ScribbleMemento *) scribbleMemento
{
  return [self scribbleMementoWithCompleteSnapshot:YES];
}

+ (Scribble *) scribbleWithMemento:(ScribbleMemento *)aMemento
{
  Scribble *scribble = [[[Scribble alloc] initWithMemento:aMemento] autorelease];
  return scribble;
}


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

@end

addMark:shouldAddToPreviousMark: 方法中,当 aMark 直接附加到主父标记时,会将其保存为 incrementalMark_ 。如果删除的Mark对象也被 incrementalMark_ 引用,则将 incrementalMark_ 设置为 nil ,以防止应用程序崩溃。

initWithMemento: 方法允许Scribble对象使用ScribbleMemento对象初始化自身,恢复其状态。 attachStateFromMemento: 方法将ScribbleMemento对象中的Mark对象添加到主父标记。 scribbleMementoWithCompleteSnapshot: 方法根据 hasCompleteSnapshot 参数创建ScribbleMemento实例, scribbleMemento 是一个便捷方法,使用 scribbleMementoWithCompleteSnapshot: 方法并将参数设置为 YES scribbleWithMemento: 是一个类方法,使用ScribbleMemento对象创建一个新的Scribble对象。

4. 练习

我们已经看到使用 incrementalMark_ 来保存添加到 parentMark_ 的特定Mark对象的引用。Scribble对象可以使用 incrementalMark_ 创建ScribbleMemento对象,以保存仅添加到其内部状态的特定更改。那么如何修改上述实现,使Scribble对象还能保存从父对象中移除的Mark对象呢?

5. 结合管理者整合所有内容

我们还缺少管理者管理ScribbleMemento对象的部分。我们可以使用ScribbleManager来扮演管理者的角色,它提供了 saveScribble: 方法供客户端保存任何Scribble对象,还提供了 scribbleAtIndex: 方法,通过索引加载并返回特定的Scribble对象。

以下是 saveScribble: 方法的简化代码:

// get a memento from the scribble
ScribbleMemento *scribbleMemento = [scribble scribbleMemento];

// get an NSData object from the memento
// so we can use it to save itself in the
// file system
NSData *mementoData = [scribbleMemento data];

NSString *mementoPath;
// ...  
// construct the path for saving
// the memento and perform any other
// operations before actually saving it
// in the file system
// ...
[mementoData writeToFile:mementoPath atomically:YES];

首先,我们通过Scribble对象的 scribbleMemento 方法获取ScribbleMemento实例,然后将其转换为NSData对象,最后将NSData对象保存到文件系统中。

以下是 scribbleAtIndex: 方法的简化代码:

NSString *scribbleMementoPath;

// ...
// use the provided index to retrieve
// the path that was used for saving
// the memento before. We will use the
// path to load the memento later
// ...

// use NSFileManager to load the memento file
// as NSData with the path that we just reconstructed
NSFileManager *fileManager = [NSFileManager defaultManager];
NSData *scribbleMementoData = [fileManager contentsAtPath:scribbleMementoPath];

// we create a ScribbleMemento from
// the NSData object. Then we use the  
// memento to resurrect a Scribble object
// based on what's saved in the memento
ScribbleMemento *scribbleMemento = [ScribbleMemento  
                                    mementoWithData:scribbleMementoData];
Scribble *resurrectedScribble = [Scribble scribbleWithMemento:scribbleMemento];

首先,使用提供的索引定位保存数据文件的路径,然后加载NSData对象,创建ScribbleMemento实例,最后使用该实例恢复Scribble对象。

6. Cocoa Touch框架中的备忘录模式

Cocoa Touch框架通过归档、属性列表序列化和核心数据采用了备忘录模式。这里我们主要关注归档,它将对象及其属性和与其他对象的关系编码为可以存储在文件系统中或在进程之间或网络上传输的存档。

归档过程将对象图捕获为与架构无关的字节流,保留对象的身份和它们之间的关系,同时也存储对象的类型。从字节流解码的对象通常使用与原始编码对象相同的类实例化。

在归档和反归档过程中,使用NSCoder对象进行编码和解码操作。NSCoder本身是一个抽象类,Apple建议使用NSKeyedArchiver和NSKeyedUnarchiver类的键控归档技术。被编码和解码的对象必须遵循NSCoding协议并实现以下方法:

- (id)initWithCoder:(NSCoder *)coder;
- (void)encodeWithCoder:(NSCoder *)coder;

在ScribbleMemento的示例中,我们使用NSKeyedArchiver和NSKeyedUnarchiver类实现了归档和反归档过程,被编码的对象是Mark复合对象。为了让NSKeyedArchiver和NSKeyedUnarchiver正常工作,所有Mark类都必须遵循NSCoding协议并实现其所需的方法。

以下是Mark协议采用NSCoding的声明:

@protocol Mark <NSObject, NSCopying, NSCoding>

@property (nonatomic, retain) UIColor *color;
@property (nonatomic, assign) CGFloat size;
@property (nonatomic, assign) CGPoint location;
@property (nonatomic, readonly) NSUInteger count;
@property (nonatomic, readonly) id <Mark> lastChild;

- (id) copy;
- (void) addMark:(id <Mark>) mark;
- (void) removeMark:(id <Mark>) mark;
- (id <Mark>) childMarkAtIndex:(NSUInteger) index;

// some other methods defined in other chapters
@end

以下是Vertex类实现NSCoding方法的代码:

- (id)initWithCoder:(NSCoder *)coder
{
  if (self = [super init])
  {
    location_ = [(NSValue *)[coder decodeObjectForKey:@"VertexLocation"] CGPointValue];
  }
  return self;
}

- (void)encodeWithCoder:(NSCoder *)coder
{
  [coder encodeObject:[NSValue valueWithCGPoint:location_] forKey:@"VertexLocation"];
}

在Vertex类中, encodeWithCoder: 方法在保存对象时,使用 encodeObject: 消息将 location_ 属性编码为键 @"VertexLocation" initWithCoder: 方法使用相同的键解码该属性。

Dot类的实现类似,但需要处理更多的属性:

- (id)initWithCoder:(NSCoder *)coder
{
  if (self = [super initWithCoder:coder])
  {
    color_ = [[coder decodeObjectForKey:@"DotColor"] retain];
    size_ = [coder decodeFloatForKey:@"DotSize"];
  }
}

通过以上步骤,我们详细介绍了Scribble对象的保存和恢复过程,以及如何在Cocoa Touch框架中应用备忘录模式进行对象的归档和反归档操作。通过合理设计和实现ScribbleMemento和Scribble类,我们可以有效地管理Scribble对象的状态,并在需要时进行恢复。同时,遵循NSCoding协议可以确保对象在归档和反归档过程中的正确性。

7. 操作步骤总结

为了更清晰地展示整个过程,下面将关键操作步骤进行总结:

7.1 保存Scribble对象
  1. 获取Scribble对象的ScribbleMemento实例:
ScribbleMemento *scribbleMemento = [scribble scribbleMemento];
  1. 将ScribbleMemento实例转换为NSData对象:
NSData *mementoData = [scribbleMemento data];
  1. 构造保存路径并将NSData对象保存到文件系统:
NSString *mementoPath;
// ... 构造路径及其他操作
[mementoData writeToFile:mementoPath atomically:YES];
7.2 恢复Scribble对象
  1. 根据索引获取保存ScribbleMemento的路径:
NSString *scribbleMementoPath;
// ... 使用索引获取路径
  1. 使用NSFileManager加载NSData对象:
NSFileManager *fileManager = [NSFileManager defaultManager];
NSData *scribbleMementoData = [fileManager contentsAtPath:scribbleMementoPath];
  1. 根据NSData对象创建ScribbleMemento实例:
ScribbleMemento *scribbleMemento = [ScribbleMemento mementoWithData:scribbleMementoData];
  1. 使用ScribbleMemento实例恢复Scribble对象:
Scribble *resurrectedScribble = [Scribble scribbleWithMemento:scribbleMemento];

8. 表格对比

下面通过表格对比ScribbleMemento和Scribble类的关键方法及其功能:
| 类名 | 方法名 | 功能描述 |
| ---- | ---- | ---- |
| ScribbleMemento | mementoWithData:data | 根据NSData对象创建ScribbleMemento实例 |
| ScribbleMemento | data | 将ScribbleMemento对象转换为NSData对象 |
| ScribbleMemento | initWithMark: | 使用Mark对象初始化ScribbleMemento实例 |
| Scribble | initWithMemento: | 使用ScribbleMemento对象初始化Scribble实例 |
| Scribble | scribbleWithMemento: | 根据ScribbleMemento对象创建新的Scribble对象 |
| Scribble | scribbleMemento | 获取Scribble对象的ScribbleMemento实例(完整快照) |
| Scribble | scribbleMementoWithCompleteSnapshot: | 根据是否需要完整快照创建ScribbleMemento实例 |
| Scribble | attachStateFromMemento: | 将ScribbleMemento对象中的Mark对象添加到Scribble的主父标记 |

9. 流程图回顾与扩展

之前我们展示了恢复Scribble对象的流程图,下面再给出一个包含保存和恢复过程的完整流程图:

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px

    A([开始]):::startend --> B{操作类型}:::decision
    B -->|保存| C(获取ScribbleMemento实例):::process
    C --> D(转换为NSData对象):::process
    D --> E(构造路径并保存到文件系统):::process
    E --> F([结束]):::startend
    B -->|恢复| G(根据索引获取保存路径):::process
    G --> H(加载NSData对象):::process
    H --> I(创建ScribbleMemento实例):::process
    I --> J(恢复Scribble对象):::process
    J --> F

10. 总结与注意事项

通过上述内容,我们深入了解了如何使用备忘录模式来管理Scribble对象的状态,包括保存和恢复操作,以及在Cocoa Touch框架中如何进行对象的归档和反归档。在实际应用中,需要注意以下几点:
- Mark属性的复制 :ScribbleMemento的 mark 属性采用复制而非保留,以避免原始Mark对象被修改影响ScribbleMemento的状态。
- NSCoding协议的遵循 :所有参与归档和反归档的对象都必须遵循NSCoding协议,并正确实现 initWithCoder: encodeWithCoder: 方法。
- incrementalMark_ 的处理 :在删除Mark对象时,若该对象被 incrementalMark_ 引用,需将 incrementalMark_ 置为 nil ,防止应用崩溃。

通过合理运用这些技术和注意事项,我们可以高效、稳定地管理Scribble对象的状态,为应用程序的开发提供有力支持。同时,对于练习中提出的问题,开发者可以根据实际需求进一步探索和修改代码,以实现保存从父对象中移除的Mark对象的功能。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值