Java案例如何实现备忘录模式?

wen java案例 46

Java备忘录模式实战案例详解:从原理到代码实现

目录导读

  1. 什么是备忘录模式?——核心概念与生活类比
  2. 备忘录模式的UML结构与角色解析
  3. Java实现备忘录模式的三种经典案例
    • 文本编辑器的撤销功能
    • 游戏角色状态存档
    • 数据库事务回滚模拟
  4. 备忘录模式在Java中的常见陷阱与优化
  5. 问答环节:开发者最常遇到的5个问题

什么是备忘录模式?——核心概念与生活类比

备忘录模式(Memento Pattern)是一种行为型设计模式,它允许在不破坏对象封装性的前提下,捕获并外部化一个对象的内部状态,以便在后续需要时将该对象恢复到先前保存的状态,简单说,就是“后悔药”机制。

Java案例如何实现备忘录模式?

生活类比
想象你在玩《塞尔达传说》——在挑战Boss前,你手动存档,如果打Boss失败,你可以读档回到挑战前的状态,这里的存档文件就是“备忘录”,游戏主角是“发起人”,而存档系统是“管理者”。

核心三要素

  • 发起人(Originator):负责创建备忘录并利用备忘录恢复自身状态。
  • 备忘录(Memento):存储发起人内部状态的对象,对外部不可修改。
  • 管理者(Caretaker):负责保存备忘录,但不能修改其内容。

适用场景

  • 需要保存对象在某一时刻的完整状态(快照)
  • 实现撤销/重做功能(如IDE的Ctrl+Z)
  • 需要避免直接暴露对象内部状态给外部

备忘录模式的UML结构与角色解析

┌─────────────────────┐          ┌─────────────────────┐
│  Originator          │          │  Memento            │
│─────────────────────│          │─────────────────────│
│ - state: String      │◄─────────│ - state: String     │
│ + saveState(): void  │          │ + getState(): void  │
│ + restore(m): void   │          └─────────────────────┘
└─────────────────────┘                     ▲
         │                                   │
         │ 创建/恢复                         │
         ▼                                   │
┌─────────────────────┐                     │
│  Caretaker          │                     │
│─────────────────────│                     │
│ - mementos: List    │─────────► 持有备忘录
│ + addMemento(m)     │
│ + getMemento(i)     │
└─────────────────────┘

关键设计原则

  • 备忘录对象应限制访问权限,仅允许发起人读写其内容,而对管理者只提供只读接口。
  • 在Java中,通常通过嵌套类包级私有来实现封装。

Java实现备忘录模式的三种经典案例

文本编辑器的撤销功能

需求:实现一个简单的文本编辑器,支持保存当前内容,并能够恢复到历史版本。

// 1. 备忘录类(内部类形式)
public class Editor {
    private String content;
    public void setContent(String content) {
        this.content = content;
    }
    public String getContent() {
        return content;
    }
    // 保存快照
    public Memento save() {
        return new Memento(content);
    }
    // 恢复快照
    public void restore(Memento memento) {
        this.content = memento.getContent();
    }
    // 内部备忘录类,对外隐藏实现细节
    public static class Memento {
        private final String content;
        private Memento(String content) {
            this.content = content;
        }
        private String getContent() {  // 私有方法,仅Editor可调用
            return content;
        }
    }
}
// 2. 管理者(历史记录)
import java.util.Stack;
public class History {
    private Stack<Editor.Memento> stack = new Stack<>();
    public void push(Editor.Memento memento) {
        stack.push(memento);
    }
    public Editor.Memento pop() {
        return stack.isEmpty() ? null : stack.pop();
    }
}
// 3. 客户端测试
public class Client {
    public static void main(String[] args) {
        Editor editor = new Editor();
        History history = new History();
        editor.setContent("第一版内容");
        history.push(editor.save());
        editor.setContent("第二版内容(错误)");
        history.push(editor.save());
        editor.setContent("第三版内容");
        // 发现第二版错了,撤销到第一版
        editor.restore(history.pop());  // 恢复到“第二版内容”
        editor.restore(history.pop());  // 恢复到“第一版内容”
        System.out.println(editor.getContent()); // 输出:第一版内容
    }
}

输出

关键点:使用Stack实现“最近撤销”,备忘录类作为内部类确保私有状态不被外界篡改。


游戏角色状态存档

需求:一个RPG游戏角色有生命值、魔法值和位置属性,要求支持存档和读档。

