设计模式手册017:备忘录模式 - 对象状态保存与恢复
本文是「设计模式手册」系列第017篇,我将以深入浅出、追本溯源的风格,带你真正理解备忘录模式的精髓。
1. 我们为何需要备忘录模式?
在软件设计中,我们经常会遇到这样的场景:需要保存对象的内部状态,并在需要时能够恢复到之前的状态。比如:
- 文本编辑器:撤销/重做功能
- 游戏系统:存档/读档功能
- 数据库事务:回滚操作
- 表单填写:自动保存草稿
初级程序员的写法:
java
public class TextEditor {
private String content;
private String fontSize;
private String fontColor;
// 直接暴露内部状态用于保存
public String getContent() { return content; }
public String getFontSize() { return fontSize; }
public String getFontColor() { return fontColor; }
public void setContent(String content) { this.content = content; }
public void setFontSize(String fontSize) { this.fontSize = fontSize; }
public void setFontColor(String fontColor) { this.fontColor = fontColor; }
// 保存状态 - 直接返回内部状态
public TextEditorState save() {
return new TextEditorState(content, fontSize, fontColor);
}
// 恢复状态 - 直接设置内部状态
public void restore(TextEditorState state) {
this.content = state.getContent();
this.fontSize = state.getFontSize();
this.fontColor = state.getFontColor();
}
}
// 状态类直接暴露所有字段
public class TextEditorState {
public String content;
public String fontSize;
public String fontColor;
public TextEditorState(String content, String fontSize, String fontColor) {
this.content = content;
this.fontSize = fontSize;
this.fontColor = fontColor;
}
// 所有getter方法直接暴露内部数据
public String getContent() { return content; }
public String getFontSize() { return fontSize; }
public String getFontColor() { return fontColor; }
}
这种写法的痛点:
- 违反封装原则:外部可以直接访问和修改对象内部状态
- 状态保存逻辑分散在各个类中
- 难以维护:当对象结构变化时,需要修改所有保存/恢复的代码
- 无法保护历史状态不被修改
2. 备忘录模式:本质与定义
2.1 模式定义
备忘录模式(Memento Pattern):在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后可以将该对象恢复到原先保存的状态。
2.2 模式结构
java
// 备忘录 - 存储原发器对象的内部状态
public class Memento {
private final String state;
public Memento(String stateToSave) {
this.state = stateToSave;
}
// 只有原发器可以访问备忘录的内部状态
public String getSavedState() {
return state;
}
}
// 原发器 - 创建备忘录并使用备忘录恢复状态
public class Originator {
private String state;
public void setState(String state) {
System.out.println("设置状态: " + state);
this.state = state;
}
public String getState() {
return state;
}
// 创建备忘录
public Memento saveToMemento() {
System.out.println("保存状态到备忘录: " + state);
return new Memento(state);
}
// 从备忘录恢复
public void restoreFromMemento(Memento memento) {
state = memento.getSavedState();
System.out.println("从备忘录恢复状态: " + state);
}
}
// 看管人 - 负责保存和管理备忘录
public class Caretaker {
private List<Memento> savedStates = new ArrayList<>();
public void addMemento(Memento memento) {
savedStates.add(memento);
}
public Memento getMemento(int index) {
return savedStates.get(index);
}
}
3. 深入理解:备忘录模式的三重境界
3.1 第一重:状态封装与保护
核心思想:备忘录应该保护原发器的内部状态不被其他对象随意访问。
java
// 好的设计:窄接口
public interface Memento {
// 不提供任何公共方法,保护状态
}
// 原发器拥有的宽接口
public interface Originator {
Memento createMemento();
void restoreMemento(Memento memento);
}
3.2 第二重:状态管理的职责分离
设计原则的体现:将状态保存和管理的职责从业务对象中分离出来。
- 原发器:负责业务逻辑和状态变化
- 备忘录:负责状态存储
- 看管人:负责状态管理
3.3 第三重:增量状态与性能优化
对于大对象,可以只保存变化的部分(增量备忘录):
java
public class IncrementalMemento {
private final Map<String, Object> changedFields;
public IncrementalMemento(Map<String, Object> changes) {
this.changedFields = new HashMap<>(changes);
}
public Map<String, Object> getChanges() {
return new HashMap<>(changedFields);
}
}
4. 实战案例:完整的文本编辑器撤销系统
让我们来看一个完整的例子:
java
// 文本状态备忘录
public class TextStateMemento {
private final String content;
private final TextFormat format;
private final int cursorPosition;
private final LocalDateTime timestamp;
public TextStateMemento(String content, TextFormat format, int cursorPosition) {
this.content = content;
this.format = format != null ? format.copy() : null;
this.cursorPosition = cursorPosition;
this.timestamp = LocalDateTime.now();
}
// 包级私有,只有同包的原发器可以访问
String getContent() {
return content;
}
TextFormat getFormat() {
return format != null ? format.copy() : null;
}
int getCursorPosition() {
return cursorPosition;
}
public LocalDateTime getTimestamp() {
return timestamp;
}
}
// 文本格式
public class TextFormat implements Cloneable {
private String fontFamily;
private int fontSize;
private String color;
private boolean bold;
private boolean italic;
public TextFormat(String fontFamily, int fontSize, String color) {
this.fontFamily = fontFamily;
this.fontSize = fontSize;
this.color = color;
}
// 复制方法
public TextFormat copy() {
TextFormat copy = new TextFormat(fontFamily, fontSize, color);
copy.bold = bold;
copy.italic = italic;
return copy;
}
// getters and setters
public String getFontFamily() { return fontFamily; }
public void setFontFamily(String fontFamily) { this.fontFamily = fontFamily; }
public int getFontSize() { return fontSize; }
public void setFontSize(int fontSize) { this.fontSize = fontSize; }
public String getColor() { return color; }
public void setColor(String color) { this.color = color; }
public boolean isBold() { return bold; }
public void setBold(boolean bold) { this.bold = bold; }
public boolean isItalic() { return italic; }
public void setItalic(boolean italic) { this.italic = italic; }
@Override
public String toString() {
return String.format("%s %dpx %s%s%s",
fontFamily, fontSize, color,
bold ? " Bold" : "",
italic ? " Italic" : "");
}
}
// 文本编辑器 - 原发器
@Slf4j
public class TextEditor {
private String content;
private TextFormat currentFormat;
private int cursorPosition;
public TextEditor() {
this.content = "";
this.currentFormat = new TextFormat("Arial", 12, "#000000");
this.cursorPosition = 0;
}
// 业务操作
public void type(String text) {
if (cursorPosition <= content.length()) {
String before = content.substring(0, cursorPosition);
String after = content.substring(cursorPosition);
content = before + text + after;
cursorPosition += text.length();
log.info("输入文本: '{}',光标位置: {}", text, cursorPosition);
}
}
public void delete(int count) {
if (cursorPosition > 0 && cursorPosition <= content.length()) {
int deleteFrom = Math.max(0, cursorPosition - count);
String before = content.substring(0, deleteFrom);
String after = content.substring(cursorPosition);
content = before + after;
cursorPosition = deleteFrom;
log.info("删除 {} 个字符,光标位置: {}", count, cursorPosition);
}
}
public void setFormat(TextFormat format) {
this.currentFormat = format.copy();
log.info("设置格式: {}", format);
}
public void setCursorPosition(int position) {
this.cursorPosition = Math.max(0, Math.min(position, content.length()));
log.info("移动光标到位置: {}", cursorPosition);
}
// 创建备忘录
public TextStateMemento save() {
log.info("保存状态 - 内容长度: {}, 光标位置: {}", content.length(), cursorPosition);
return new TextStateMemento(content, currentFormat, cursorPosition);
}
// 从备忘录恢复
public void restore(TextStateMemento memento) {
this.content = memento.getContent();
this.currentFormat = memento.getFormat();
this.cursorPosition = memento.getCursorPosition();
log.info("恢复状态 - 内容长度: {}, 光标位置: {}, 时间: {}",
content.length(), cursorPosition, memento.getTimestamp());
}
// 获取当前状态信息
public void printStatus() {
System.out.println("=== 当前状态 ===");
System.out.println("内容: " + (content.isEmpty() ? "(空)" : content));
System.out.println("格式: " + currentFormat);
System.out.println("光标: " + cursorPosition);
System.out.println("长度: " + content.length());
System.out.println("==============");
}
// getters
public String getContent() { return content; }
public TextFormat getCurrentFormat() { return currentFormat; }
public int getCursorPosition() { return cursorPosition; }
}
// 历史管理器 - 看管人
@Slf4j
public class HistoryManager {
private final Deque<TextStateMemento> history = new ArrayDeque<>();
private final Deque<TextStateMemento> redoStack = new ArrayDeque<>();
private final int maxHistorySize;
public HistoryManager(int maxHistorySize) {
this.maxHistorySize = maxHistorySize;
}
public void saveState(TextStateMemento memento) {
history.push(memento);
redoStack.clear(); // 新的操作清空重做栈
// 限制历史记录大小
if (history.size() > maxHistorySize) {
history.removeLast();
}
log.info("保存历史记录,当前历史大小: {}", history.size());
}
public TextStateMemento undo() {
if (history.size() > 1) { // 保留当前状态作为重做的起点
TextStateMemento current = history.pop();
TextStateMemento previous = history.peek();
redoStack.push(current);
log.info("执行撤销,历史大小: {}, 重做大小: {}", history.size(), redoStack.size());
return previous;
}
log.warn("无法撤销,历史记录不足");
return null;
}
public TextStateMemento redo() {
if (!redoStack.isEmpty()) {
TextStateMemento next = redoStack.pop();
history.push(next);
log.info("执行重做,历史大小: {}, 重做大小: {}", history.size(), redoStack.size());
return next;
}
log.warn("无法重做,重做栈为空");
return null;
}
public boolean canUndo() {
return history.size() > 1;
}
public boolean canRedo() {
return !redoStack.isEmpty();
}
public void clear() {
history.clear();
redoStack.clear();
log.info("清空所有历史记录");
}
public List<String> getHistoryTimestamps() {
return history.stream()
.map(m -> m.getTimestamp().format(DateTimeFormatter.ofPattern("HH:mm:ss")))
.collect(Collectors.toList());
}
}
// 使用示例
@Slf4j
public class TextEditorDemo {
public static void main(String[] args) {
TextEditor editor = new TextEditor();
HistoryManager history = new HistoryManager(10);
// 初始保存
history.saveState(editor.save());
// 一系列操作
editor.type("Hello");
history.saveState(editor.save());
editor.type(" World");
history.saveState(editor.save());
editor.setFormat(new TextFormat("Times New Roman", 16, "#FF0000"));
history.saveState(editor.save());
editor.type("!");
history.saveState(editor.save());
System.out.println("\n=== 当前状态 ===");
editor.printStatus();
// 执行撤销
System.out.println("\n=== 执行两次撤销 ===");
TextStateMemento undoState = history.undo();
if (undoState != null) {
editor.restore(undoState);
}
editor.printStatus();
undoState = history.undo();
if (undoState != null) {
editor.restore(undoState);
}
editor.printStatus();
// 执行重做
System.out.println("\n=== 执行一次重做 ===");
TextStateMemento redoState = history.redo();
if (redoState != null) {
editor.restore(redoState);
}
editor.printStatus();
// 显示历史记录
System.out.println("\n=== 历史记录时间点 ===");
history.getHistoryTimestamps().forEach(System.out::println);
}
}
5. Spring Boot中的优雅实现
在Spring Boot中,我们可以利用事务和持久化实现备忘录模式:
java
// 实体类
@Entity
@Table(name = "document_states")
@Data
public class DocumentState {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String documentId;
@Lob
private String content;
@Lob
private String formatJson;
private int cursorPosition;
@CreationTimestamp
private LocalDateTime createdAt;
private String operationType; // CREATE, EDIT, DELETE等
}
// 备忘录仓库
@Repository
public interface DocumentStateRepository extends JpaRepository<DocumentState, Long> {
List<DocumentState> findByDocumentIdOrderByCreatedAtDesc(String documentId);
@Modifying
@Query("DELETE FROM DocumentState ds WHERE ds.documentId = :documentId AND ds.createdAt < :beforeDate")
void deleteOldStates(@Param("documentId") String documentId,
@Param("beforeDate") LocalDateTime beforeDate);
@Query("SELECT ds FROM DocumentState ds WHERE ds.documentId = :documentId AND ds.createdAt = " +
"(SELECT MAX(ds2.createdAt) FROM DocumentState ds2 WHERE ds2.documentId = :documentId)")
Optional<DocumentState> findLatestState(@Param("documentId") String documentId);
}
// 文档服务
@Service
@Slf4j
public class DocumentService {
private final DocumentStateRepository stateRepository;
private final ObjectMapper objectMapper;
public DocumentService(DocumentStateRepository stateRepository, ObjectMapper objectMapper) {
this.stateRepository = stateRepository;
this.objectMapper = objectMapper;
}
// 保存文档状态
@Transactional
public void saveDocumentState(Document document, String operationType) {
try {
DocumentState state = new DocumentState();
state.setDocumentId(document.getId());
state.setContent(document.getContent());
state.setFormatJson(objectMapper.writeValueAsString(document.getFormat()));
state.setCursorPosition(document.getCursorPosition());
state.setOperationType(operationType);
stateRepository.save(state);
log.info("保存文档状态: {} - {}", document.getId(), operationType);
// 清理旧的状态记录(只保留最近50条)
cleanOldStates(document.getId());
} catch (JsonProcessingException e) {
log.error("序列化文档格式失败", e);
throw new RuntimeException("保存文档状态失败", e);
}
}
// 恢复文档状态
@Transactional
public Document restoreDocumentState(String documentId, Long stateId) {
DocumentState state = stateRepository.findById(stateId)
.orElseThrow(() -> new IllegalArgumentException("状态记录不存在: " + stateId));
try {
Document document = new Document();
document.setId(state.getDocumentId());
document.setContent(state.getContent());
document.setFormat(objectMapper.readValue(state.getFormatJson(), DocumentFormat.class));
document.setCursorPosition(state.getCursorPosition());
log.info("恢复文档状态: {} - {}", documentId, state.getCreatedAt());
return document;
} catch (JsonProcessingException e) {
log.error("反序列化文档格式失败", e);
throw new RuntimeException("恢复文档状态失败", e);
}
}
// 获取文档历史记录
public List<DocumentHistory> getDocumentHistory(String documentId) {
return stateRepository.findByDocumentIdOrderByCreatedAtDesc(documentId)
.stream()
.map(this::toHistory)
.collect(Collectors.toList());
}
// 清理旧的状态记录
private void cleanOldStates(String documentId) {
List<DocumentState> allStates = stateRepository.findByDocumentIdOrderByCreatedAtDesc(documentId);
if (allStates.size() > 50) {
List<DocumentState> toDelete = allStates.subList(50, allStates.size());
stateRepository.deleteAll(toDelete);
log.info("清理了 {} 个旧状态记录", toDelete.size());
}
}
private DocumentHistory toHistory(DocumentState state) {
return DocumentHistory.builder()
.id(state.getId())
.documentId(state.getDocumentId())
.operationType(state.getOperationType())
.contentPreview(getContentPreview(state.getContent()))
.createdAt(state.getCreatedAt())
.build();
}
private String getContentPreview(String content) {
if (content == null || content.length() <= 50) {
return content;
}
return content.substring(0, 50) + "...";
}
}
// 历史记录DTO
@Data
@Builder
public class DocumentHistory {
private Long id;
private String documentId;
private String operationType;
private String contentPreview;
private LocalDateTime createdAt;
}
// REST控制器
@RestController
@RequestMapping("/api/documents")
@Slf4j
public class DocumentController {
private final DocumentService documentService;
public DocumentController(DocumentService documentService) {
this.documentService = documentService;
}
@PostMapping("/{documentId}/save-state")
public ResponseEntity<Void> saveState(@PathVariable String documentId,
@RequestBody SaveStateRequest request) {
documentService.saveDocumentState(request.getDocument(), request.getOperationType());
return ResponseEntity.ok().build();
}
@GetMapping("/{documentId}/history")
public ResponseEntity<List<DocumentHistory>> getHistory(@PathVariable String documentId) {
List<DocumentHistory> history = documentService.getDocumentHistory(documentId);
return ResponseEntity.ok(history);
}
@PostMapping("/{documentId}/restore/{stateId}")
public ResponseEntity<Document> restoreState(@PathVariable String documentId,
@PathVariable Long stateId) {
Document document = documentService.restoreDocumentState(documentId, stateId);
return ResponseEntity.ok(document);
}
}
6. 备忘录模式的变体与进阶用法
6.1 命令模式 + 备忘录模式
结合命令模式实现更强大的撤销/重做系统:
java
// 命令接口
public interface Command {
void execute();
void undo();
}
// 具体命令
@Slf4j
public class TextEditCommand implements Command {
private final TextEditor editor;
private final String newText;
private final int position;
private TextStateMemento backup;
public TextEditCommand(TextEditor editor, String newText, int position) {
this.editor = editor;
this.newText = newText;
this.position = position;
}
@Override
public void execute() {
// 保存当前状态
backup = editor.save();
// 执行编辑操作
editor.setCursorPosition(position);
editor.type(newText);
log.info("执行编辑命令: '{}' 在位置 {}", newText, position);
}
@Override
public void undo() {
if (backup != null) {
editor.restore(backup);
log.info("撤销编辑命令");
}
}
}
// 命令管理器
@Slf4j
public class CommandManager {
private final Deque<Command> undoStack = new ArrayDeque<>();
private final Deque<Command> redoStack = new ArrayDeque<>();
public void executeCommand(Command command) {
command.execute();
undoStack.push(command);
redoStack.clear();
log.info("执行命令,撤销栈大小: {}", undoStack.size());
}
public void undo() {
if (!undoStack.isEmpty()) {
Command command = undoStack.pop();
command.undo();
redoStack.push(command);
log.info("撤销命令,撤销栈大小: {}, 重做栈大小: {}", undoStack.size(), redoStack.size());
}
}
public void redo() {
if (!redoStack.isEmpty()) {
Command command = redoStack.pop();
command.execute();
undoStack.push(command);
log.info("重做命令,撤销栈大小: {}, 重做栈大小: {}", undoStack.size(), redoStack.size());
}
}
public boolean canUndo() {
return !undoStack.isEmpty();
}
public boolean canRedo() {
return !redoStack.isEmpty();
}
}
6.2 增量备忘录模式
对于大对象,保存完整状态开销大,可以只保存变化的部分:
java
// 增量备忘录
public class IncrementalMemento {
private final Map<String, Object> stateChanges;
private final LocalDateTime timestamp;
public IncrementalMemento(Map<String, Object> changes) {
this.stateChanges = new HashMap<>(changes);
this.timestamp = LocalDateTime.now();
}
public Map<String, Object> getChanges() {
return new HashMap<>(stateChanges);
}
public LocalDateTime getTimestamp() {
return timestamp;
}
}
// 支持增量保存的原发器
@Slf4j
public class LargeObject {
private Map<String, Object> state = new HashMap<>();
private Map<String, Object> changesSinceLastSave = new HashMap<>();
public void setProperty(String key, Object value) {
Object oldValue = state.get(key);
if (!Objects.equals(oldValue, value)) {
state.put(key, value);
changesSinceLastSave.put(key, value);
log.info("设置属性 {}: {} -> {}", key, oldValue, value);
}
}
public Object getProperty(String key) {
return state.get(key);
}
public IncrementalMemento saveIncremental() {
if (changesSinceLastSave.isEmpty()) {
log.info("没有变化,不保存增量");
return null;
}
IncrementalMemento memento = new IncrementalMemento(changesSinceLastSave);
changesSinceLastSave.clear();
log.info("保存增量备忘录,包含 {} 个变化", memento.getChanges().size());
return memento;
}
public void restoreIncremental(IncrementalMemento memento) {
if (memento != null) {
state.putAll(memento.getChanges());
log.info("从增量备忘录恢复 {} 个属性", memento.getChanges().size());
}
}
public IncrementalMemento saveFull() {
log.info("保存完整状态,包含 {} 个属性", state.size());
return new IncrementalMemento(new HashMap<>(state));
}
public void restoreFull(IncrementalMemento memento) {
if (memento != null) {
this.state = new HashMap<>(memento.getChanges());
this.changesSinceLastSave.clear();
log.info("从完整备忘录恢复状态");
}
}
}
7. 备忘录模式 vs 其他模式
7.1 备忘录模式 vs 原型模式
- 原型模式:通过复制现有对象来创建新对象,关注对象创建
- 备忘录模式:保存和恢复对象状态,关注状态管理
7.2 备忘录模式 vs 命令模式
- 命令模式:将请求封装为对象,支持撤销/重做
- 备忘录模式:保存对象状态,用于状态恢复
7.3 备忘录模式 vs 序列化
- 序列化:将对象转换为字节流,用于持久化或传输
- 备忘录模式:设计模式,强调封装性和状态管理
8. 总结与思考
8.1 备忘录模式的优点
- 封装性保护:不暴露对象内部实现细节
- 简化原发器:状态保存逻辑分离到备忘录中
- 易于实现撤销:提供状态恢复机制
- 状态持久化:支持状态保存到外部存储
8.2 备忘录模式的缺点
- 内存开销:保存大量状态可能占用较多内存
- 看管人职责:看管人不知道备忘录内容,但需要管理生命周期
- 性能考虑:大对象的状态保存和恢复可能影响性能
8.3 深入思考
备忘录模式的本质是**"状态封装的时空旅行"**。它让我们可以在不破坏对象封装的前提下,穿越时间回到过去的状态。
设计之美的思考:
"备忘录模式体现了'时间管理'的智慧。在软件的世界里,我们不仅需要关注对象当前的状态,更需要有能力回到过去、展望未来。这种时空穿梭的能力,让我们的程序更加健壮和用户友好。"
从源码的角度看,备忘录模式在现实世界中无处不在:
- 数据库事务的回滚机制
- 版本控制系统(Git的commit和reset)
- 浏览器的前进后退
- 游戏存档系统
- 编辑器的撤销重做
何时使用备忘录模式:
- 需要保存和恢复对象状态
- 需要实现撤销/重做功能
- 需要保存对象状态而不暴露其内部细节
- 需要实现快照功能
下一篇预告:设计模式手册018 - 访问者模式:如何在不修改类的前提下扩展功能?
版权声明:本文为CSDN博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。