## 引入
在閻宏博士的《JAVA與模式》一書中開頭是這樣描述備忘錄(Memento)模式的:
> 備忘錄模式又叫做快照模式(Snapshot Pattern)或Token模式,是對象的行為模式。
備忘錄對象是一個用來存儲另外一個對象內部狀態的快照的對象。備忘錄模式的用意是在不破壞封裝的條件下,將一個對象的狀態捕捉(Capture)住,并外部化,存儲起來,從而可以在將來合適的時候把這個對象還原到存儲起來的狀態。備忘錄模式常常與命令模式和迭代子模式一同使用。
## 定義
所謂備忘錄模式就是在不破壞封裝的前提下,捕獲一個對象的內部狀態,并在該對象之外保存這個狀態,這樣可以在以后將對象恢復到原先保存的狀態。
備忘錄模式將要保存的細節給封裝在備忘錄中,就是那天要改變保存的細節也不會影響到客戶端。
## 結構
備忘錄模式的結構圖如下所示

備忘錄模式所涉及的角色有三個:備忘錄(Memento)角色、發起人(Originator)角色、負責人(Caretaker)角色。
**備忘錄(Memento)角色**
備忘錄角色又如下責任:
* (1)將發起人(Originator)對象的內戰狀態存儲起來。備忘錄可以根據發起人對象的判斷來決定存儲多少發起人(Originator)對象的內部狀態。
* (2)備忘錄可以保護其內容不被發起人(Originator)對象之外的任何對象所讀取。
備忘錄有兩個等效的接口:
* 窄接口:負責人(Caretaker)對象(和其他除發起人對象之外的任何對象)看到的是備忘錄的窄接口(narrow interface),這個窄接口只允許它把備忘錄對象傳給其他的對象。
* 寬接口:與負責人對象看到的窄接口相反的是,發起人對象可以看到一個寬接口(wide interface),這個寬接口允許它讀取所有的數據,以便根據這些數據恢復這個發起人對象的內部狀態。
**發起人(Originator)角色**
發起人角色有如下責任:
* (1)創建一個含有當前的內部狀態的備忘錄對象。
* (2)使用備忘錄對象存儲其內部狀態。
**負責人(Caretaker)角色**
負責人角色有如下責任:
* (1)負責保存備忘錄對象。
* (2)不檢查備忘錄對象的內容。
## 代碼實現
小時候都有過打游戲的經歷,在一個小游戲中,每過一關都可以存檔,當挑戰下一關沒有通過時,可以直接通過讀檔這一操作,從通過的最近一關開始過起。而不是從第一關重新再打。現在通過備忘錄模式,演示這個場景。
### “白箱”備忘錄模式的實現
備忘錄角色對任何對象都提供一個接口,即寬接口,備忘錄角色的內部所存儲的狀態就對所有對象公開。因此這個實現又叫做“白箱實現”。
“白箱”實現將發起人角色的狀態存儲在一個大家都看得到的地方,因此是破壞封裝性的。但是通過程序員自律,同樣可以在一定程度上實現模式的大部分用意。因此白箱實現仍然是有意義的。
下面給出一個示意性的“白箱實現”。
原生器類(發起者)
```
public class Originator {
private long missionNumber;
//添加備份
public Memento createMemento(){
return new Memento(this.missionNumber);
}
//恢復備份
public void restoreMemento(Memento memento){
setMissionNumber(memento.getMissionNumber());
}
public long getMissionNumber() {
return missionNumber;
}
public void setMissionNumber(long missionNumber) {
this.missionNumber = missionNumber;
}
}
```
備忘錄類
```
public class Memento {
//記錄游戲的關卡數
private long missionNumber;
public Memento(long missionNumber){
this.missionNumber = missionNumber;
}
public long getMissionNumber() {
return missionNumber;
}
public void setMissionNumber(long missionNumber) {
this.missionNumber = missionNumber;
}
}
```
管理者類
```
public class Caretaker {
//備忘錄
private Memento memento;
public Memento getMemento() {
return memento;
}
public void setMemento(Memento memento) {
this.memento = memento;
}
}
```
測試類
```
public class MementoTest {
public static void main(String[] args) {
Originator originator = new Originator();
originator.setMissionNumber(99);
System.out.println("現在正在過游戲的99關");
Caretaker caretaker = new Caretaker();
caretaker.setMemento(originator.createMemento());
System.out.println("成功過關,開始存檔");
originator.setMissionNumber(100);
System.out.println("現在正在過游戲的100關");
System.out.println("很遺憾闖關失敗,啟動備忘錄模式,回到最近通過的那一關");
originator.restoreMemento(caretaker.getMemento());
System.out.println("現在關數: "+originator.getMissionNumber());
}
}
```
輸出:

