设计模式与生活⑮:看到哪儿了,得有个地方替你记着

周日,睡到自然醒。

没有闹钟,也没什么事催着。醒了先躺着听一会儿窗外的声音,楼下有人遛狗,远处有车经过。

起来做了个早餐。煎两个蛋,热一杯牛奶。吃完出门散了会儿步。

下午窝在沙发上看电影。看到一半,起身泡了杯茶,顺手按了暂停。回来坐下,点开,进度条还停在刚才那儿。

晚上看书,看着看着眼皮沉了。合上书,关灯睡觉。

一天下来没干什么大事,但每一件都能顺顺当当接着做。不是记性好,是那些东西替他记着了。

看到哪儿了,书替你记着

今天聊的备忘录模式(Memento):把对象当前的状态装进一个盒子,交给别人保管,需要时原样还回来。系列第十五篇。

01 先看"自己伸手记"的办法

最直接的写法,是把阅读器里的状态摊开。调用方想知道读到哪儿,就自己读字段、自己存。

java 复制代码
class EbookReader {
    int chapter;
    int charOffset;
}

状态直接露在外面

保存和恢复,就变成这样:

java 复制代码
int savedChapter = reader.chapter;
int savedOffset = reader.charOffset;

// 合上书,过一会儿再打开
reader.chapter = savedChapter;
reader.charOffset = savedOffset;

每一步都靠外面自己搬

能跑。但问题在于,这份状态本来该由阅读器自己管,现在却被搬到外面,同包的地方都能伸手改。

哪天阅读器内部换了一种定位方式,比如把章节和偏移合成一个 position 字段,所有存过这几个字段的地方都得跟着改。而且外部拿到这些字段之后,很容易顺手多做一点事:改一个偏移,或者保存时漏掉一个字段。

书签是书自己夹的,位置也该由书自己知道。

02 让对象自己打包一份

给阅读器加两个方法:mark() 打个包,resumeFrom() 按包恢复。

java 复制代码
class EbookReader {
    private String book;
    private int chapter;
    private int charOffset;

    // ...
    ReadingMark mark() {
        return new ReadingMark(chapter, charOffset);
    }

    void resumeFrom(ReadingMark mark) {
        this.chapter = mark.chapter();
        this.charOffset = mark.charOffset();
    }
}

阅读器自己知道该存什么

这个包就是备忘录(Memento):

java 复制代码
final class ReadingMark {
    private final int chapter;
    private final int charOffset;

    ReadingMark(int chapter, int charOffset) {
        this.chapter = chapter;
        this.charOffset = charOffset;
    }

    int chapter() {
        return chapter;
    }

    int charOffset() {
        return charOffset;
    }
}

字段只读,外面改不动

ReadingMark 的字段是 final,构造器也没有开放成 public。外面拿到了,只能整个交给阅读器,没法一个字段一个字段地动手脚。这就叫保存状态,但不把内部结构暴露出去。

顺带说一句:如果状态里装的是 List、Map 这种可变对象,就不能只存引用。存进去的是同一份,后面原对象一改,存下来的那份也跟着变。要复制一份再放进去。这个坑,Day 12 讲原型时已经踩过。

03 书签夹只管收着,不关心里面是什么

存下来的书签(也就是那份快照)总得有地方放。这个保管者就是Caretaker(保管人):

java 复制代码
class MarkShelf {
    private final Deque<ReadingMark> marks = new ArrayDeque<>();

    void keep(ReadingMark mark) {
        marks.push(mark);
    }

    ReadingMark recent() {
        return marks.peek();
    }
}

只管收,不拆开看

注意,MarkShelf 没有任何方法去读 chapter 或 charOffset。它根本不关心里面装的是什么,只负责"收好"和"拿出来"。

组装起来是这样:

java 复制代码
EbookReader reader = new EbookReader("一本小说");
MarkShelf shelf = new MarkShelf();

reader.goTo(3, 1200);              // 读到第三章
shelf.keep(reader.mark());         // 这段写得真好,存个书签

reader.goTo(6, 800);               // 读到第六章
shelf.keep(reader.mark());         // 这段也想回头再看

reader.goTo(9, 2600);              // 继续往后读
reader.printPosition();

reader.resumeFrom(shelf.recent()); // 点最近那个书签
reader.printPosition();

运行结果:

text 复制代码
一本小说 · 第 9 章,字符偏移 2600
一本小说 · 第 6 章,字符偏移 800

读到第九章,点一下书签,回到第六章

谁产生状态、谁保存状态、谁保管,各管一段

04 书签记的是位置,不是记忆

这里得挑明几处失真,也顺手把边界划清楚。

