當我們在開發應用程式的時候,常常會遇到一種需要 Cache 資料的情境:當使用者打開畫面,我們從 API 或 DB 讀取資料,讓資料成功顯示在畫面上。過了一段時間,使用再次打開相同畫面時,我們希望不要再次呼叫 API,直接顯示上一次取得的資料,以節省流量。為了完成這個功能,我們從會在原本的程式碼中,加上一段 Cache 資料的實作。
https://dartpad.dev/?id=4f6412479033c28f75da665ca1ed433d
這個做法雖然可以滿足我們的需求,但是也帶來了一個問題。這段類別違反了單一職責,他同時具備了 Cache 的職責與讀取資料的職責。假設今天我們不想把資料暫存在記憶體中,而是想暫存在 sqlite 中,我們就會需要回頭修改這個類別。其次,雖然 NewsRepository 的設計本身符合開放封閉原則,但是我們修改時卻不是拓展而是修改,反而讓設計不符合原則了。
為了尋求更好的設計,我們可以使用裝飾者模式。我們建立一個 NewsCachedDecorator,讓它實作與 NewsRepository 相同的介面,並把 NewsRepostory 傳入裝飾者中。
https://dartpad.dev/?id=faa4503480e41c56bbf62248da73c225
最後再依賴注入的時候,CachedDecorator 包在實作外面,當畫面呼叫 NewsRepository 時,自然會先經過 Cache,由 Cache 決定是否重打 API。
假設今天我們不需要 Cache 了,我們可以直接移除 Cache 裝飾者就好。
或者,我們想更換 Cache 的實作方式,改成使用套件而非自己實作時,也可以修改 Cache 裝飾者就好,而不用修改原本的 NewsRepository 實作。
在上面的例子中,我們把 Cache 設計在 Repository 層,但是其實我們也可以把 Cache 的職責,往上放到狀態管理層,往下放到實際呼叫 Web API 的地方。許多套件本身也都有提供 Cache 的功能,讓開發者可以更簡單的 Cache API 的回傳值。那到底我們應該放在哪邊才對呢?其實這個問題並沒有正確答案,需要讀者根據自己的情境選擇。當我們很確定所有 API 都有相同的 Cache 行為時,那把 Cache 職責放在呼叫 Web API 的地方可能會適合。但是當今天我們需要根據不同資料,而有不同的 Cache 行為時,放在 Repository 可能比較合適。
Cache 是一種很常見的使用場景,透過裝飾者模式分離 Cache 與呼叫 API 的職責,讓程式符合單一職責與開放封閉原則。當時決定要使用 Cache 時,也需要決定要把 Cache 實作在什麼地方,這其中就需要讀者根據需求與專案的狀況選擇。