设计模式(六)
备忘录模式(Memento Pattern)详解
一、核心概念
备忘录模式允许在不破坏封装性的前提下,捕获并保存对象的内部状态,以便后续恢复。该模式通过三个角色实现:
-
原发器(Originator):需要保存状态的对象,创建和恢复备忘录。
-
备忘录(Memento):存储原发器的内部状态,对外提供有限访问。
-
管理者(Caretaker):负责存储和管理备忘录,但不直接访问其内容。
二、代码示例:文本编辑器
场景:实现文本编辑器的撤销功能,保存和恢复编辑内容。
#include <iostream>
#include <string>
#include <vector>// 备忘录类:存储原发器的内部状态
class EditorMemento {
private:std::string content; // 编辑器内容int cursorPos; // 光标位置public:EditorMemento(const std::string& content, int cursorPos): content(content), cursorPos(cursorPos) {}// 仅向原发器暴露状态friend class TextEditor;
};// 原发器类:文本编辑器
class TextEditor {
private:std::string content;int cursorPos;public:// 创建备忘录EditorMemento* createMemento() const {return new EditorMemento(content, cursorPos);}// 从备忘录恢复状态void restore(const EditorMemento* memento) {content = memento->content;cursorPos = memento->cursorPos;std::cout << "恢复到: " << content << " (光标位置: " << cursorPos << ")" << std::endl;}// 编辑操作void type(const std::string& text) {content += text;cursorPos += text.length();std::cout << "输入: " << text << " (当前内容: " << content << ")" << std::endl;}void moveCursor(int pos) {cursorPos = pos;std::cout << "光标移动到位置: " << cursorPos << std::endl;}
};// 管理者类:保存历史记录
class HistoryManager {
private:std::vector<EditorMemento*> history;TextEditor* editor;public:explicit HistoryManager(TextEditor* editor) : editor(editor) {}~HistoryManager() {for (auto m : history) delete m;}// 保存当前状态void save() {history.push_back(editor->createMemento());std::cout << "保存状态 #" << history.size() << std::endl;}// 撤销到上一个状态void undo() {if (history.empty()) {std::cout << "没有可撤销的操作" << std::endl;return;}EditorMemento* last = history.back();editor->restore(last);history.pop_back();delete last;}
};// 客户端代码
int main() {TextEditor editor;HistoryManager history(&editor);editor.type("Hello");history.save(); // 保存状态 #1editor.type(" World");history.save(); // 保存状态 #2editor.moveCursor(5);editor.type(" there"); // 当前内容: Hello there Worldhistory.undo(); // 恢复到 "Hello World"history.undo(); // 恢复到 "Hello"return 0;
}
三、备忘录模式的优势
-
封装性保护:
- 原发器的内部状态不会被外部直接访问,保持封装完整性。
-
简化原发器:
- 状态管理逻辑由管理者负责,原发器职责更清晰。
-
状态恢复:
- 提供透明的状态恢复机制,支持撤销、回滚等操作。
-
符合开闭原则:
- 新增备忘录类型无需修改现有代码。
四、实现变种
-
白盒备忘录:
- 备忘录的所有状态对所有类公开(通过friend或内部类),牺牲封装性换取简单实现。
-
黑盒备忘录:
- 备忘录的状态完全封装,仅允许原发器访问(如示例中的friend机制)。
-
增量备忘录:
- 仅保存状态变化的部分,节省内存(适用于大型对象)。
-
多重撤销:
- 使用栈或列表存储多个备忘录,支持多级撤销/重做。
五、适用场景
-
撤销/重做功能:
- 如文本编辑器、图形设计软件的历史记录。
-
事务管理:
- 数据库事务的回滚机制。
-
游戏存档:
- 保存游戏状态,支持读档功能。
-
状态快照:
- 需要定期保存对象状态的系统(如虚拟机快照)。
六、注意事项
-
内存消耗:
- 频繁创建备忘录可能导致内存占用过高,可考虑增量保存或限制历史记录数量。
-
深拷贝与浅拷贝:
- 确保备忘录正确复制对象状态,避免浅拷贝导致的共享引用问题。
-
生命周期管理:
- 管理者需负责备忘录的生命周期,避免内存泄漏。
-
性能考虑:
- 大型对象的状态保存与恢复可能影响性能,需优化序列化过程。
七、与其他模式的对比
-
与命令模式的区别:
- 命令模式通过记录操作实现撤销,而备忘录通过保存状态实现撤销。
-
与原型模式的结合:
- 备忘录可通过原型模式(克隆)创建,简化状态复制。
-
与状态模式的区别:
- 状态模式管理对象内部状态的变化,而备忘录模式保存和恢复状态。
备忘录模式是实现对象状态保存与恢复的优雅解决方案,通过分离状态管理职责,既保护了对象封装性,又提供了灵活的历史记录功能。在需要撤销操作、事务回滚或状态快照的场景中尤为实用。
组合模式(Composite Pattern)详解
一、核心概念
组合模式允许将对象组合成树形结构,以表示“部分-整体”的层次关系。该模式让客户端可以统一处理单个对象(叶子节点)和组合对象(容器节点),无需区分它们。
基本对象可以被组合成更复杂的组合对象,而这个组合对象又可以被组合,这样不断地递归下去,客户代码中,任何用到基本对象的地方都可以使用组合对象了
“我感觉用户是不用关心到底是处理一个叶节点还是处理一个组合组件,也就用不着为定义组合而写一些选择判断语句了。”
“简单点说,就是组合模式让客户可以一致地使用组合结构和单个对象。”
——《大话设计模式》
核心组件:
- 抽象组件(Component):定义叶子节点和容器节点的公共接口。
- 叶子节点(Leaf):表示树的叶子节点,没有子节点。
- 容器节点(Composite):包含子节点(叶子或容器),实现组件接口。
二、代码示例:文件系统
场景:模拟文件系统,文件(叶子节点)和目录(容器节点)都可被操作。
#include <iostream>
#include <string>
#include <vector>
#include <memory>// 抽象组件:文件系统组件
class FileSystemComponent {
protected:std::string name;public:explicit FileSystemComponent(const std::string& name) : name(name) {}virtual ~FileSystemComponent() = default;// 公共接口方法virtual void display(int depth = 0) const = 0;virtual void add(FileSystemComponent* component) { throw std::runtime_error("不支持的操作"); }virtual void remove(FileSystemComponent* component) { throw std::runtime_error("不支持的操作"); }virtual bool isComposite() const { return false; }
};// 叶子节点:文件
class File : public FileSystemComponent {
private:int size; // 文件大小(KB)public:File(const std::string& name, int size) : FileSystemComponent(name), size(size) {}void display(int depth) const override {std::cout << std::string(depth * 2, ' ') << "- " << name << " (文件, " << size << "KB)" << std::endl;}
};// 容器节点:目录
class Directory : public FileSystemComponent {
private:std::vector<FileSystemComponent*> children;public:explicit Directory(const std::string& name) : FileSystemComponent(name) {}~Directory() override {for (auto child : children) delete child;}void display(int depth) const override {std::cout << std::string(depth * 2, ' ') << "+ " << name << " (目录, " << children.size() << "个子项)" << std::endl;for (const auto& child : children) {child->display(depth + 1);}}void add(FileSystemComponent* component) override {children.push_back(component);}void remove(FileSystemComponent* component) override {for (auto it = children.begin(); it != children.end(); ++it) {if (*it == component) {children.erase(it);delete component; // 释放内存break;}}}bool isComposite() const override { return true; }
};// 客户端代码
int main() {// 创建目录结构Directory* root = new Directory("根目录");Directory* doc = new Directory("文档");doc->add(new File("报告.docx", 512));doc->add(new File("演示.pptx", 2048));Directory* pics = new Directory("图片");pics->add(new File("风景.jpg", 1024));pics->add(new File("头像.png", 256));root->add(doc);root->add(pics);root->add(new File("README.txt", 16));// 统一操作:显示整个文件系统root->display();// 删除一个文件std::cout << "\n删除一个文件后:\n";pics->remove(pics->isComposite() ? pics->children[0] : nullptr);root->display();delete root; // 自动释放所有子节点return 0;
}
三、组合模式的优势
-
一致性接口:
- 客户端可以统一处理单个对象和组合对象,无需区分。
-
简化客户端代码:
- 无需为叶子节点和容器节点编写不同的处理逻辑。
-
灵活扩展:
- 新增组件类型(叶子或容器)无需修改现有代码,符合开闭原则。
-
树形结构构建:
- 方便构建复杂的树形结构,并递归遍历。
四、实现变种
-
透明式组合:
- 所有方法(如
add
、remove
)都在抽象组件中声明,叶子节点实现为空或抛出异常。
- 所有方法(如
-
安全式组合:
- 仅在容器节点中声明管理子节点的方法,客户端需区分叶子和容器。
-
共享组件:
- 允许组件被多个容器共享(需谨慎管理生命周期)。
-
带缓存的组合:
- 在容器节点中缓存计算结果,避免重复计算。
五、适用场景
-
树形结构表示:
- 如文件系统、组织架构、XML/JSON解析树。
-
递归操作:
- 需要对整个树或部分树进行统一操作(如计算总大小、渲染UI)。
-
层次化菜单:
- 如应用程序菜单、网站导航栏。
-
游戏场景管理:
- 游戏中的场景、角色、道具等层次结构。
六、注意事项
-
内存管理:
- 确保容器节点正确管理子节点的生命周期,避免内存泄漏。
-
性能考虑:
- 递归操作可能导致性能问题,可考虑缓存中间结果。
-
类型安全:
- 客户端需清楚组件类型(叶子/容器),避免无效操作。
-
遍历限制:
- 复杂树形结构的遍历可能需要专门的迭代器模式辅助。
七、与其他模式的对比
-
与装饰器模式的区别:
- 装饰器增强单个对象功能,组合模式处理整体-部分层次结构。
-
与享元模式的结合:
- 组合模式可与享元模式结合,共享叶子节点以节省内存。
-
与访问者模式的结合:
- 访问者模式可用于处理组合结构中的元素,分离操作逻辑。
组合模式是处理树形结构的强大工具,通过统一接口使客户端可以透明地操作单个对象和组合对象。在需要表示“部分-整体”关系的场景中,该模式能显著简化代码并提高可扩展性。
在组合模式中,透明方式(Transparent Approach) 是一种实现策略,它将管理子节点的方法(如 add()
、remove()
、getChild()
等)定义在抽象组件接口中,使得所有组件(包括叶子节点和容器节点)都具有相同的接口。这种方式的核心是让客户端可以统一处理所有组件,无需区分它们是叶子还是容器。
透明方式(Transparent Approach)
透明方式的实现特点
-
统一接口:
抽象组件定义了所有组件(叶子和容器)的公共接口,包括管理子节点的方法。 -
叶子节点的默认实现:
叶子节点无法包含子节点,因此这些方法的实现通常为空或抛出异常。 -
客户端无差别使用:
客户端可以一致地处理所有组件,无需检查组件类型。
代码示例:透明式组合模式
以下是使用透明方式实现的文件系统示例:
#include <iostream>
#include <string>
#include <vector>
#include <memory>// 抽象组件:统一所有方法
class FileSystemComponent {
protected:std::string name;public:explicit FileSystemComponent(const std::string& name) : name(name) {}virtual ~FileSystemComponent() = default;// 统一接口:叶子和容器都必须实现virtual void display(int depth = 0) const = 0;virtual void add(FileSystemComponent* component) { throw std::runtime_error("不支持添加子节点"); }virtual void remove(FileSystemComponent* component) { throw std::runtime_error("不支持移除子节点"); }virtual FileSystemComponent* getChild(int index) { throw std::runtime_error("不支持获取子节点"); }virtual bool isComposite() const { return false; }
};// 叶子节点:文件
class File : public FileSystemComponent {
private:int size;public:File(const std::string& name, int size) : FileSystemComponent(name), size(size) {}void display(int depth) const override {std::cout << std::string(depth * 2, ' ') << "- " << name << " (文件, " << size << "KB)" << std::endl;}
};// 容器节点:目录
class Directory : public FileSystemComponent {
private:std::vector<FileSystemComponent*> children;public:explicit Directory(const std::string& name) : FileSystemComponent(name) {}~Directory() override {for (auto child : children) delete child;}void display(int depth) const override {std::cout << std::string(depth * 2, ' ') << "+ " << name << " (目录, " << children.size() << "个子项)" << std::endl;for (const auto& child : children) {child->display(depth + 1);}}// 容器节点实现完整的子节点管理void add(FileSystemComponent* component) override {children.push_back(component);}void remove(FileSystemComponent* component) override {for (auto it = children.begin(); it != children.end(); ++it) {if (*it == component) {children.erase(it);delete component;break;}}}FileSystemComponent* getChild(int index) override {if (index >= 0 && index < children.size()) {return children[index];}return nullptr;}bool isComposite() const override { return true; }
};// 客户端代码:统一处理所有组件
void printComponentInfo(FileSystemComponent* component) {component->display();// 尝试添加子节点(叶子节点会抛出异常)try {component->add(new File("test.txt", 10));std::cout << "成功添加子节点" << std::endl;} catch (const std::exception& e) {std::cout << "错误: " << e.what() << std::endl;}
}int main() {// 创建文件和目录FileSystemComponent* file = new File("data.csv", 512);FileSystemComponent* dir = new Directory("文档");// 统一处理printComponentInfo(file); // 叶子节点不支持添加子节点printComponentInfo(dir); // 目录支持添加子节点delete file;delete dir;return 0;
}
透明方式的优缺点
优点
-
简化客户端代码:
客户端无需区分组件类型,统一调用相同接口,降低代码复杂度。 -
增强一致性:
所有组件对外呈现相同的接口,符合“一致对待组合对象和叶子对象”的设计目标。 -
支持多态操作:
通过抽象组件的指针或引用,可以递归处理整个树形结构。
缺点
-
违反接口隔离原则:
叶子节点被迫实现它们不需要的方法(如add()
),可能导致运行时异常。 -
安全性降低:
客户端可能在不合法的情况下调用叶子节点的管理方法(如对文件调用add()
)。 -
潜在的运行时错误:
叶子节点的默认实现通常抛出异常,需依赖客户端进行类型检查或异常处理。
透明方式 vs 安全方式
维度 | 透明方式(Transparent) | 安全方式(Safe) |
---|---|---|
接口定义 | 所有方法在抽象组件中声明 | 仅在容器节点中声明子节点管理方法 |
叶子节点实现 | 实现为空或抛出异常 | 不实现子节点管理方法 |
类型检查 | 客户端无需检查组件类型 | 客户端需显式区分叶子和容器 |
安全性 | 低(可能在运行时抛出异常) | 高(编译时即可发现错误) |
一致性 | 高(统一接口) | 低(不同组件接口不同) |
适用场景 | 强调统一操作,客户端无需区分组件类型 | 安全性要求高,需明确区分叶子和容器 |
透明方式的适用场景
-
客户端无需关心组件类型:
当客户端只需要统一操作组件,而不关心其具体实现时。 -
树形结构遍历:
在递归遍历树形结构时,透明方式更简洁,无需类型检查。 -
简化API:
当希望提供简单、一致的API时,透明方式更符合直觉。
透明方式是组合模式的典型实现,它通过牺牲一定的安全性来换取接口的一致性和客户端代码的简化。在实际应用中,需根据具体场景权衡选择透明方式或安全方式。