### “黑箱”備忘錄模式的實現
備忘錄角色對發起人(Originator)角色對象提供一個寬接口,而為其他對象提供一個窄接口。這樣的實現叫做“黑箱實現”。
在JAVA語言中,實現雙重接口的辦法就是將備忘錄角色類設計成發起人角色類的內部成員類。
將Memento設成Originator類的內部類,從而將Memento對象封裝在Originator里面;在外部提供一個標識接口MementoIF給Caretaker以及其他對象。這樣,Originator類看到的是Menmento的所有接口,而Caretaker以及其他對象看到的僅僅是標識接口MementoIF所暴露出來的接口。
發起人角色類Originator中定義了一個內部的Memento類。由于此Memento類的全部接口都是私有的,因此只有它自己和發起人類可以調用。
```
public class Originator2 {
private long missionNumber;
//添加備份
public MementoIF createMemento() {
return new Memento(this.missionNumber);
}
//恢復備份
public void restoreMemento(MementoIF memento) {
setMissionNumber(((Memento)memento).getMissionNumber());
}
public long getMissionNumber() {
return missionNumber;
}
public void setMissionNumber(long missionNumber) {
this.missionNumber = missionNumber;
}
private class Memento implements MementoIF{
private long missionNumber;
public Memento(long missionNumber){
this.missionNumber = missionNumber;
}
public long getMissionNumber() {
return missionNumber;
}
public void setMissionNumber(long missionNumber) {
this.missionNumber = missionNumber;
}
}
}
```
窄接口MementoIF,這是一個標識接口,因此它沒有定義出任何的方法。
```
public interface MementoIF {
}
```
負責人角色類Caretaker能夠得到的備忘錄對象是以MementoIF為接口的,由于這個接口僅僅是一個標識接口,因此負責人角色不可能改變這個備忘錄對象的內容。
```
public class Caretaker {
//備忘錄
private MementoIF memento;
public MementoIF getMemento() {
return memento;
}
public void setMemento(MementoIF memento) {
this.memento = memento;
}
}
```
客戶端角色類
```
public class MementoTest {
public static void main(String[] args) {
Originator2 originator = new Originator2();
originator.setMissionNumber(99);
System.out.println("現在正在過游戲的99關");
Caretaker caretaker = new Caretaker();
caretaker.setMemento(originator.createMemento());
System.out.println("成功過關,開始存檔");
originator.setMissionNumber(100);
System.out.println("現在正在過游戲的100關");
System.out.println("很遺憾闖關失敗,啟動備忘錄模式,回到最近通過的那一關");
originator.restoreMemento(caretaker.getMemento());
System.out.println("現在關數: "+originator.getMissionNumber());
}
}
```
### 多重檢查點
前面所給出的白箱和黑箱的示意性實現都是只存儲一個狀態的簡單實現,也可以叫做只有一個檢查點。常見的系統往往需要存儲不止一個狀態,而是需要存儲多個狀態,或者叫做有多個檢查點。
備忘錄模式可以將發起人對象的狀態存儲到備忘錄對象里面,備忘錄模式可以將發起人對象恢復到備忘錄對象所存儲的某一個檢查點上。下面給出一個示意性的、有多重檢查點的備忘錄模式的實現。
發起人角色源代碼
```
public class Originator3 {
private List<String> names;
private int index;
public Originator3(){
names = new ArrayList<>();
index = 0;
}
//添加備份
public Memento createMemento() {
return new Memento(names, index);
}
//恢復備份
public void restoreMemento(Memento memento) {
names = memento.getNames();
index = memento.getIndex();
}
public void setName(String name) {
names.add(name);
index++;
}
public void printNames(){
for (String name: names){
System.out.println(name);
}
}
}
```
備忘錄角色類,這個實現可以存儲任意多的狀態,外界可以使用檢查點指數index來取出檢查點上的狀態。
```
public class Memento {
//記錄游戲的關卡數
private List<String> names;
private int index;
public List<String> getNames() {
return names;
}
public int getIndex() {
return index;
}
public Memento(List<String> names, int index) {
this.names = new ArrayList<>(names);//這里必須要New
this.index = index;
}
}
```
負責人角色類
```
public class Caretaker {
//備忘錄
private List<Memento> mementos = new ArrayList<>();
private Originator3 originator;
private int current;
public Caretaker(Originator3 originator) {
this.originator = originator;
current = 0;
}
/**
* 創建一個新的檢查點
* @return
*/
public int createMemento(){
Memento memento = originator.createMemento();
mementos.add(memento);
return current++;
}
/**
* 將發起人恢復到某個檢查點
* @param index
*/
public void restoreMemento(int index){
Memento memento = mementos.get(index);
originator.restoreMemento(memento);
}
public void removeMemento(int index){
mementos.remove(index);
}
}
```
```
public class MementoTest {
public static void main(String[] args) {
Originator3 originator = new Originator3();
originator.setName("第一關");
System.out.println("現在正在過游戲的第一關");
Caretaker caretaker = new Caretaker(originator);
caretaker.createMemento();
System.out.println("成功過關,開始存檔");
originator.setName("第二關");
System.out.println("現在正在過游戲的第二關");
caretaker.createMemento();
System.out.println("成功過關,開始存檔");
originator.setName("第三關");
System.out.println("現在正在過游戲的第三關");
caretaker.createMemento();
System.out.println("成功過關,開始存檔");
originator.setName("第四關");
System.out.println("現在正在過游戲的第四關");
System.out.println("很遺憾闖關失敗,當前記錄為");
originator.printNames();
System.out.println("啟用備份,回退");
caretaker.restoreMemento(1);
originator.printNames();
}
}
```
### “自述歷史”模式
所謂“自述歷史”模式(History-On-Self Pattern)實際上就是備忘錄模式的一個變種。在備忘錄模式中,發起人(Originator)角色、負責人(Caretaker)角色和備忘錄(Memento)角色都是獨立的角色。雖然在實現上備忘錄類可以成為發起人類的內部成員類,但是備忘錄類仍然保持作為一個角色的獨立意義。在“自述歷史”模式里面,發起人角色自己兼任負責人角色。
“自述歷史”模式的類圖如下所示:

