"Ddd" 2026-07-10
資料袋與領域模型
判斷一個型別該是一袋欄位還是有行為的領域模型:判準是「有沒有不允許任意組合的欄位」。含判準用錯時規則退化成建議的機制、以及資料袋起步後升級的演化訊號。 2026-07-10
entity 與 value object 的判準
同一個業務概念該建成 entity 還是 value object:判準是「操作需不需要 identity-based 回寫」、而不是概念重要性或有沒有 id 可填。含判準隨生命週期重問的交棒時機、value object 的語意封閉、枚舉分層。 2026-07-10
不變式的強制層次
業務約束落在文件層、型別層、執行層的差異與代價:違反時是靜默、編譯失敗還是當場拒絕。含不變式被撞時「需求違規 vs 約束錯形」的分辨、存在條件與輸入品質的分層邊界。 2026-07-10
狀態轉換與稽核軌跡
領域方法作為唯一變更路徑:判準是「變更有沒有需要一起完成的伴隨動作」。含唯一路徑與建議路徑的分界、稽核軌跡出洞的靜默機制與凍結作為稽核端點。 2026-07-10
建構路徑設計
工廠表達力不足時缺陷如何被逃生口吸收——逃生口讓正確的修法變不必要、以語意錯誤在下游復發。含原始值官方出口的穩態邊界、封裝擺盪的判讀。 2026-07-13
組裝層的可達性
行為測試全綠、功能在實機上沒有入口的失效形態出現時使用。mock 換掉的正是組裝,組裝完成與否在行為測試裡沒有證言;把可達性當成組裝層的不變式,在測試、發版與設計文件各給一個強制點。 2026-07-16
觀測出口的職責三分
repository 要補「資料變了」的推送能力、卻不確定 Stream 介面放 domain 算不算洩漏時使用。歸屬判準是介面用什麼語言表達、不是需求來自誰:契約歸 domain、變更偵測歸 infrastructure、框架訂閱歸組裝層。 2026-07-16
讀模型的升級判準
repository 開始長出畫面專用查詢方法、或有人提議「上 CQRS」時使用。讀側是一道階梯而不是開關:訊號決定該爬到哪一階,自檢問句是「這個查詢回傳的是讀的形狀、還是 aggregate 的形狀」。 2026-07-16
domain event 與狀態流
為了讓某個畫面刷新而補發事件、或監聽端掛著全事件過濾器時使用。事件記錄離散事實、狀態流發布連續觀測——判準是消費者問「發生了什麼」還是「現在是什麼」;載體借用的代價是涵蓋面靠枚舉維持。 2026-07-16
domain event 與命令、查詢的分界
事件類別以動詞開頭、或事件成對出現 Requested/Provided 帶 correlation id 時使用。事件只承載已發生的事實——命令有唯一處理者與成敗、查詢是一問一答的對話;把意圖或對話裝進事件通道、責任結構會跟著錯位。 2026-07-17
跨邊界參照與狀態所有權
下游持有上游資料的 id、操作靠這個 id 回寫時:上游的哪些操作會讓 id 死亡、有沒有跨操作不變的穩定身份、身份轉移後本端的狀態搬不搬家——參照設計的判準與遷移分工 2026-07-16
Domain Event
系統裡出現「為了讓某頁刷新而補發事件」或「監聽端掛全事件過濾器」時使用。domain event 是已發生的業務事實——過去式命名、發布後不可變、錯過代表事實遺失。 2026-07-16
State Stream(狀態流)
畫面刷新靠補償、或考慮拿既有事件當刷新訊號時使用。狀態流是持續發布「資料當前值」的觀測載體——新值蓋過舊值、錯過中間值無代價,回答「現在是什麼」。 2026-07-16
Observation Outlet(觀測出口)
repository 只有 pull 介面、衍生視圖靠補償刷新,考慮補「資料變了」的推送能力時使用。觀測出口是 pull 介面的 push 對應——能力橫跨三層、歸屬由各層的表達語言決定。 2026-07-10
約束要讓違反路徑走不通:只寫在文件層的設計意圖是沒關的逃生口
設計 entity 的變更路徑、或審查「請走 X」類慣例時使用。約束有文件、型別、執行三個落點;只落在文件層的意圖對繞過路徑沒有任何阻力,而註解宣稱的約束比沒有約束更糟——讓讀者以為有防護。判準是讓違反意圖的路徑走不通、不是寫文件請大家不要走。 2026-07-10
逃生口吸收建構路徑的缺陷:修工廠的表達力、不是修拼裝點
同族語意錯誤重複出現、或測試 Arrange 段大量用萬能拼裝工具建物件時使用。全欄位 copyWith 這類逃生口總有辦法把物件拼出來,於是建構路徑的表達力缺陷永遠不被迫修好——需求被逃生口吸收、以語意錯誤的形式在別處復發。修上游的表達力、不是修每一個拼裝點。 2026-07-17
自持狀態與可導出狀態:上游身份轉移時,誰要搬家、誰自動對齊
後端合併操作讓資料換了身份,前端持有的多份「以舊 id 為 key」的狀態怎麼辦?POS App 的答案分兩類:能從上游重新導出的狀態不用管、下一輪同步自動對齊;必須自行持有的狀態(差異比對基準、追蹤記錄)才需要通知搬家。分類錯誤的代價是兩個方向的 bug。 2026-07-17
單調狀態機與樂觀更新的回滾契約:前台不得顯示後端沒記錄的狀態
POS App 的品項處理狀態只能遞增——現實世界的動作不可逆,狀態機跟著不可逆。樂觀更新讓 UI 先行,但後端拒絕時必須回滾:因為這個狀態是其他防護規則的資料來源,前台多顯示一格進度,防護就會在錯誤的前提上放行。 2026-07-17
跨邊界參照的生命週期:前端凍結的 id,死活由後端決定
POS App 的前端把後端資料的 id 凍結在本地追蹤記錄裡,後端的「合併」操作會重建資料——舊 id 全部失效,取消與追加功能無聲死亡。判準:跨邊界持有的每一個參照,都要回答「對方的哪些操作會讓它死」;穩定身份是實測出來的事實,不是推理出來的假設。 2026-07-15
一行查詢放哪、一個測試留不留:結構改動後的兩次「還需要存在嗎」
修完一個應收金額 bug 收尾時,兩個『還需不需要』的問題浮出來。一段『過濾出已結帳品項』的查詢該掛哪一層——inline 在 widget 是洩漏、開一個 service 是儀式,判準是『查詢誰擁有的資料就掛給誰』,答案是既有的 repository。一個為了鎖住修復而寫的測試該不該留——當修復是靠刪掉舊函式、移除參數達成時,那個 bug 被結構免疫了,測試變贅述;判準是『這測試守的失敗模式,結構是否已經替它擋掉』。 2026-07-13
測試全綠、功能失聯:五個 runtime 問題與組裝層的接線缺口
113 張票收尾全綠的版本,實機測試找出五個問題:路由指向佔位頁、provider 佔位 throw、按鈕空 callback,加上兩個平台語意差異。單元測試的 mock override 是正當的測試 seam、同時是遮蔽接線斷裂的來源——記下三層共振的機制、反向追溯設計文件的結果、以及修補時規格層/測試層/發版層的分層落點。 2026-07-10
「978ABC」被拒的理由寫著長度不對 — 驗證的兩層分工與順序陷阱
輸入驗證有兩層職責:建構期不變式守「這個物件能不能存在」、無狀態 validator 守「使用者輸入對不對」,混在一起會讓測試建不出 fixture、錯誤訊息歸錯類。順序陷阱:先標準化再檢查等於先銷毀證據再診斷——含字母的 ISBN 被削成三位數、錯誤訊息說長度不對。 2026-07-10
「該收多少錢」抽成 pure function — IO 在邊界、領域計算在核心
多個畫面都要顯示「未結帳的份數與金額」時,把計算抽成無 IO 的 pure function:資料由 caller 從 repository 拿好傳入、函式只做合併 / 扣減 / 折扣運算。含合併鍵要跟同一性定義同維度的陷阱、兩層折扣各自 clamp 的邊界、以及用註解預留擴充點讓未來規則接入不動本體。 2026-07-10
16 種支付渠道、4 種行為分類 — 分層 enum:保真層與行為層的粒度分工
同一個分類系統要同時服務序列化(要無損)跟 UI 行為分流(要粗粒度)時,單一 enum 選哪個粒度都錯。解法是分層:保真層無損對齊後端完整列舉、行為層收斂成行為真正分歧的少數大類、層間用 exhaustive switch 衍生——粒度轉換獲得編譯期保證。 2026-07-10
BookImported 不能寫成 ImportBook — 事件命名的過去式是語意類別、不是風格
domain event 用過去式命名(BookImported)因為事件是已發生的事實;動詞開頭(ImportBook)是命令的形狀、訂閱者會誤讀成指令。字尾清單的自動檢查抓不住不規則動詞——偵測可機械化、判定要看語意。附一次版本終止:工作日誌宣稱的命名問題實際早已不存在、執行前驗前提省下整輪流程。 2026-07-10
copyWith 是逃生口,不是設計 — 從一個測試 bug 追到 entity 稽核軌跡的洞
copyWith 對純資料載體是正確工具,對有領域方法的 entity 是繞過不變式的逃生口。從一個 3 字元 ID 觸發的例外,追出同族語意錯誤、被繞過的領域方法、以及從未被強制的註解約束。 2026-07-10
Domain 層的 947 處硬編碼中文 — 訊息代碼跟顯示文字的分層責任
domain 層的 enum 或 getter 直接回傳 UI 顯示字串時,多語言支援與分層原則同時失守。修法是 domain 只回訊息代碼與結構化資料、UI 層用 translator extension 翻譯;遷移排序從最小模組先行驗證模式。適用於盤點「這個字串屬於領域事實還是呈現」的情境。 2026-07-10
Exception 型別綁 ErrorCategory 的建構不變式 — 以及合法需求撞上不變式的時刻
把「錯誤代碼必須屬於對應分類」做成建構期不變式,錯誤分類錯亂會變成測試失敗而不是靜默混亂;同一批修復出現三種形態——換對值、換精確值、以及改繼承逃離約束。第三種是分類學本身的訊號:一個 domain 的錯誤天生橫跨技術分類時,分類軸跟階層軸不正交。 2026-07-10
mock 要配置 55 個方法、實際只用 5 個 — 測試痛是介面設計痛的探針
service 測試的 mock 負擔正比於它依賴的介面寬度:依賴四個大介面共 55 個方法、實際呼叫 5 個,91% 的 mock 配置是純浪費、還會炸 MissingStubError。修法是介面隔離——抽出只含實際使用方法的 Port,讓 mock 縮到跟真實依賴一樣窄;為未來預留的方法用 TODO 標記啟用時機。 2026-07-10
Value Object 的封裝擺盪:從全移除、完全封裝、到加回 .value getter
VO 的封裝邊界在兩個極端之間來回——純字串(零封裝)跟完全封裝(禁止取原始值)各有成立的理由、也各自撞牆。穩態是給原始值一個有語意的官方出口,而不是把「取原始值」本身當違規。含 176 個編譯錯誤的工作量低估、以及「相容性介面」作為理想撤退訊號的判讀。 2026-07-10
文件裡的扁平 Product、程式碼裡的雙層聚合 — 宣稱型文件的半衰期
refactor 總結文件記的是決策時刻的快照:扁平 Product(一商品一價一庫存)在真實 POS 業務下演化成 Product + ProductSpecification 雙層、價格三種下沉到規格。欄位放聚合根還是子層的判準是「兩個規格會不會不同」;文件預言的需求全中、預言的結構全錯——這正是先蓋結構會蓋錯的實證。 2026-07-10
功能「完成」、測試全過、資料從未落地 — 持久化迴圈是驗收的盲區
domain 功能的測試可以全綠、而它的資料從未被序列化、資料庫沒有對應的表——單元測試都在記憶體內驗證行為、沒有一條測試走「存進去、重建、讀出來」的迴圈。驗收定義要含 roundtrip;entity 欄位與 schema 欄位的差集是靜默資料失真的清單。 2026-07-10
只活在結帳流程裡的領域物件 — ephemeral model 與「Rx 外殼、immutable 內核」
流程型狀態(結帳中的輸入金額、支付方式、會員)建模成生命週期等於流程的 ephemeral 物件:結完即丟、下次全新,殘留狀態忘記重置的 bug 被結構性消滅。實作形態是 reactive 外殼包 immutable 內核——對外只開語意化變更方法、每次變更是原子的狀態替換。 2026-07-10
用事件做同步查詢、等於手工重建 RPC — 跨 domain 解耦的完整帳單
domain 直接查另一個 domain 的 repository 違反依賴方向;改事件驅動的 request/response 解了耦、但帳單具體:correlation id 配對並發、timeout 機制、三條錯誤路徑——都是同步呼叫免費附贈的東西。判準是需要解耦「依賴方向」還是「時間與部署」:前者用消費端 Port 就夠、後者才值得付事件的價。 2026-07-10
同一個子系統膨脹兩次:異步查詢系統的過度設計震盪
過度設計會復發、且兩輪的機制不同:設計期的膨脹來自想像的需求(別層已處理的重試、用不到的優先級佇列),迭代期的膨脹來自不刪的舊版本(三個實作並存、狀態多處追蹤)。偽需求的檢驗法是問「這個能力已經有別層在做嗎」。 2026-07-10
同一個品項、四個 model — value object 什麼時候該升級成 entity
同一個業務概念要不要拆成多個 model、value object 什麼時候該升級成 entity——判準是操作需不需要 identity-based 回寫。以 POS 品項從點選、掛單、結算到歷史訂單的四階段模型為例,含 snapshot 與 live reference 的凍結時機。 2026-07-10
兩個 domain 各自實作同一個 API service — 100% 覆蓋率的假象
同名 service 在多個 domain 各自實作時,覆蓋率數字會失去意義:每份實作各測各的、mock 各有介面,統一的行為從未被測過。重複實作是上游訊號——規劃文件沒抽出跨 domain 的共同技術需求;單檔品質審查看不到跨檔重複。 2026-07-10
兩個 ImportResult 各自都合理 — 傘狀名的碰撞與做一半的重命名
同一個 domain 裡兩個類別都叫 ImportResult:一個是驗證結果、一個是操作結果,各自誕生時都合理。修法是依語意責任命名(ImportValidationResult);而重命名是一組原子操作——類別名、檔名、import、文件宣稱,做一半留下的不一致比不做更迷惑,且宣稱完成與實際完成的漂移要靠稽核抓。 2026-07-10
金額型別的三段遷移:double、Decimal、再到 Money extension type
金額欄位從 double 換 Decimal 只解決精度、沒解決「任何人都能對它做無意義運算」;用 Dart extension type 包成 Money 之後,型別系統只開放領域有意義的運算。含 implements Object 的 subtype 設計、以及大規模型別遷移前先寫 characterization test 鎖行為的做法。 2026-07-10
核心 entity 重寫、140+ 檔消費端不動 — Deprecated Getter Facade 的過渡設計
重寫被百餘檔引用的核心 entity 時,直接改會同時打爆全部消費端、長期分支的 merge 成本隨時間暴漲。第三條路是 facade:舊欄位保留為 deprecated getter、內部從新結構回讀,消費端零修改編譯通過、@Deprecated 讓編譯器自動列出遷移清單、再逐波清償。facade 要配退場計畫、否則就是永久相容層。 2026-07-10
桌子跟購物車是兩個聚合 — 從「提前結帳」推導生命週期解耦
兩個業務資源該綁死成一對一、還是解耦成獨立生命週期加綁定關係——判準是有沒有業務操作需要其中一方獨立存活。以 POS 的提前結帳、純佔桌、外賣單推導桌位與購物車的聚合邊界,含組合空間大於業務空間時的非法組合封鎖。 2026-07-10
會員身分、計價、支付方式必須一起換 — 耦合欄位的原子切換
多個狀態欄位被同一條業務規則綁住時,分開的 setter 會製造不一致的中間態;把切換收成單一方法、一次狀態更新內同步全部欄位,並注意衍生值重算的順序。以 POS 結帳的會員登出重算為例,含不變式收進 model 的 canCheckout 設計。 2026-06-11
SaaS 選型訪談方法論 - 從使用者操作推導到技術選型
用結構化訪談協議做 SaaS 專案初始化的設計與選型:定錨、交付形態 gate、BDD 操作盤點、DDD domain / event 切分、防護底線與決策記錄,並用合成 dry-run 驗證協議本身 Tarragon (CC BY 4.0) | 使用 hugo 製作