MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C 中的备忘录模式应用与场景

2024-05-127.8k 阅读

备忘录模式基础概念

备忘录模式定义

备忘录模式(Memento Pattern)是一种行为设计模式,它允许在不破坏对象封装性的前提下,捕获并保存对象内部状态,以便在将来某个时刻恢复到该状态。简单来说,就是可以对一个对象进行备份,在需要的时候恢复到备份时的状态。

备忘录模式结构

  1. Originator(原发器):需要被备份状态的对象,它负责创建备忘录,并可以使用备忘录恢复自身状态。例如,在游戏中,玩家角色(原发器)的生命值、装备等状态需要被备份。
  2. Memento(备忘录):用于存储原发器的内部状态,它提供了原发器恢复状态所需的信息。就像游戏存档文件,记录了玩家角色某一时刻的所有状态数据。
  3. Caretaker(负责人):负责管理备忘录,它不直接操作或检查备忘录的内容,只负责保存和提供备忘录。类似于游戏的存档管理系统,负责存档和读取存档。

Objective-C 中实现备忘录模式

实现原发器

在 Objective-C 中,我们首先定义原发器类。假设我们有一个简单的 GameCharacter 类,代表游戏角色,该角色有生命值和攻击力两个属性,这两个属性就是我们需要备份的状态。

#import <Foundation/Foundation.h>

@interface GameCharacter : NSObject

@property (nonatomic, assign) NSInteger health;
@property (nonatomic, assign) NSInteger attackPower;

// 创建备忘录
- (id)createMemento;
// 使用备忘录恢复状态
- (void)restoreFromMemento:(id)memento;

@end

@implementation GameCharacter

- (id)createMemento {
    NSMutableDictionary *mementoDict = [NSMutableDictionary dictionary];
    mementoDict[@"health"] = @(self.health);
    mementoDict[@"attackPower"] = @(self.attackPower);
    return mementoDict;
}

- (void)restoreFromMemento:(id)memento {
    if ([memento isKindOfClass:[NSMutableDictionary class]]) {
        self.health = [((NSMutableDictionary *)memento)[@"health"] integerValue];
        self.attackPower = [((NSMutableDictionary *)memento)[@"attackPower"] integerValue];
    }
}

@end

实现备忘录

在上面的代码中,我们用 NSMutableDictionary 来充当备忘录。虽然这不是一个专门的备忘录类,但在这种简单场景下,它可以有效地存储原发器的状态。如果需要更正式的备忘录类,可以如下定义:

#import <Foundation/Foundation.h>

@interface GameCharacterMemento : NSObject

@property (nonatomic, strong) NSDictionary *stateDict;

- (instancetype)initWithStateDict:(NSDictionary *)dict;

@end

@implementation GameCharacterMemento

- (instancetype)initWithStateDict:(NSDictionary *)dict {
    self = [super init];
    if (self) {
        self.stateDict = dict;
    }
    return self;
}

@end

然后修改 GameCharacter 类的 createMementorestoreFromMemento 方法:

#import <Foundation/Foundation.h>
#import "GameCharacterMemento.h"

@interface GameCharacter : NSObject

@property (nonatomic, assign) NSInteger health;
@property (nonatomic, assign) NSInteger attackPower;

// 创建备忘录
- (GameCharacterMemento *)createMemento;
// 使用备忘录恢复状态
- (void)restoreFromMemento:(GameCharacterMemento *)memento;

@end

@implementation GameCharacter

- (GameCharacterMemento *)createMemento {
    NSMutableDictionary *mementoDict = [NSMutableDictionary dictionary];
    mementoDict[@"health"] = @(self.health);
    mementoDict[@"attackPower"] = @(self.attackPower);
    return [[GameCharacterMemento alloc] initWithStateDict:mementoDict];
}

- (void)restoreFromMemento:(GameCharacterMemento *)memento {
    if ([memento isKindOfClass:[GameCharacterMemento class]]) {
        self.health = [((NSDictionary *)memento.stateDict)[@"health"] integerValue];
        self.attackPower = [((NSDictionary *)memento.stateDict)[@"attackPower"] integerValue];
    }
}

