ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

设计模式 21 · 备忘录模式

设计模式 21 · 备忘录模式

命令模式那篇讲"撤销"时,我们埋了个钩子:命令模式靠"每个命令记住自己的逆操作"来撤销,但如果对象状态很复杂、记录逆操作不方便,还有另一条路——直接给对象拍个快照,撤销时整个恢复。这条路,就是这一篇的备忘录模式(Memento)

备忘录模式解决的问题一句话:在不破坏封装的前提下,捕获一个对象的内部状态,并在需要时把它恢复到这个状态。关键词是"捕获状态"“恢复"和"不破坏封装”。最形象的类比就是游戏存档:你在打一个 Boss 前"存个档",万一打输了,就"读档"回到存档那一刻,一切重来。这个"存档"就是一份备忘录——它保存了游戏在某一刻的完整状态,让你能随时回滚。

我们的订单场景里,一个天然的例子是订单编辑的"草稿"功能:用户在后台编辑一个复杂订单(改地址、调数量、加优惠、改备注……),编辑到一半想"存个草稿",过会儿如果改乱了,能"恢复到草稿那一刻"。这就需要在某个时间点,把订单的完整状态"拍照"存下来,之后能整个恢复回去。备忘录模式,就是干这件事的标准方案。

这一篇和命令模式、以及第 6 篇原型模式都有呼应(它们都涉及"保存状态"),我会讲清它们的分工。因为它的核心思想比较直接,篇幅上会比策略、状态那些略简洁。

这篇文章按这条线索展开:先看"想恢复状态,却要么做不到、要么破坏封装"的两难;再引出备忘录如何优雅地捕获和恢复状态;然后讲清它的三个角色、以及"不破坏封装"这个精妙之处;接着说它和命令、原型的分工与现实身影;最后给出适用边界。贯穿例是订单草稿。

目录

  1. 想恢复状态,却陷入两难
  2. 备忘录模式:给状态拍快照
  3. 三个角色,与"不破坏封装"的精妙
  4. 和命令、原型的分工,以及现实身影
  5. 什么时候用备忘录模式

一、想恢复状态,却陷入两难

看订单草稿。用户编辑订单,想在某一刻存个草稿,之后能恢复。假设订单是这样:

