--- 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 設計模式` `基礎進階`