@end

实现负责人

接下来定义负责人类,负责管理备忘录。

#import <Foundation/Foundation.h>
#import "GameCharacterMemento.h"

@interface GameSaveManager : NSObject

@property (nonatomic, strong) GameCharacterMemento *memento;

- (void)saveGame:(GameCharacterMemento *)memento;
- (GameCharacterMemento *)loadGame;

@end

@implementation GameSaveManager

- (void)saveGame:(GameCharacterMemento *)memento {
    self.memento = memento;
}

- (GameCharacterMemento *)loadGame {
    return self.memento;
}

@end

使用示例

下面是一个使用上述代码的示例:

#import <Foundation/Foundation.h>
#import "GameCharacter.h"
#import "GameSaveManager.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        GameCharacter *character = [[GameCharacter alloc] init];
        character.health = 100;
        character.attackPower = 20;

        GameSaveManager *saveManager = [[GameSaveManager alloc] init];
        GameCharacterMemento *memento = [character createMemento];
        [saveManager saveGame:memento];

        // 模拟角色受伤和升级
        character.health -= 30;
        character.attackPower += 5;

        NSLog(@"当前角色状态:生命值 %ld,攻击力 %ld", (long)character.health, (long)character.attackPower);

        // 恢复到之前的状态
        GameCharacterMemento *loadedMemento = [saveManager loadGame];
        [character restoreFromMemento:loadedMemento];

        NSLog(@"恢复后的角色状态:生命值 %ld,攻击力 %ld", (long)character.health, (long)character.attackPower);
    }
    return 0;
}

备忘录模式在 Objective-C 应用场景

游戏开发场景

  1. 游戏存档与读档:如前面提到的游戏角色状态备份。在角色扮演游戏中,玩家的角色可能有复杂的状态,包括生命值、魔法值、经验值、装备、技能等。通过备忘录模式,可以方便地对这些状态进行存档。当玩家想要读取存档时,利用保存的备忘录恢复角色状态。例如,在《塞尔达传说》系列游戏中,玩家可以在游戏中的特定地点进行存档,之后可以从存档点读取游戏进度,恢复游戏角色的各种状态,这背后就可以使用备忘录模式来实现。
  2. 关卡进度保存:对于关卡制游戏,每通过一个关卡,游戏状态需要被保存,包括关卡内的道具收集情况、敌人击杀进度等。当玩家因为各种原因(如退出游戏、游戏崩溃)需要重新进入关卡时,可以通过备忘录恢复到之前的关卡进度。比如《植物大战僵尸》,玩家在闯关过程中,游戏会保存当前关卡的植物种植情况、僵尸出现进度等信息,以便玩家再次进入关卡时能继续游戏。

软件撤销与重做功能

  1. 文本编辑软件:在文本编辑应用中,用户的每一步操作(如插入文字、删除文字、修改格式等)都可以看作是对文档状态的一次改变。使用备忘录模式,可以在每次操作后创建一个备忘录,记录文档的当前状态。当用户点击“撤销”按钮时,就从备忘录中恢复到上一步的文档状态;当点击“重做”按钮时,又可以从后续的备忘录中恢复到之后的状态。以苹果的 TextEdit 软件为例,用户在输入文字和进行格式调整过程中,能够随时撤销或重做操作,这一功能可以通过备忘录模式来实现底层逻辑。
  2. 图形设计软件:在图形设计软件如 Sketch 或 Adobe Illustrator 中,用户对图形的绘制、移动、缩放、填充颜色等操作也可以利用备忘录模式来实现撤销和重做功能。每一次对图形的操作都会改变图形对象的状态,通过保存这些状态的备忘录,软件可以实现高效的撤销和重做功能,方便设计师进行创作。