publicclassOrder{privateStringaddress;privateintcount;privateStringcoupon;privateStringremark;// ... 一堆状态字段}

要实现"存草稿 + 恢复",你会发现自己陷入两难:

做法一:把所有字段暴露出去,让外面存。外面拿到所有字段存起来,恢复时再一个个设回去:

// 存草稿:把 order 的每个字段都读出来存着StringsavedAddress=order.getAddress();intsavedCount=order.getCount();StringsavedCoupon=order.getCoupon();// ... 恢复时再一个个 setXxx 回去

问题:这彻底破坏了封装。外面被迫知道了订单的所有内部字段,还得有权限读写它们。而且订单以后加一个字段,所有"存草稿"的地方都得记得同步加一行——极易遗漏,漏一个草稿就少存一个字段。订单的内部状态,本不该这样赤裸裸地摊给外面。

做法二:让订单自己往外吐一个"完整状态对象"。好一点,但如果这个状态对象把所有字段的 getter/setter 都开放了,外面依然能随意读写订单的内部——封装还是破了。

问题的两难在于:"保存和恢复状态"这件事,天然需要访问对象的内部;但直接把内部暴露给外面,又破坏了封装。我们真正想要的是:让订单自己负责"打包"它的内部状态成一个"备忘录",这个备忘录对外是个"黑盒"(别人拿到也看不懂、改不了里面),但订单自己能从这个备忘录里"还原"出当时的状态。状态的打包和还原,都由订单自己做,外面只负责"保管"这个黑盒备忘录。这就是备忘录模式的精妙之处。

二、备忘录模式:给状态拍快照

备忘录模式的做法:由对象自己(原发器)创建一个"备忘录"来保存自己的内部状态;备忘录对外是个封闭的黑盒;需要恢复时,把备忘录交还给对象,由对象自己从里面还原状态。一个"管理者"负责保管这些备忘录,但不能窥探其内容。

第一步,备忘录类(保存状态的黑盒):

// 备忘录:保存订单某一刻的状态。对外是黑盒publicclassOrderMemento{privatefinalStringaddress;privatefinalintcount;privatefinalStringcoupon;privatefinalStringremark;// 构造和 getter 是"包级私有"或仅对 Order 可见 —— 外面拿不到内容OrderMemento(Stringaddress,intcount,Stringcoupon,Stringremark){this.address=address;this.count=count;this.coupon=coupon;this.remark=remark;}StringgetAddress(){returnaddress;}// 仅 Order 能访问// ... 其余 getter 同样受限}

第二步,原发器(订单)——它自己会"打包"和"还原"状态:

publicclassOrder{privateStringaddress;privateintcount;privateStringcoupon;privateStringremark;// 存草稿:把当前状态打包成一个备忘录publicOrderMementosave(){returnnewOrderMemento(address,count,coupon,remark);}// 恢复:从备忘录里还原状态(由订单自己读,不破坏封装)publicvoidrestore(OrderMementomemento){this.address=memento.getAddress();this.count=memento.getCount();this.coupon=memento.getCoupon();this.remark=memento.getRemark();}}

第三步,管理者(草稿箱)——只负责保管备忘录,不看内容:

publicclassDraftCaretaker{privateOrderMementodraft;// 保管草稿,但看不懂里面是什么publicvoidsave(OrderMementom){this.draft=m;}publicOrderMementoget(){returndraft;}}

用起来,存草稿和恢复都干净利落:

Orderorder=...;DraftCaretakercaretaker=newDraftCaretaker();caretaker.save(order.save());// 编辑到一半,存个草稿order.setCount(99);// 继续瞎改...改乱了order.restore(caretaker.get());// 恢复到草稿那一刻,瞎改的全没了

对比第一节,升级点非常清晰:订单的状态由订单自己打包成备忘录、也由它自己还原,外面(草稿箱)只拿到一个"看不懂、改不了"的黑盒,全程没有暴露订单的任何内部字段——封装完好无损;而"存草稿 + 恢复"这个需求,优雅地实现了。用一张图看这个"拍快照 + 保管 + 还原"的三方协作最清楚:

图里最该记住的,是那个只有原发器能看懂、管理者只能"保管不能拆开"的黑盒备忘录。这就是备忘录模式和"做法一"最本质的区别:状态的读写权限,始终牢牢握在对象自己手里,外部永远碰不到内部字段。

三、三个角色,与"不破坏封装"的精妙

备忘录模式的角色,三个:

角色本例中是谁职责
原发器(Originator)Order创建备忘录保存状态、从备忘录恢复状态
备忘录(Memento)OrderMemento保存原发器的内部状态,对外封闭
管理者(Caretaker)DraftCaretaker保管备忘录,但不能读取/修改其内容

这个模式最精妙、也最容易被做错的地方,在于**“不破坏封装”** 这四个字。它靠一个巧妙的设计实现:备忘录对"原发器"是开放的(原发器能读写它的全部状态),但对"管理者"和其他所有人是封闭的(只能持有引用、不能看内容)。

在 Java 里,这通常靠访问权限来实现,常见两种手法:

  • 内部类:把Memento做成Originator的私有内部类,这样只有Originator能访问它的字段,外面连它长什么样都看不到,只能拿到一个Object或一个空接口引用。
  • 包级私有:像上面代码那样,把Memento的构造和 getter 设成"包级私有"(不写public),只有同包的Order能访问,外面调不了。

核心思想是:管理者只是个"保管箱",它保管的是一个密封的信封,能存能取,但拆不开。只有原发器持有拆信封的钥匙。这样一来,"保存/恢复状态需要访问内部"和"不想暴露内部"这对矛盾,就被巧妙地化解了——访问内部的权限,只给了对象自己。

四、和命令、原型的分工,以及现实身影

备忘录和前面两个模式(命令、原型)都涉及"状态",容易混,理清分工:

  • 备忘录 vs 命令(第 19 篇)的撤销:两者都能实现"撤销",但思路不同。命令的撤销是"记住逆操作"(改数量的逆操作是改回旧数量),是增量式的——记住"怎么反着做一遍"。备忘录的撤销是"存整个快照",是全量式的——直接把整个状态存下来,恢复时整个覆盖。状态简单、逆操作好写,用命令;状态复杂、逆操作难写,用备忘录直接存快照。而且两者常配合:命令模式做撤销时,如果操作难以逆转,就在命令里存一个备忘录,undo()时恢复它。
  • 备忘录 vs 原型(第 6 篇):原型是"克隆一个完整的新对象",备忘录是"保存状态、之后恢复到原对象"。原型的产物是个能独立使用的新对象;备忘录的产物是个只用于"恢复"的黑盒状态。实现上,备忘录有时会用到克隆(把状态深拷贝一份存起来),但意图不同——原型为了"造新的",备忘录为了"存旧的以便回滚"。

现实身影:

  • 数据库事务的回滚(savepoint):事务开始时的状态相当于一个备忘录,回滚就是恢复到那个状态。
  • 各种编辑器/IDE 的撤销、游戏存档、浏览器的会话恢复:都是备忘录思想——保存某一刻的完整状态,以便回到那一刻。
  • Serializable序列化:把对象序列化成字节存起来、之后反序列化恢复,本质也是一种备忘录(把整个对象状态打包保存)。

一个识别信号:凡是"保存某一刻的完整状态,以便日后整个恢复回去"(存档/读档、快照/回滚),就是备忘录。

五、什么时候用备忘录模式

适合用备忘录模式的信号:

  • 你需要保存对象某一刻的完整状态,并能在之后恢复到那个状态(存档、草稿、快照);
  • 你想实现撤销/回滚,但对象状态复杂、"记逆操作"不划算,直接存快照更省事;
  • 你希望在不暴露对象内部结构的前提下做这件事。

不必用的信号:

  • 对象状态就一两个简单字段——那随手存一下就行,套三个角色是过度设计;
  • 撤销的逆操作很简单——那用命令模式的undo()更轻量;
  • 状态对象非常大、且要频繁存快照——注意内存开销:每个备忘录都是一份完整状态,存太多、太大会吃内存(可以考虑只存关键字段、或限制快照数量)。

判断的核心还是那句话:先确认真的需要"保存并整体恢复完整状态",且在意封装,备忘录才值得上。还要留意它的内存成本——快照不是免费的。


小结。备忘录模式在不破坏封装的前提下,捕获对象的内部状态并支持恢复:由对象自己(原发器)把状态打包成一个"黑盒备忘录",管理者只负责保管(能存取、拆不开),恢复时由对象自己从备忘录还原——访问内部的权限始终握在对象手里,靠内部类或包级私有实现这份封装。它是"存档/读档"“草稿/恢复”"快照/回滚"的标准方案。它和命令模式的撤销互补(增量记逆操作 vs 全量存快照,可配合),和原型的克隆意图不同(造新的 vs 存旧的)。用它要留意内存开销。下一篇是行为型的收尾,我们把三个相对冷门、适用面很窄的模式——中介者、访问者、解释器——放在一篇里集中讲透,重点说清它们各自解决什么问题、以及为什么大多数时候你并不需要它们。

返回列表