持久化是獨立的關注點#

書中早早就把資料持久化(persistence)視為一個獨立的關注點,而不是貼在領域類別上的一個方法。

  • Customer 可以拆成 CustomerDataCustomerPersistenceCustomerGUIDisplayCustomerImportExport 等類別。
  • 變更 GUI 時不必動 CustomerPersistence;改變儲存格式時也不必動 CustomerData
  • 這與「Separate Concerns to Make Smaller Concerns」一致——讓每個類別只面對一個變動軸。

如果一個 Customer 類別要同時處理欄位、JSON、UI 與 DB schema,每次需求變動都會牽動四個方向。拆開後變動成本明顯下降。

集合介面:把儲存藏在背後#

CDDiscCollectionCustomerCollectionCDReleaseCollection 都是介面,呼叫端不用知道:

  • 是 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持久化邊界仍要驗證輸入