数据库事务管理

  1. 本地数据库操作:在 iOS 应用开发中,经常会使用 SQLite 等本地数据库。当进行一系列数据库操作(如插入多条记录、更新数据等)时,为了保证数据的一致性和完整性,可以使用备忘录模式。在操作开始前,创建一个备忘录记录数据库的当前状态。如果在操作过程中出现错误,就可以利用备忘录将数据库恢复到操作前的状态,实现事务回滚。例如,在一个记账应用中,当用户进行一笔复杂的账目记录操作,涉及到多个表的插入和更新时,如果出现错误,使用备忘录模式可以确保数据库不会处于不一致的状态。
  2. 远程数据库同步:在与远程数据库进行数据同步时,也可以应用备忘录模式。在同步开始前,记录本地数据库的状态作为备忘录。如果同步过程中出现网络故障或其他问题导致同步失败,可以根据备忘录恢复到同步前的状态,避免数据混乱。例如,在一个企业级的移动办公应用中,员工在本地创建或修改的数据需要同步到远程服务器数据库,使用备忘录模式可以保障同步过程中的数据可靠性。

多版本管理

  1. 文件版本管理:在一些文档管理应用中,用户可能希望保存文件的多个版本。每次对文件进行重大修改时,可以创建一个备忘录记录文件的当前状态。这样,用户可以随时查看或恢复到之前的文件版本。例如,在 Google Docs 中,用户可以查看文档的修订历史,并可以恢复到特定的历史版本,这类似于备忘录模式的应用,只不过 Google Docs 可能使用更复杂的分布式存储和版本控制技术,但基本概念是相通的。
  2. 项目代码版本管理:虽然像 Git 这样的版本控制系统使用了更复杂的分布式版本控制算法,但从概念上讲,也可以看作是备忘录模式的一种大规模应用。每次提交代码时,相当于创建了一个备忘录,记录了项目代码的当前状态。开发人员可以在需要时切换到特定的提交版本,恢复项目代码到当时的状态。在 iOS 项目开发中,团队使用 Git 进行版本管理,每个提交就如同一个备忘录,方便团队成员协作开发和处理代码回溯等问题。

备忘录模式在 Objective-C 中的优势

保持对象封装性

  1. 原发器状态保护:在 Objective-C 中,通过备忘录模式,原发器类(如 GameCharacter)可以控制哪些状态需要被备份以及如何备份和恢复,而外部对象(如 GameSaveManager)不需要了解原发器内部状态的具体结构和细节。例如,GameCharacter 类的 createMemento 方法创建备忘录时,外部只得到一个备忘录对象,无法直接访问 GameCharacterhealthattackPower 属性,从而保护了原发器的封装性。
  2. 备忘录访问控制:备忘录类(如 GameCharacterMemento)虽然存储了原发器的状态,但外部对象(除了原发器)通常不应该直接访问备忘录的内部数据。在 Objective-C 中,可以通过合理的类设计和访问修饰符(如 @private@protected 等)来限制对备忘录内部数据的访问,进一步保证原发器状态的封装性。

方便状态恢复与管理

  1. 简单高效的恢复操作:在 Objective-C 应用中,当需要恢复对象状态时,通过备忘录模式可以非常方便地实现。如在游戏存档读档场景中,只需要调用原发器的 restoreFromMemento 方法,并传入对应的备忘录对象,就可以快速恢复对象到之前的状态,代码逻辑清晰,易于理解和维护。
  2. 状态管理灵活性:备忘录模式使得对象状态的管理更加灵活。可以根据需要创建多个备忘录,保存对象在不同时间点的状态。例如,在文本编辑软件中,可以保存多次编辑操作后的多个版本的备忘录,用户可以根据自己的需求选择恢复到任意一个版本的状态。