public class GameCharacter {
    private int hp;
    private int mp;
    private int x, y;
    public GameCharacter(int hp, int mp, int x, int y) {
        this.hp = hp;
        this.mp = mp;
        this.x = x;
        this.y = y;
    }
    public void setPosition(int x, int y) {
        this.x = x;
        this.y = y;
    }
    public void receiveDamage(int damage) {
        this.hp = Math.max(0, hp - damage);
    }
    // 创建备忘录
    public CharacterMemento saveProgress() {
        return new CharacterMemento(hp, mp, x, y);
    }
    // 恢复进度
    public void loadProgress(CharacterMemento memento) {
        this.hp = memento.getHp();
        this.mp = memento.getMp();
        this.x = memento.getX();
        this.y = memento.getY();
    }
    @Override
    public String toString() {
        return String.format("HP:%d MP:%d 位置:(%d,%d)", hp, mp, x, y);
    }
    // 备忘录,注意访问权限
    public static class CharacterMemento {
        private final int hp, mp, x, y;
        private CharacterMemento(int hp, int mp, int x, int y) {
            this.hp = hp;
            this.mp = mp;
            this.x = x;
            this.y = y;
        }
        // 仅对外提供getter(管理者无权修改)
        public int getHp() { return hp; }
        public int getMp() { return mp; }
        public int getX() { return x; }
        public int getY() { return y; }
    }
}
// 存档管理器
public class SaveManager {
    private Map<String, GameCharacter.CharacterMemento> saves = new HashMap<>();
    public void save(String slot, GameCharacter.CharacterMemento memento) {
        saves.put(slot, memento);
    }
    public GameCharacter.CharacterMemento load(String slot) {
        return saves.get(slot);
    }
}
// 客户端
GameCharacter hero = new GameCharacter(100, 50, 0, 0);
SaveManager manager = new SaveManager();
hero.receiveDamage(30);  // 战斗后HP为70
manager.save("save1", hero.saveProgress());
hero.setPosition(10, 20);
hero.receiveDamage(50);  // 再受伤害HP为20
// Boss战前读档
hero.loadProgress(manager.load("save1"));
System.out.println(hero); // HP:70 MP:50 位置:(0,0)

输出HP:70 MP:50 位置:(0,0)


数据库事务回滚模拟

需求:模拟一个简单的银行转账场景,如果转账失败需回滚账户余额。

public class BankAccount {
    private double balance;
    public BankAccount(double balance) {
        this.balance = balance;
    }
    public void debit(double amount) {
        if (amount > 0 && balance >= amount) {
            balance -= amount;
        }
    }
    public void credit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }
    public AccountMemento save() {
        return new AccountMemento(balance);
    }
    public void restore(AccountMemento m) {
        this.balance = m.getBalance();
    }
    public double getBalance() { return balance; }
    // 备忘录类
    public static class AccountMemento {
        private final double balance;
        private AccountMemento(double balance) {
            this.balance = balance;
        }
        private double getBalance() { return balance; }
    }
}
// 事务管理器
public class TransactionManager {
    public boolean transfer(BankAccount from, BankAccount to, double amount) {
        BankAccount.AccountMemento mementoFrom = from.save();
        BankAccount.AccountMemento mementoTo = to.save();
        try {
            from.debit(amount);
            to.credit(amount);
            // 模拟失败场景(如余额不足)
            if (from.getBalance() < 0) {
                throw new RuntimeException("余额不足");
            }
            return true; // 提交
        } catch (Exception e) {
            // 回滚
            from.restore(mementoFrom);
            to.restore(mementoTo);
            System.out.println("事务回滚:" + e.getMessage());
            return false;
        }
    }
}

要点:在业务操作前保存快照,异常时恢复,保证数据一致性。


备忘录模式在Java中的常见陷阱与优化

陷阱1:备忘录对象过大

当发起人状态包含大量数据(如整个文档),每次保存完整快照会导致内存消耗暴增。
优化方案

  • 使用“差异备忘录”,只保存变更的部分。
  • 结合序列化,将备忘录写入磁盘(如游戏存档)。

陷阱2:封装性被破坏

如果备忘录类被定义为public且对外暴露setter,管理者可能非法修改内部状态。
正确做法

  • 使用内部类,并将构造器和getter设为private(仅发起人可访问)。
  • 或者使用接口隔离:定义一个Memento接口(只有getter),内部实现类实现具体setter。

陷阱3:状态恢复不完整

某些对象包含不可序列化的资源(如数据库连接、文件句柄),备忘录模式无法保存这些。
解决方法

  • 将状态与资源分离,备忘录只保存可序列化部分。
  • 或者重写对象的clone()方法(注意深拷贝问题)。

性能优化技巧

  • 使用LruCache管理备忘录数量,避免无限存储。
  • 在关键节点(如游戏Boss战前)才保存快照,而非每次动作都存。

问答环节:开发者最常遇到的5个问题

Q1:备忘录模式与原型模式(Prototype)有何区别?
A:原型模式侧重于通过克隆创建新对象,而备忘录模式侧重于保存和恢复对象状态,原型可作用于任意对象,备忘录需要专门设计发起人与管理者的协作关系。

Q2:为什么备忘录类要使用内部类?
A:内部类可以访问外部类的私有成员,同时可以将自己的构造器和方法设为私有,从而只允许外部类(发起人)创建和访问,管理者只能持有引用却无法修改内容。

Q3:在Java中,如何实现“撤销到任意步骤”而非仅上一步?
A:将管理者的数据结构从Stack改为ListMap,并记录每个快照的时间戳或版本号,用户通过索引或ID选择恢复点。

Q4:动态代理可以替代备忘录模式吗?
A:不可以,动态代理主要用于方法拦截,无法自动保存对象内部状态,备忘录模式专注于“快照”语义,与代理职责不同。

Q5:哪些场景不适合备忘录模式?
A:对象状态极其复杂且频繁变化(如实时渲染引擎);对象包含大量无法序列化的资源;或者期望以极低内存开销实现撤销功能(此时应考虑命令模式+反向操作)。


备忘录模式是Java开发中实现“撤销/恢复”功能最常见的方案,其核心在于封装状态快照职责分离,通过本文的三个案例(文本编辑器、游戏存档、事务回滚),你已掌握从简单到复杂场景的实战技巧,在实际项目中,建议结合Serializable接口或Jackson库来序列化复杂对象的备忘录,同时注意控制快照粒度和存储策略,掌握这个模式后,你就能为你的程序轻松添加“时间回溯”的超能力了。

抱歉,评论功能暂时关闭!