Try   HackMD

Memo 備忘錄模式

OverView of Content

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

又稱為備忘錄 (Memo)

Memo 使用場景

  • 需要儲存一個物件在某一個時刻的狀態 (並非執行步驟)

    提供 rollback 操作

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

Memo 定義 & Memo UML

  • Memo 定義:在不破獲封裝的前提下,保存一個物件的狀態,好讓之後可以恢復該對象的狀態

  • Memo UML 角色關係

    Memo 類角色 說明
    Originator(發起人) 讓使用者操控 (可讓使用者直接接觸),同時包含 Memo 功能
    Memo(備忘錄) 跟 Originator 有相對應的 member,讓 使用者無法直接接觸 Memo 類
    Caretaker(管理備忘錄) 專門用來記錄、管理 Memo 類
    • Memo 有很清楚得分出類之間的職責,符合單一職責、迪米特(最少知識)原則

      單一職責Originator 負責操作備忘錄、Memo 負責紀錄數據、Caretaker 管理紀錄

      迪米特:User 不會直接接觸到 Memo,使用者只會知道 Originator

Memo 優缺點

  • 優點 :

    1. 提供使用者快速方便的恢復機制,方便找回歷史狀態或是 rollback 資料,而這些操作都是安全性的操作

    2. 從上面可以看出 使用者不必關心細節 (gettersetter)

  • 缺點 :

    消耗資源,類的增加 (通病),每次儲存都會消耗資源空間

Memo 實現

Memo 標準

  1. Originator

    GameProvider 讓使用者直接操控的類 (對外暴露),同時包括 Memo 類的創建、操作,提供給使用者使用

    ​​​​// 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,該類不會讓使用者直接使用

    ​​​​public class Memo { ​​​​ int hp = 100; ​​​​ int mp = 100; ​​​​}
    • Originator 是跟 Memo 類有相同的 member,所以它必須擁有該類的所有屬性 (可用 data class),在這部分可以用另外一個抽象優化

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

  3. Caretaker

    身為 Memo 管理員,這裏使用 Map 儲存 Memo,專職在 處理存取,該類會讓使用者使用

    ​​​​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); ​​​​ } ​​​​}
    • 這裡使用 Map 來儲存 Memo 物件,在實際工作上,最好是添加一個上限或是 LRU 樹來替換舊的備份,否則可能導致 OOM !
  • User 使用:

    User 必須使用 Originator 發起備份、Caretaker 管理(儲存、取得)備份

    ​​​​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(); ​​​​ } ​​​​}

Memo 變形 - Clone 取代 Memo

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

    • 不符合單一職責? 原本的對象需要覆蓋拷貝的責任了!

      的確如此,但這裡我們可以想做,把拷貝責任封裝到 Cloneable 界面,其實這就算是一個簡易的封裝

    • 這種變形僅限於 簡單場合

      雖然 clone 可以加快性能,但是我們還是要注意 深、淺拷貝問題,避免增加了邏輯複雜度,再次簡化後 UML 如下

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

    ​​​​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 模式的變化板而已

    ​​​​fun main() { ​​​​ // 發起者 ​​​​ val game = GameProvider().apply { ​​​​ attack() ​​​​ createMemo() // 創建備忘錄 ​​​​ attack() ​​​​ defense() ​​​​ finishGame() ​​​​ } ​​​​ game.apply { ​​​​ // 恢復 ​​​​ restore() ​​​​ finishGame() ​​​​ } ​​​​}

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 並傳入 Bundle 物件

    ​​​​// 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 的訊息)

    • SparseArray<T> 簡單來說:是一個以 Integer 為 Key 的 Map 資料格式,也就是 Map<Integer, T>,不過它的效率較高
    ​​​​// 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 這個函數)

    ​​​​// View.java ​​​​ public void saveHierarchyState(SparseArray<Parcelable> container) { ​​​​ // @ 追蹤 dispatchSaveInstanceState 方法 ​​​​ dispatchSaveInstanceState(container); ​​​​ }
    1. View#dispatchSaveInstanceState 方法:

      • 從這邊可以看出 xml 上 沒有設定 android:id 屬性的 View 是不會回復狀態 !

      • 由於儲存 View 的空間是使用 SparseArray 結構,所以 同一個 View Tree 上不能有相同的 ID 的 View,否則就只會有一個更新

      ​​​​​​​​// 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 的狀態

      ​​​​​​​​// 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 恢復資料

其實流程跟 onSaveInstanceState 差不多,只是做了反向操作

  • Activity#onRestoreInstanceState 開始看,可以看到這裡 取得 Bundle 對象 (也就是 Memo),呼叫 PhoneWindow 並傳入 Bundle 物件

    ​​​​// 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 的訊息)

    • SparseArray<T> 簡單來說:是一個以 Integer 為 Key 的 Map 資料格式,也就是 Map<Integer, T>,不過它的效率較高
    ​​​​// 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 這個函數)

    ​​​​// View.java ​​​​public void restoreHierarchyState(SparseArray<Parcelable> container) { ​​​​ // @ 追蹤 dispatchRestoreInstanceState 方法 ​​​​ dispatchRestoreInstanceState(container); ​​​​}
    1. View#dispatchRestoreInstanceState 方法:

      • 從這邊可以看出 xml 上 沒有設定 android:id 屬性的 View 是不會回復狀態 !

      • 由於儲存 View 的空間是使用 SparseArray 結構,所以 同一個 View Tree 上不能有相同的 ID 的 View,否則就只會有一個更新

      ​​​​​​​​// 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 的狀態

      ​​​​​​​​// 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); ​​​​​​​​ } ​​​​​​​​ } ​​​​​​​​}

更多的物件導向設計

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

創建模式 - Creation Patterns

行為模式 - Behavioral Patterns

結構模式 - Structural Patterns

Appendix & FAQ

tags: Java 設計模式 基礎進階