You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

备忘录与命令设计模式区别及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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 07:17:17