備忘錄角色有如下責任:
* (1)將發起人(Originator)對象的內部狀態存儲起來。
* (2)備忘錄可以保護其內容不被發起人(Originator)對象之外的任何對象所讀取。
發起人角色有如下責任:
* (1)創建一個含有它當前的內部狀態的備忘錄對象。
* (2)使用備忘錄對象存儲其內部狀態。
客戶端角色有負責保存備忘錄對象的責任。
發起人角色同時還兼任負責人角色,也就是說它自己負責保持自己的備忘錄對象。
```
public class Originator4 {
private long missionNumber;
//添加備份
public MementoIF createMemento() {
return new Memento(this);
}
//恢復備份
public void restoreMemento(MementoIF memento) {
setMissionNumber(((Memento) memento).getMissionNumber());
}
public long getMissionNumber() {
return missionNumber;
}
public void setMissionNumber(long missionNumber) {
this.missionNumber = missionNumber;
}
private class Memento implements MementoIF {
private long missionNumber;
/**
* 構造方法
*/
private Memento(Originator4 o) {
this.missionNumber = o.missionNumber;
}
private long getMissionNumber() {
return missionNumber;
}
}
}
```
窄接口MementoIF,這是一個標識接口,因此它沒有定義出任何的方法。
```
public interface MementoIF {
}
```
客戶端角色
```
public class MementoTest {
public static void main(String[] args) {
Originator4 originator = new Originator4();
originator.setMissionNumber(99);
System.out.println("現在正在過游戲的99關");
System.out.println("成功過關,開始存檔");
MementoIF mementoIF = originator.createMemento();//存檔99
originator.setMissionNumber(100);
System.out.println("現在正在過游戲的100關");
System.out.println("很遺憾闖關失敗,啟動備忘錄模式,回到最近通過的那一關");
originator.restoreMemento(mementoIF);
System.out.println("現在關數: "+originator.getMissionNumber());
}
}
```
由于“自述歷史”作為一個備忘錄模式的特殊實現形式非常簡單易懂,它可能是備忘錄模式最為流行的實現形式。
## 優點
* 有時一些發起人對象的內部信息必須保存在發起人對象以外的地方,但是必須由發起人對象自己讀取。這時使用備忘錄模式可以將復雜的發起人內部信息對其它對象屏蔽起來。從而可以恰當的保持封裝的邊界。
* 備忘錄模式簡化了發起者類。發起者不再需要管理和保存其內部狀態的一個個版本。客戶端可以自行管理他們所需要的這些狀態的版本。
* 當發起者角色的狀態發生改變的時候,有可能這個狀態無效,需要回滾到前一個狀態,這時候可以使用暫時存儲起來的備忘錄將狀態進行還原。
## 缺點
* 如果發起者角色的狀態有很多并且需要完整的儲存到備忘錄對象中,那么備忘錄對象會很消耗資源。
**注意事項**
備忘錄模式最理想的情況是只允許生成該備忘錄的那個原發器能夠訪問這個備忘錄的內部狀態。
## 適用場景
* 1、 需要保存一個對象在某一個時刻的狀態或部分狀態。
* 2、 如果用一個接口來讓其他對象得到這些狀態,將會暴露對象的實現細節并破壞對象的封裝性,一個對象不希望外界直接訪問其內部狀態,通過負責人可以間接訪問其內部狀態。
## 總結
* 1、 備忘錄模式可以實現在不破壞封裝的前提下,捕獲一個類的內部狀態,并且在該對象之外保存該對象的狀態,保證該對象能夠恢復到歷史的某個狀態。
* 2、 備忘錄模式實現了內部狀態的封裝,除了創建它的原發器之外其他對象都不能夠訪問它。
* 3、 備忘錄模式會占用較多的內存,消耗資源。
- java
- 設計模式
- 設計模式總覽
- 設計原則
- 工廠方法模式
- 抽象工廠模式
- 單例模式
- 建造者模式
- 原型模式
- 適配器模式
- 裝飾者模式
- 代理模式
- 外觀模式
- 橋接模式
- 組合模式
- 享元模式
- 策略模式
- 模板方法模式
- 觀察者模式
- 迭代子模式
- 責任鏈模式
- 命令模式
- 備忘錄模式
- 狀態模式
- 訪問者模式
- 中介者模式
- 解釋器模式
- 附錄
- JVM相關
- JVM內存結構
- Java虛擬機的內存組成以及堆內存介紹
- Java堆和棧
- 附錄-數據結構的堆棧和內存分配的堆區棧區的區別
- Java內存之Java 堆
- Java內存之虛擬機和內存區域概述
- Java 內存之方法區和運行時常量池
- Java 內存之直接內存(堆外內存)
- JAVA內存模型
- Java內存模型介紹
- 內存模型如何解決緩存一致性問題
- 深入理解Java內存模型——基礎
- 深入理解Java內存模型——重排序
- 深入理解Java內存模型——順序一致性
- 深入理解Java內存模型——volatile
- 深入理解Java內存模型——鎖
- 深入理解Java內存模型——final
- 深入理解Java內存模型——總結
- 內存可見性
- JAVA對象模型
- JVM內存結構 VS Java內存模型 VS Java對象模型
- Java的對象模型
- Java的對象頭
- HotSpot虛擬機
- HotSpot虛擬機對象探秘
- 深入分析Java的編譯原理
- Java虛擬機的鎖優化技術
- 對象和數組并不是都在堆上分配內存的
- 垃圾回收
- JVM內存管理及垃圾回收
- JVM 垃圾回收器工作原理及使用實例介紹
- JVM內存回收理論與實現(對象存活的判定)
- JVM參數及調優
- CMS GC日志分析
- JVM實用參數(一)JVM類型以及編譯器模式
- JVM實用參數(二)參數分類和即時(JIT)編譯器診斷
- JVM實用參數(三)打印所有XX參數及值
- JVM實用參數(四)內存調優
- JVM實用參數(五)新生代垃圾回收
- JVM實用參數(六) 吞吐量收集器
- JVM實用參數(七)CMS收集器
- JVM實用參數(八)GC日志
- Java性能調優原則
- JVM 優化經驗總結
- 面試題整理
- 面試題1
- java日志規約
- Spring安全
- OAtuth2.0簡介
- Spring Session 簡介(一)
- Spring Session 簡介(二)
- Spring Session 簡介(三)
- Spring Security 簡介(一)
- Spring Security 簡介(二)
- Spring Security 簡介(三)
- Spring Security 簡介(四)
- Spring Security 簡介(五)
- Spring Security Oauth2 (一)
- Spring Security Oauth2 (二)
- Spring Security Oauth2 (三)
- SpringBoot
- Shiro
- Shiro和Spring Security對比
- Shiro簡介
- Session、Cookie和Cache
- Web Socket
- Spring WebFlux