支持撤销与重做功能

  1. 撤销操作实现:在实现撤销功能方面,备忘录模式为 Objective-C 应用提供了很好的解决方案。通过在每次操作后保存备忘录,当用户执行撤销操作时,就可以从最近的备忘录中恢复对象状态,实现撤销效果。如在图形设计软件中,用户每次绘制图形或调整图形属性后,软件创建一个备忘录,用户点击撤销按钮时,软件从备忘录恢复图形到上一步的状态。
  2. 重做操作实现:重做功能同样可以基于备忘录模式实现。在执行撤销操作后,备忘录对象仍然保留,如果用户想要重做撤销的操作,就可以从后续的备忘录中恢复状态。例如,在文本编辑软件中,用户撤销了一次文字删除操作后,又想恢复删除的文字,就可以通过重做功能从对应的备忘录中恢复到删除文字后的状态。

备忘录模式在 Objective-C 中的注意事项

备忘录大小管理

  1. 状态数据量考量:在 Objective-C 中,如果原发器对象的状态数据量非常大,那么创建的备忘录对象可能也会占用大量内存。例如,在一个处理高清图片编辑的应用中,图片对象(原发器)的状态包括像素数据、颜色模式、图层信息等,这些数据量巨大。如果每次创建备忘录都完整保存这些状态,可能会导致内存消耗过高,甚至引发应用程序崩溃。
  2. 优化存储策略:为了应对备忘录大小问题,可以采用一些优化存储策略。比如,对于大的数据块,可以考虑采用引用的方式而不是直接复制存储。在 Objective-C 中,可以使用 NSURL 来引用文件路径,而不是将整个文件内容存储在备忘录中。另外,可以采用增量存储的方式,只记录对象状态的变化部分,而不是整个状态,这样可以有效减少备忘录的大小。

备忘录的生命周期管理

  1. 负责人与备忘录关系:在 Objective-C 应用中,负责人对象(如 GameSaveManager)负责管理备忘录的生命周期。需要注意的是,负责人对象应该合理地保存和释放备忘录对象,避免出现内存泄漏或悬空指针等问题。例如,如果 GameSaveManager 在保存备忘录后,没有正确管理其内存,当备忘录对象不再被需要时没有及时释放,就会导致内存泄漏。
  2. 原发器与备忘录关联:原发器对象和备忘录对象之间也存在一定的生命周期关联。原发器创建备忘录后,在使用备忘录恢复状态时,需要确保备忘录的有效性。如果备忘录对象已经被释放或损坏,恢复操作可能会失败或导致程序出现未定义行为。因此,在设计时需要考虑如何在原发器和备忘录之间建立合理的生命周期管理机制。

版本兼容性问题

  1. 原发器状态变化:当原发器对象的状态结构发生变化时,可能会导致与之前版本的备忘录不兼容。在 Objective-C 开发中,例如 GameCharacter 类增加了新的属性,如 defensePower,那么之前创建的备忘录就无法直接用于恢复状态,因为备忘录中没有存储 defensePower 的值。
  2. 版本控制策略:为了处理版本兼容性问题,需要制定合适的版本控制策略。可以在备忘录中添加版本标识,当原发器恢复状态时,首先检查备忘录的版本,根据不同版本进行相应的处理。例如,如果备忘录版本较旧,缺少新属性的值,可以给新属性设置默认值,然后再进行状态恢复。另外,在进行原发器状态结构变更时,尽量采用兼容旧版本的方式,避免完全破坏与旧备忘录的兼容性。

线程安全问题

  1. 多线程环境影响:在多线程的 Objective-C 应用中,备忘录模式的使用需要考虑线程安全问题。如果多个线程同时对原发器进行操作并创建或使用备忘录,可能会导致数据竞争和不一致的问题。例如,一个线程正在创建备忘录,另一个线程同时修改原发器的状态,可能会导致备忘录记录的状态不准确。
  2. 同步机制应用:为了保证线程安全,可以使用 Objective-C 提供的同步机制,如 NSLockNSConditiondispatch_queue 等。例如,可以在原发器的 createMementorestoreFromMemento 方法中使用 NSLock 来确保在同一时间只有一个线程可以操作备忘录相关的代码,避免数据竞争。在多线程环境下,负责人对象管理备忘录时也需要采用同步机制,确保备忘录的保存和读取操作是线程安全的。