---
# System prepended metadata

title: Memo 備忘錄模式
tags: [基礎進階, Java 設計模式]

---

---
title: 'Memo 備忘錄模式'
disqus: kyleAlien
---

Memo 備忘錄模式
===

## OverView of Content
備忘錄是一種行為模式，被儲存的物件不會被外部讀取，在不破壞封裝的前提下，獲取、儲存一個物件的內部狀態

又稱為備忘錄 (Memo)

:::success
* 如果喜歡讀更好看一點的網頁版本，可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/)
    
    本篇文章對應的是 [**Memo 備忘錄模式 | 實現與解說 | Android Framwrok Activity 保存**](https://devtechascendancy.com/object-oriented_design_memo_framework/)
:::

[TOC]

## Memo 使用場景
* 需要儲存一個物件在某一個時刻的狀態 (並非執行步驟)

    > 提供 rollback 操作

* 當一個物件不希望直接被讀取到其狀態 (private)，就可 **透過中間物件存取 (這個中間物件就是 Memo)**

### Memo 定義 & Memo UML
* Memo 定義：在不破獲封裝的前提下，保存一個物件的狀態，好讓之後可以恢復該對象的狀態

* Memo UML 角色關係

    | Memo 類角色 | 說明 |
    | -------- | -------- |
    | Originator（發起人） | 讓使用者操控 (可讓使用者直接接觸)，同時包含 Memo 功能 |
    | Memo（備忘錄） | 跟 Originator 有相對應的 member，讓 **使用者無法直接接觸 Memo 類** |
    | Caretaker（管理備忘錄） | **專門用來記錄、管理 Memo 類** |

    :::info
    * Memo 有很清楚得分出類之間的職責，符合單一職責、迪米特（最少知識）原則
    
        > **單一職責**：`Originator` 負責操作備忘錄、`Memo` 負責紀錄數據、`Caretaker` 管理紀錄
        > 
        > **迪米特**：User 不會直接接觸到 Memo，使用者只會知道 `Originator` 
    :::

    > ![](https://i.imgur.com/dJHp5Li.png)


### Memo 優缺點
* **優點** : 
    1. 提供使用者快速方便的恢復機制，方便找回歷史狀態或是 rollback  資料，而這些操作都是安全性的操作
    
    2. 從上面可以看出 **使用者不必關心細節** (`getter`、`setter`)
 
* **缺點** : 
    
    消耗資源，類的增加 (通病)，每次儲存都會消耗資源空間


## Memo 實現


### Memo 標準

1. **`Originator` 類**：
    
    `GameProvider` 讓使用者直接操控的類 (對外暴露)，同時包括 Memo 類的創建、操作，提供給使用者使用

    ```java=
    // Originator

    public class GameProvider {
        private int hp = 100;
        private int mp = 100;

        public Memo createMemo() {
            Memo memo = new Memo();
            memo.hp = hp;
            memo.mp = mp;

            return memo;
        }

        public void restore(Memo memo) {
            if(memo == null) {
                return;
            }

            hp = memo.hp;
            mp = memo.mp;
        }

        public GameProvider attack() {
            mp -= 20;
            return this;
        }

        public GameProvider defense() {
            hp -= 5;
            return this;
        }

        public void finishGame() {
            System.out.println(this);
        }

        @Override
        public String toString() {
            return "Hp: " + hp + ", mp: " + mp;
        }
    }
    ```

2. **`Memo`  類**：
    
    專注於儲存 `Originator` 需要的 Member，該類不會讓使用者直接使用

    ```java=
    public class Memo {
        int hp = 100;
        int mp = 100;
    }
    ```

    
    :::warning
    * `Originator` 是跟 `Memo` 類有相同的 member，所以它必須擁有該類的所有屬性 (可用 `data class`)，在這部分可以用另外一個抽象優化

        > 可以選擇使用 Memo 變形、反射、抽象... 等等
    :::

3. **`Caretaker` 類**：
    
    身為 Memo 管理員，這裏使用 Map 儲存 Memo，專職在 **處理存取**，該類會讓使用者使用

    ```java=
    public class Caretaker {

        private final Map<String, Memo> memoList = new HashMap<>();


        public void setMemo(String name, Memo memo) {
            if(memo == null) {
                return;
            }

            memoList.put(name, memo);
        }

        public Memo getMemo(String name) {
            if(name == null || name.isEmpty()) {
                return null;
            }

            return memoList.get(name);
        }
    }
    ```    
    :::danger
    * 這裡使用 Map 來儲存 Memo 物件，在實際工作上，最好是添加一個上限或是 LRU 樹來替換舊的備份，否則可能導致 OOM !
    :::

* User 使用：
    
    User 必須使用 `Originator` 發起備份、`Caretaker` 管理（儲存、取得）備份

    ```java=
    public class MemoMain {

        public static void main(String[] args) {

            // 備忘錄管理員
            Caretaker caretaker = new Caretaker();

            // 發起者
            GameProvider game = new GameProvider();
            game.attack();
            game.attack();
            game.defense();

            final String player = "Alien";
            // 儲存備忘錄
            caretaker.setMemo(player, game.createMemo());    // 創建備忘錄
            game.finishGame();

            GameProvider game2 = new GameProvider();
            // 使用 memo 紀錄者，讀取紀錄訊息
            game2.restore(caretaker.getMemo(player));
            game2.finishGame();
        }

    }
    ```
    > ![](https://i.imgur.com/9BWMrGg.png)

### Memo 變形 - Clone 取代 Memo

* 這裡我們可以使用 java 的特性，讓需要紀錄的對象實做 `Cloneable` 界面，讓其轉為可 Clone 的對象；優點是可以更簡單的使用、加快性能

    :::warning
    * **不符合單一職責？** 原本的對象需要覆蓋拷貝的責任了！

        的確如此，但這裡我們可以想做，**把拷貝責任封裝到 `Cloneable` 界面**，其實這就算是一個簡易的封裝
        
        > ![](https://i.imgur.com/EJruqTv.png)

    :::
    
    :::danger
    * 這種變形僅限於 **簡單場合**
    
        雖然 clone 可以加快性能，但是我們還是要注意 **深、淺拷貝問題**，避免增加了邏輯複雜度，再次簡化後 UML 如下
        
        > ![](https://i.imgur.com/N8SZkwW.png)

    :::
    
1. **`Originator` 類**：概括了保存、數據複製的工作

    ```kotlin=
    class GameProvider : Cloneable {
        private var hp : Int = 100
        private var mp : Int = 100

        // 取代 Caretaker 功能
        private var backup : GameProvider? = null

        fun createMemo() {
            // 取代 Memo 功能
            backup = this.clone() as GameProvider
        }

        fun restore() {
            backup?.let {
                hp = it.hp
                mp = it.mp
            }
        }

        fun attack() {
            mp -=20
        }

        fun defense() {
            hp -=5
        }

        fun finishGame() {
            println(this)
        }

        override fun toString(): String {
            return "Hp: $hp, mp: $mp"
        }

    }
    ```
    
* **User 使用變形 Memo 設計**：用起來也相當簡化，基本上就算是 `clone` 模式的變化板而已

    ```kotlin=
    fun main() {
        // 發起者
        val game = GameProvider().apply {
            attack()

            createMemo() // 創建備忘錄

            attack()

            defense()

            finishGame()
        }

        game.apply {
            // 恢復
            restore()

            finishGame()
        }
    }
    ```
    > ![](https://i.imgur.com/u0ZYsv4.png)


## Android Framework

Activity 被系統回收前會透過，特殊方法來儲存、恢復資料

| 功能 | Activity 處理的方法 |
| -------- | -------- |
| 暫存 | onSaveInstanceState |
| 恢復 | onRestoreInstanceState |

Memo 設計的概念分類如下

| Memo 設計腳色 | Android 實做 | 說明 |
| -------- | -------- | -------- |
| Memo | Bundle | 設定要緩存、攜帶的資料 |
| Originator | View、ViewGroup | 一般的 View 操作，不過 **多了 Bundle 存取控制** |
| Caretaker | Activity、Fragment | 儲存、恢復 Bundle 數據 |


### Activity - onSaveInstanceState 暫存資料


* 從 `Activity`#`onSaveInstanceState` 開始看，可以看到這裡 **取得 Bundle 對象 (也就是 Memo)，呼叫 [PhoneWindow](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java) 並傳入 Bundle 物件**

    ```java=
    // Activity.java

        private Window mWindow;    // 實作類是 PhoneWindow

        private static final String WINDOW_HIERARCHY_TAG = "android:viewHierarchyState";

        protected void onSaveInstanceState(@NonNull Bundle outState) {
            
            // @ 追蹤 saveHierarchyState 方法
            outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());

            outState.putInt(LAST_AUTOFILL_ID, mLastAutofillId);
            
            // Fragment 狀態保存
            Parcelable p = mFragments.saveAllState();
            if (p != null) {
                outState.putParcelable(FRAGMENTS_TAG, p);
            }
            
            if (mAutoFillResetNeeded) {
                outState.putBoolean(AUTOFILL_RESET_NEEDED, true);
                getAutofillManager().onSaveInstanceState(outState);
            }
            // 分發給所有 Activity 生命週期監聽的 onActivitySaveInstanceState
            dispatchActivitySaveInstanceState(outState);
        }

        private void dispatchActivitySaveInstanceState(@NonNull Bundle outState) {
            // Activity 生命週期所有的 callback
            Object[] callbacks = collectActivityLifecycleCallbacks();
            
            if (callbacks != null) {
                for (int i = callbacks.length - 1; i >= 0; i--) {
                    ((Application.ActivityLifecycleCallbacks) callbacks[i])
                            .onActivitySaveInstanceState(this, outState);
                }
            }
            getApplication().dispatchActivitySaveInstanceState(this, outState);
        }
    ```
* `PhoneWindow`#`saveHierarchyState` 方法：目的是 **恢復 View 的狀態**，**創建 `SparseArray<Parcelable>` 並以 id 作為 key、Parcelable 作為 value (儲存 View 的訊息)**

    :::success
    * `SparseArray<T>` 簡單來說：是一個以 Integer 為 Key 的 Map 資料格式，也就是  `Map<Integer, T>`，不過它的效率較高
    :::

    ```java=
    // PhoneWindow.java

        ViewGroup mContentParent;

        @Override
        public Bundle saveHierarchyState() {
            // 創建 Bundle 準備傳遞數據
            Bundle outState = new Bundle();
            if (mContentParent == null) {
                // 佈局尚未加載
                return outState;
            }

            SparseArray<Parcelable> states = new SparseArray<Parcelable>();
            // @ 追蹤 saveHierarchyState 方法
            mContentParent.saveHierarchyState(states);
            
            ... 省略部分

            return outState;
        }
    ```

* **`saveHierarchyState` 方法**：View 遞迴呼叫，讓每個 View、ViewGroup 自己處理 (**ViewGroup 會在 Override 這個函數**)
    ```java=
    // View.java

        public void saveHierarchyState(SparseArray<Parcelable> container) {
            // @ 追蹤 dispatchSaveInstanceState 方法
            dispatchSaveInstanceState(container);
        }
    ```

    1. View#**`dispatchSaveInstanceState`** 方法：

        :::danger
        * 從這邊可以看出 xml 上 **沒有設定 `android:id` 屬性的 View 是不會回復狀態 !**
        
        * 由於儲存 View 的空間是使用 SparseArray 結構，所以 **同一個 View Tree 上不能有相同的 ID 的 View，否則就只會有一個更新**
        :::

        ```java=
        // View.java

        protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
            if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
                // 清除 PFLAG_SAVE_STATE_CALLED flag
                mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;

                // 呼叫 onSaveInstanceState
                Parcelable state = onSaveInstanceState();

                // 判斷 PFLAG_SAVE_STATE_CALLED flag
                if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
                    throw new IllegalStateException(
                            "Derived class did not call super.onSaveInstanceState()");
                }
                if (state != null) {
                    // Log.i("View", "Freezing #" + Integer.toHexString(mID)
                    // + ": " + state);
                    container.put(mID, state);
                }
            }
        }

        @CallSuper
        @Nullable 
        protected Parcelable onSaveInstanceState() {
            mPrivateFlags |= PFLAG_SAVE_STATE_CALLED;

            ... 省略部分
        }
        ```

    2. ViewGroup#**`dispatchRestoreInstanceState`** 方法：迭代該 ViewGroup 中所有的 View，並遞迴呼叫所有 View#`dispatchRestoreInstanceState` 方法來回復 View 的狀態

        ```java=
        // ViewGroup.java

        @Override
        protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
            // ViewGroup 如果有 id 也會存取
            super.dispatchSaveInstanceState(container);

            // 該 ViewGroup  ChildView 的數量
            final int count = mChildrenCount;
            final View[] children = mChildren;
            for (int i = 0; i < count; i++) {
                View c = children[i];
                if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) {
                // 只要沒有禁止儲存，就會用遞迴不斷呼叫 dispatchRestoreInstanceState 方法
                    c.dispatchSaveInstanceState(container);
                }
            }
        }
        ```

### Activity - onRestoreInstanceState 恢復資料

:::info
其實流程跟 `onSaveInstanceState` 差不多，只是做了反向操作
:::

* 從 `Activity`#`onRestoreInstanceState` 開始看，可以看到這裡 **取得 Bundle 對象 (也就是 Memo)，呼叫 [`PhoneWindow`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java) 並傳入 Bundle 物件**

    ```java=
    // Activity.java

    private Window mWindow;    // 實作類是 PhoneWindow

    private static final String WINDOW_HIERARCHY_TAG = "android:viewHierarchyState";

    protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
        if (mWindow != null) {
            // 取得復用的 Bundle 對象
            Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG);
            if (windowState != null) {

                // @ 追蹤 restoreHierarchyState 方法
                mWindow.restoreHierarchyState(windowState);
            }
        }
    }
    ```
* PhoneWindow#`restoreHierarchyState` 方法：目的是 **恢復 View 的狀態**，**取得 `SparseArray<Parcelable>` 並以 id 作為 key、Parcelable 作為 value (儲存 View 的訊息)**

    :::success
    * `SparseArray<T>` 簡單來說：是一個以 Integer 為 Key 的 Map 資料格式，也就是  `Map<Integer, T>`，不過它的效率較高
    :::

    ```java=
    // PhoneWindow.java

    ViewGroup mContentParent;

    @Override
    public void restoreHierarchyState(Bundle savedInstanceState) {
        if (mContentParent == null) {
            // 尚未加載 xml 布局
            return;
        }

        // 取得 SparseArray<Parcelable>
        SparseArray<Parcelable> savedStates
                = savedInstanceState.getSparseParcelableArray(VIEWS_TAG);

        if (savedStates != null) {
            // @ 追蹤 restoreHierarchyState 方法
            mContentParent.restoreHierarchyState(savedStates);
        }

        // 找尋當前 focused 的 View
        int focusedViewId = savedInstanceState.getInt(FOCUSED_ID_TAG, View.NO_ID);

        ... 省略部分

    }
    ```

* **`restoreHierarchyState` 方法**：View 遞迴呼叫，讓每個 View、ViewGroup 自己處理 (**ViewGroup 會在 Override 這個函數**)
    ```java=
    // View.java

    public void restoreHierarchyState(SparseArray<Parcelable> container) {
        // @ 追蹤 dispatchRestoreInstanceState 方法
        dispatchRestoreInstanceState(container);
    }
    ```

    1. `View`#**`dispatchRestoreInstanceState`** 方法：

        :::danger
        * 從這邊可以看出 xml 上 **沒有設定 `android:id` 屬性的 View 是不會回復狀態 !**
        
        * 由於儲存 View 的空間是使用 SparseArray 結構，所以 **同一個 View Tree 上不能有相同的 ID 的 View，否則就只會有一個更新**
        :::

        ```java=
        // View.java

        protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {

            // 如果該 View 沒有 ID 則不儲存
            if (mID != NO_ID) {
                Parcelable state = container.get(mID);
                if (state != null) {

                    // 清除 PFLAG_SAVE_STATE_CALLED
                    mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;

                    // 呼叫 onRestoreInstanceState 方法
                    onRestoreInstanceState(state);

                    // 判斷 PFLAG_SAVE_STATE_CALLED，代表 onRestoreInstanceState 只會被呼叫一次
                    if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
                        throw new IllegalStateException(
                                "Derived class did not call super.onRestoreInstanceState()");
                    }
                }
            }
        }

        @CallSuper
        protected void onRestoreInstanceState(Parcelable state) {
            mPrivateFlags |= PFLAG_SAVE_STATE_CALLED;

            ... 省略部分
        }
        ```

    2. `ViewGroup`#**`dispatchRestoreInstanceState`** 方法：迭代該 ViewGroup 中所有的 View，並遞迴呼叫所有 View#`dispatchRestoreInstanceState` 方法來回復 View 的狀態

        ```java=
        // ViewGroup.java

        @Override
        protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
            // ViewGroup 如果有 id 也會存取
            super.dispatchRestoreInstanceState(container);

            // 該 ViewGroup  ChildView 的數量
            final int count = mChildrenCount;
            final View[] children = mChildren;

            for (int i = 0; i < count; i++) {
                View c = children[i];
                // 只要沒有禁止儲存，就會用遞迴不斷呼叫 dispatchRestoreInstanceState 方法
                if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) {
                    c.dispatchRestoreInstanceState(container);
                }
            }
        }
        ```
## 更多的物件導向設計

物件導向的設計基礎如下，如果是初學者或是不熟悉的各位，建議可以從這些基礎開始認識，打好基底才能走個更穩（在學習的時候也需要不斷回頭看）！

:::info
* [**設計建模 2 大概念- UML 分類、使用**](https://devtechascendancy.com/introduction-to-uml-and-diagrams/)

* [**物件導向設計原則 – 6 大原則(一)**](https://devtechascendancy.com/object-oriented-design-principles_1/)

* [**物件導向設計原則 – 6 大原則(二)**](https://devtechascendancy.com/object-oriented-design-principles_2/)
:::


### 創建模式 - Creation Patterns

* [**創建模式 PK**](https://devtechascendancy.com/pk-design-patterns-factory-builder-best/)

* **創建模式 - `Creation Patterns`**：
    
    創建模式用於「**物件的創建**」，它關注於如何更靈活、更有效地創建對象。這些模式可以隱藏創建對象的細節，並提供創建對象的機制，例如單例模式、工廠模式… 等等，詳細解說請點擊以下連結

    :::success
    
    * [**Singleton 單例模式 | 解說實現 | Android Framework Context Service**](https://devtechascendancy.com/object-oriented_design_singleton/)

    * [**Abstract Factory 設計模式 | 實現解說 | Android MediaPlayer**](https://devtechascendancy.com/object-oriented_design_abstract-factory/)

    * [**Factory 工廠方法模式 | 解說實現 | Java 集合設計**](https://devtechascendancy.com/object-oriented_design_factory_framework/)

    * [**Builder 建構者模式 | 實現與解說 | Android Framwrok Dialog 視窗**](https://devtechascendancy.com/object-oriented_design_builder_dialog/)
    
    * [**Clone 原型模式 | 解說實現 | Android Framework Intent**](https://devtechascendancy.com/object-oriented_design_clone_framework/)

    * [**Object Pool 設計模式 | 實現與解說 | 利用 JVM**](https://devtechascendancy.com/object-oriented_design_object-pool/)

    * [**Flyweight 享元模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_flyweight/)
    :::


### 行為模式 - Behavioral Patterns

* [**行為模式 PK**](https://devtechascendancy.com/pk-design-patterns-cmd-strat-state-obs-chain/)

* **行為模式 - `Behavioral Patterns`**：
    
    行為模式關注物件之間的「**通信**」和「**職責分配**」。它們描述了一系列對象如何協作，以完成特定任務。這些模式專注於改進物件之間的通信，從而提高系統的靈活性。例如，策略模式、觀察者模式… 等等，詳細解說請點擊以下連結

    :::warning
    * [**Stragety 策略模式 | 解說實現 | Android Framework 動畫**](https://devtechascendancy.com/object-oriented_design_stragety_framework/)

    * [**Interpreter 解譯器模式 | 解說實現 | Android Framework PackageManagerService**](https://devtechascendancy.com/object-oriented_design_interpreter_framework/)

    * [**Chain 責任鏈模式 | 解說實現 | Android Framework View 事件傳遞**](https://devtechascendancy.com/object-oriented_design_chain_framework/)

    * [**State 狀態模式 | 實現解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_state/)

    * [**Specification 規格模式 | 解說實現 | Query 語句實做**](https://devtechascendancy.com/object-oriented_design_specification-query/)

    * [**Command 命令、Servant 雇工模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_command_servant/)

    * [**Memo 備忘錄模式 | 實現與解說 | Android Framwrok Activity 保存**](https://devtechascendancy.com/object-oriented_design_memo_framework/)

    * [**Visitor 設計模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_visitor_dispatch/)

    * [**Template 設計模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_template/)

    * [**Mediator 模式設計 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_programming_mediator/)

    * [**Composite 組合模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_programming_composite/)
    :::


### 結構模式 - Structural Patterns

* [**結構模式 PK**](https://devtechascendancy.com/pk-design-patterns-proxy-decorate-adapter/)

* **結構模式 - `Structural Patterns`**：
    
    結構模式專注於「物件之間的組成」，以形成更大的結構。這些模式可以幫助你確保當系統進行擴展或修改時，不會破壞其整體結構。例如，外觀模式、代理模式… 等等，詳細解說請點擊以下連結

    :::danger
    * [**Bridge 橋接模式 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_bridge/)

    * [**Decorate 裝飾模式 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_decorate/)

    * [**Proxy 代理模式 | 解說實現 | 分析動態代理**](https://devtechascendancy.com/object-oriented_design_proxy_dynamic-proxy/)

    * [**Iterator 迭代設計 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_iterator/)

    * [**Facade 外觀、門面模式 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_facade/)

    * [**Adapter 設計模式 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_adapter/)
    :::



## Appendix & FAQ
:::info

:::

###### tags: `Java 設計模式` `基礎進階`