备忘录与命令设计模式区别及Java纸牌游戏撤销重做实现咨询
备忘录(Memento)vs 命令(Command)设计模式:核心区别
这俩经常被放在一起讨论,因为都能用来实现撤销/重做功能,但它们解决问题的核心思路完全不同,我从几个关键维度拆解清楚:
核心目标差异
- 备忘录模式聚焦于保存对象的状态快照,它的核心是「记录某个时刻对象是什么样」,之后可以把对象直接恢复到这个状态。比如游戏存档、编辑器的历史版本,都是备忘录的典型场景。
- 命令模式则是封装操作本身,把每个用户动作变成一个独立的命令对象,核心是「记录用户做了什么动作」,撤销时执行该动作的反向操作即可。
存储内容不同
- 备忘录保存的是对象的完整(或关键)状态数据,比如纸牌游戏里,备忘录可能会记录所有玩家的手牌、牌堆剩余卡牌、当前回合玩家等全套状态。
- 命令模式保存的是操作的具体细节,比如「玩家A从牌堆抽了黑桃3」「玩家B把红桃Q移到弃牌堆」这类动作指令,不需要保存整个游戏状态。
撤销实现逻辑
- 备忘录的撤销很直接:把当前对象的状态替换成备忘录里的快照就行。但如果对象状态很大(比如大型游戏的全局状态),内存开销会比较高。
- 命令模式的撤销需要每个命令自己实现
undo()方法,比如「抽牌」的undo就是「把牌放回牌堆」,它不需要保存整个状态,只需要记录操作的反向逻辑,内存效率更高。
适用场景
- 当你需要频繁保存/恢复对象状态,或者状态容易被完整快照时,用备忘录更合适,比如图片编辑器的历史记录、单机游戏的存档读档。
- 当操作本身可以被清晰封装,且反向操作容易实现时,命令模式更灵活,比如UI按钮操作、数据库事务,还有你现在要做的纸牌游戏撤销/重做。
你的纸牌游戏撤销/重做方案:评估与优化建议
首先得夸一句:你的思路方向完全正确——用栈(或双向栈)管理操作记录是命令模式的经典用法,比备忘录模式更适合纸牌游戏这种操作离散的场景。不过有几个细节可以调整得更健壮、更易维护:
现有方案的潜在问题
你提到「反转最后存储的两组操作(源操作和目标操作)」,这里有点模糊:其实一个完整的用户动作(比如移动卡牌)应该是一个独立的命令对象,而不是拆分成两组。如果拆分记录,撤销时需要同时处理两个操作,容易出现状态不一致的问题(比如其中一个操作执行失败,另一个没回滚)。
优化后的具体实现步骤
1. 定义统一的命令接口
先写一个包含核心方法的接口,让所有操作命令都遵循这个规范:
public interface GameCommand { void execute(); // 执行操作 void undo(); // 撤销操作 void redo(); // 重做操作 boolean isUndoable(); // 标记该操作是否可撤销(比如游戏初始化就不能撤销) }
2. 实现具体的命令类
针对纸牌游戏的常见操作(移动卡牌、抽牌、出牌等),实现对应的命令类。以移动卡牌为例:
public class MoveCardCommand implements GameCommand { private final Card card; private final CardPile sourcePile; private final CardPile targetPile; // 构造方法传入操作所需的所有信息 public MoveCardCommand(Card card, CardPile sourcePile, CardPile targetPile) { this.card = card; this.sourcePile = sourcePile; this.targetPile = targetPile; } @Override public void execute() { sourcePile.removeCard(card); targetPile.addCard(card); } @Override public void undo() { // 反向执行操作:把卡牌从目标堆移回源堆 targetPile.removeCard(card); sourcePile.addCard(card); } @Override public void redo() { // 重做就是重新执行原操作 execute(); } @Override public boolean isUndoable() { return true; // 移动操作是可以撤销的 } }
3. 用双栈管理命令历史
用两个栈分别管理已执行的命令(用于撤销)和已撤销的命令(用于重做),这样逻辑更清晰:
public class GameHistory { private final Deque<GameCommand> undoStack = new ArrayDeque<>(); private final Deque<GameCommand> redoStack = new ArrayDeque<>(); // 执行新命令的方法 public void executeCommand(GameCommand command) { command.execute(); undoStack.push(command); redoStack.clear(); // 执行新操作后,之前的重做记录就失效了,清空重做栈 } // 撤销操作 public void undo() { if (!undoStack.isEmpty()) { GameCommand command = undoStack.pop(); if (command.isUndoable()) { command.undo(); redoStack.push(command); } else { // 如果当前操作不可撤销,继续尝试撤销前一个操作 undo(); } } } // 重做操作 public void redo() { if (!redoStack.isEmpty()) { GameCommand command = redoStack.pop(); command.redo(); undoStack.push(command); } } }
4. 整合到游戏逻辑中
当用户触发操作时,创建对应的命令对象,交给GameHistory处理即可:
// 示例:用户选择一张卡牌,从手牌移到弃牌堆 Card selectedCard = playerHand.getSelectedCard(); CardPile discardPile = game.getDiscardPile(); GameCommand moveCommand = new MoveCardCommand(selectedCard, playerHand, discardPile); gameHistory.executeCommand(moveCommand);
额外优化建议
- 处理不可撤销操作:比如游戏开始发牌、强制结束回合这类操作,在对应的命令类里把
isUndoable()返回false,撤销时会自动跳过。 - 支持批量操作:如果用户有连续操作(比如一次出多张牌),可以用
CompositeCommand把多个命令打包成一个,这样撤销时可以一次撤销整个批量操作,提升用户体验。 - 保证状态一致性:在
undo()和redo()方法里加一些校验,比如确保卡牌确实存在于目标/源堆,避免出现空指针或状态错误。
内容的提问来源于stack exchange,提问作者B.M. Corwen
相关产品推荐
相关产品推荐