第一,书签只记"读到哪儿",不记"读到了什么";备忘录也一样。 它保存的是对象的状态字段,不是你当时的心情、理解或者记忆。恢复之后,书还在那儿,但你已经不是当时那个读到这句话的人了。

第二,现实中的阅读 app 能跨设备接着看,靠的通常不是内存里一直停着那个对象,而是把位置写进本地存储或者账号里,再同步过去。备忘录模式只说明"存一份状态、之后按它恢复"这个结构,不是一套跨端同步方案。

第三,现实里的书签谁都能翻出来看一眼,代码里的快照却不对外开放。 这份限制是模式加上去的,日常书签本来没有。模式真正要保护的是对象内部结构不被外部乱动,不是把书签内容藏成秘密。

第四,真实书签不会问"这是谁的书",快照也不会绑定读者身份。这些现实里的额外信息,是应用层自己加的,不属于模式本身。

05 跟命令的撤销、原型,差在哪儿

命令模式(Day 6)的 undo 是"反向做一遍" :改配置就把老值写回去;像重启服务这种退不回去的,当时也只能把 undo 空着。它对应的是一次操作。

备忘录模式是把对象的状态拉回某个时点 :读到第九章,点书签,回到第六章。它对应的是一个状态。

不过这两者经常配合。Day 6 里 ConfigChange 在 undo 时记得老值,那个"老值"其实就是一份简易备忘录------只不过它只管一个字段、一次操作。状态多了、要回到某个完整时点,才会把备忘录单独抽出来。

原型模式(Day 12)是"复制一个新对象继续用" :复制出来的周报,是另一份能独立修改的东西。备忘录是"存一份状态,之后把原对象改回去":书还是那本书,只是位置变了回去。

一个产出的是"另一个对象",一个产出的是"一份稍后要还回去的状态"。这两个别混。

06 什么时候,别急着做快照

状态就一两个字段,直接存。 比如只记一个页码,那 int page 就够了,外面存一个整数比造一套快照省事。

状态特别大,别全量存。 整个文档内容、整段视频缓冲都塞进快照,内存会顶不住。只存恢复时真正需要的字段,或者只存变化的部分。

没有"回到某个时点"的需求,别存。 只是想知道当前读到哪儿,直接读个字段就行,不用先造快照再恢复。

快照里有可变引用,记得复制。 否则你以为存的是"当时",恢复回来的却是"后来"。

判断标准落在一句话:是否需要在不暴露对象内部结构的前提下,保存某个时刻的完整状态,并在之后原样恢复? 是,备忘录才有意义。

07 一天结束,进度自己会在

书合上,灯关了。书签还夹在那儿,明天翻开就是。

周日这一天,没什么要复盘的。但"看到哪儿了"这件事,总得有个地方替人记着------记在书里,记在进度条上,记在那个只有它自己看得懂的盒子里。

跟前面十四篇对一句:单例管造几个,工厂管怎么造,模板方法管事情怎么走,策略管这一步用什么做法,装饰器管外面加几层,命令管把请求做成对象,代理管谁能碰到真东西,观察者管变化后通知谁,外观管一堆零件只留一个入口,状态管当前状态下怎么做,责任链管请求由哪一环接住,原型管拿哪个现成对象复制,桥接管抽象和实现怎么独立扩展,享元管把能共享的部分只留一份。本篇备忘录,管把某个时刻的状态存下来、再原样恢复。

带走一句话:备忘录 = 把对象此刻的状态装进一个只有它自己看得懂的盒子,交给别人保管,需要时原样还回来。

日子不用存档。读到哪一页,接着读就是了。

睡到自然醒,读到自然困。


参考资料

相关推荐
2601_9679791214 小时前
设计模式 - 访问者模式
设计模式·访问者模式·应用场景·对象结构·操作分离
是2的10次方啊1 天前
设计模式与生活⑭:一栋楼,共用一份热
设计模式
是2的10次方啊2 天前
设计模式与生活⑬:你坐几号线,谁来运营是另一回事
设计模式
Tt有了t3 天前
设计模式速查
java·python·设计模式
一条Go3 天前
四个进程,一把锁:Alioth 的告警评测为什么不需要消息队列
后端·设计模式·架构
一缕82年的清风3 天前
被中产唾弃的“十元快剪”,藏着最狠的极客工程学
人工智能·设计模式
是2的10次方啊3 天前
设计模式与生活⑫:周报又到手,别从空白开始
设计模式
拆房老料4 天前
DeepSeek Super-agent 架构解析:从单任务 Agent 到持续运行的任务系统
人工智能·设计模式·系统架构
工一木子4 天前
设计模式与生活⑩:地铁没变,你换了个状态
设计模式