持久化是獨立的關注點#
書中早早就把資料持久化(persistence)視為一個獨立的關注點,而不是貼在領域類別上的一個方法。
Customer可以拆成CustomerData、CustomerPersistence、CustomerGUIDisplay、CustomerImportExport等類別。- 變更 GUI 時不必動
CustomerPersistence;改變儲存格式時也不必動CustomerData。 - 這與「Separate Concerns to Make Smaller Concerns」一致——讓每個類別只面對一個變動軸。
如果一個
Customer類別要同時處理欄位、JSON、UI 與 DB schema,每次需求變動都會牽動四個方向。拆開後變動成本明顯下降。
集合介面:把儲存藏在背後#
CDDiscCollection、CustomerCollection、CDReleaseCollection 都是介面,呼叫端不用知道:
- 是 in-memory Java collection?
- 是關聯式資料庫?
- 是文字檔或 XML?
- 還是遠端 API?
interface CDDiscCollection
CDDisc find_by_physical_id(PhysicalID a_physical_id)
CDDisc[] find_by_cd_release(CDRelease a_cd_release)
add(CDDisc a_cd_disc)
remove(CDDisc a_cd_disc)If It Has Collection Operations, Make It a Collection——集合分離物件使用與儲存,並隱藏聚合操作的實作。
設定驅動的持久化選擇#
採 Configuration 為 Service Locator 的變體,搭配 DTO:
DataAccessConfiguration描述要使用哪個資料來源。Configuration內部可以從 in-memory 預設、設定檔、資料庫等不同地方讀取設定。- 應用程式啟動時從
Configuration取設定,不依賴特定來源。
When in Doubt, Indirect——用工廠或設定取得實例,使呼叫端不直接
new出實作;測試時可注入 in-memory,生產時可注入資料庫實作。
範式不對齊(Paradigm Mismatch)#
第三方實作的範式不一定符合你設計的介面。可以時用 adapter 隱藏差異;不可以時就要重寫介面。
範例:
- 字典(dictionary)介面假設「同 key 重複加入會覆蓋」。
- 第三方實作卻在重複時拋例外。
- 解法:寫個 adapter,先檢查 key、需要時先刪除再加入。
但有些範式衝突無法包裝。例如:
- 介面假設交易(transaction)跨多個動作。
- 第三方實作只支援單一動作。
- 此時必須改介面。
介面在還沒驗證過實作之前不應視為定案。沒寫過實作的介面,可能根本無法落地。
物件序列化的副作用#
第一次釋出時,作者用 Java 序列化做持久化:
- 所有資料類別必須 implement
Serializable。 - 這是「實作上溯影響類別定義」的典型例子——意外耦合。
- 改用資料庫實作則不會強迫資料類別實作介面。
選擇是否要建立「資料類別 + 持久化類別」雙人組:
- 嚴格分離可避免上述耦合,但會多出一倍的類別。
- 在這個專案規模下,作者選擇接受耦合,前提是知道未來必要時可以重構。
配置與生產之間的測試替換#
Build Flexibility for Testing——設計時保留替換點。
實踐:
- 測試時用
Vector或 Java collection 實作,免去啟動 DB 服務的開銷。 - 生產時可換成關聯式 DB、NoSQL、文字檔——只要符合介面即可。
- 強制把資料持久化層從業務邏輯中抽出,無形中也提高設計品質。
遷移:從舊系統流到新系統#
新系統往往要載入舊系統的資料,書中為三個集合各加上 import 方法:
CustomerCollection
add_customers_from_file(String filename)
CDDiscCollection
add_cd_discs_from_file(String filename)
CDReleaseCollection
add_cd_releases_from_file(String filename)實務考量:
- 格式可選(tab 分隔、XML、CSV),重點是保留結構與型別語意。
- 同一份 import 程式可同時用於遷移與測試 → 一份程式雙倍價值。
- 第一次落地後再回顧,把 import 從集合中拆出去(後來改放到
com.samscdrental.importexport套件)。
Figure Out How to Migrate Before You Migrate——思考遷移路徑常會發現設計上其他考量。
資料正確性的驗證#
匯入舊系統資料時的常見問題:
- 實體 CDDisc 的 ID 不在清單中。
- ID 對應的標題與盤面不符。
- 庫存清單上有的 CDDisc 在實體上找不到。
- 狀態欄位不一致。
即使資料來自既有電腦系統,也不能假設它經過驗證。先在介面驗證,再讓它進入持久化層;否則錯誤會擴散到資料庫深處。
唯一性的判定(Know Who It Is)#
Know Who It Is——為「應該唯一」的物件決定唯一性準則。
書中討論 Customer 的唯一性:
- Social Security number:可靠,但客戶可能不願給。
- 信用卡卡號:客戶可能每次給不同卡。
- Email:能造很多個,最不可靠。
- 駕照號:對在地客戶有效,跨州遷移就失效。
設計上的延伸:
- 同一客戶在 paper 系統可能擁有多個 ID,因為他想擺脫過去逾期紀錄。
- 匯入時應檢查是否重複,並決定 overwrite 或拋錯。
- 客戶被列為
NeverAgain後,永遠不可從集合中刪除——以免他重新註冊規避限制。
把儲存與序列化解耦#
當持久化方式可能改變時:
- 為每個資料類別準備獨立的 ImportExport 類別。
- 依檔案 IO 與屬性解析分層處理錯誤:集合層處理檔案級錯誤,物件層處理屬性級錯誤。
- 透過
DataAccessHelper提供共用方法,避免在每個 ImportExport 中複製貼上序列化邏輯。
持久化指南總結#
| 指南 | 訊息 |
|---|---|
| Separate Concerns to Make Smaller Concerns | 持久化、UI、業務、ImportExport 各歸其位 |
| If It Has Collection Operations, Make It a Collection | 用集合介面隱藏儲存實作 |
| When in Doubt, Indirect | 用設定/工廠抽掉具體儲存決定 |
| Build Flexibility for Testing | 測試時可換 in-memory 實作 |
| Figure Out How to Migrate Before You Migrate | 在動手前思考遷移路徑 |
| Know Who It Is | 為唯一性訂定準則 |
| Adopt and Adapt | 第三方持久化包用 adapter 包裝為理想介面 |
| Don’t Let the Cold Air In | 持久化邊界仍要驗證輸入 |