物件導向設計
概覽
這套規則專為採用物件導向風格的程式碼庫而設計,並非通用建議。專案自身的書面慣例優先於此套件。請遵循正在修改的模組的慣用語法。此技能會評估任務規模並命名適合的技能。
在回答之前,請先載入此索引所命名的技能:其規則與範例存在於該技能中,而非此處。
任務需要多少設計
| 任務 | 使用 | 跳過 |
|---|---|---|
| 只使用一次即捨棄的答案 | 無 | 所有 |
| 現有類別中的一個方法 | 其名稱與合約 | 重新建模 |
| 現有流程內的一個功能 | 該流程使用的慣用語 | 新邊界 |
| 一個新的類別或模組 | 設計迴圈 | 無 |
| 一個新的上下文 (Context) | 設計迴圈,然後是其邊界 | 無 |
| 從無到有的新系統 | 設計迴圈、上下文,然後是結構 | 無 |
在設計之前
在設計之前,先陳述守門規則:軟體必須遵守的一條規則,並提供一個它接受和一個它拒絕的範例。例如:「一個時段最多只能容納一個預約。」它接受將第二位病人預約到另一個時段,並拒絕將第二位病人預約到同一個時段。如果你能將規則寫下來,就開始設計。如果你做不到,那麼缺少的就不是設計,而這套技能也無法提供。
設計迴圈
- 用領域的語言來說明軟體必須做什麼 (說領域語言), 並找出知道每個行為需要什麼的物件 (分配責任)。
- 找出一致性邊界:哪些事物必須在同一個交易中保持一致 (設計聚合)。
- 決定每一條規則的位置:實體 (Entity)、值物件 (Value Object),還是領域服務 (Domain Service) (放置領域邏輯)。
- 設計每個物件應答的訊息 (封裝狀態與行為), 並使非法狀態無法被建構 (使非法狀態無法存在)。
- 反轉依賴,使領域層不認識資料庫或框架 (反轉依賴, 分層系統)。
- 使用領域的語言來命名事物,並僅註解程式碼無法表達的內容 (命名類別與方法, 註解程式碼無法表達的內容)。
- 根據下方的紅旗警訊檢查結果 (審查物件設計)。
規則的嚴格程度
此套件中的每一條核心規則都帶有三個標籤之一,標籤說明了其嚴格程度。
- 規則 (Rule)。 違反即為缺陷。沒有框架可以豁免。
- 預設 (Default)。 除非有明確的條件成立,否則請執行此操作。條件本身就是一個句子,絕不是附加子句。
- 判斷 (Judgement)。 兩種答案都是可辯護的。技能會說明是什麼因素打破了平衡。
症狀對應技能
症狀指向處理它們的技能,此表格會隨著技能的增加而增長。
| 症狀 | 技能 |
|---|---|
| 哪個類別應該做這件事? | 分配責任 |
| 這個類別做太多事了 | 分配責任 |
| 一個變更波及許多類別 | 診斷耦合 |
| 這兩個部分的耦合程度如何? | 診斷耦合 |
| 這是物件還是單純的資料? | 決定物件或資料 |
| 我們的領域模型是貧血的 | 決定物件或資料 |
| 其他程式碼替這個物件做決定 | 封裝狀態與行為 |
| Setter 讓此物件可以違反其規則 | 封裝狀態與行為 |
| 此物件可以半成品狀態存在 | 建構有效的物件 |
| 這個建構函數需要太多引數 | 建構有效的物件 |
| 每個新案例都意味著編輯運作中的程式碼 | 封裝會變動的部分 |
| 這個功能應該有多少彈性? | 封裝會變動的部分 |
| 這是值物件還是實體? | 建模值與實體 |
| 原始字串和數字攜帶了領域規則 | 建模值與實體 |
| 這個類別應該繼承那個類別,還是持有它? | 選擇組合勝過繼承 |
| 變更基底類別會破壞其子類別 | 選擇組合勝過繼承 |
| 子類別違反了其父類別承諾的內容 | 尊重可替代性 |
| 程式碼檢查它持有哪個子類型 | 尊重可替代性 |
| 同一種型別的 switch 判斷一再出現 | 用多型取代條件式 |
| 子類別還是模式匹配? | 用多型取代條件式 |
| 程式碼穿過一串物件鏈 | 限制物件的所知範圍 |
| 這個類別只轉發呼叫 | 限制物件的所知範圍 |
| 這個類別或方法應該叫什麼? | 命名類別與方法 |
| 像 Manager、Helper 或 Util 的名稱 | 命名類別與方法 |
| 呼叫者依賴於它們從未使用的介面方法 | 設計介面 |
| 介面還是抽象基底類別? | 設計介面 |
| 領域程式碼依賴於資料庫或框架 | 反轉依賴 |
| 物件在哪裡被建立和組裝? | 反轉依賴 |
| 呼叫者知道另一個類別如何儲存或格式化資料 | 隱藏實作 |
| 一個類別洩漏了其內部錯誤或步驟 | 隱藏實作 |
| 標記或可選欄位允許不可能的組合 | 使非法狀態無法存在 |
| 一個型別允許領域中從不曾有的值 | 使非法狀態無法存在 |
| 程式碼的用詞與團隊的用詞不同 | 說領域語言 |
| 一個詞對兩個團隊代表不同意義 | 說領域語言 |
| 哪些物件必須一起變更? | 設計聚合 |
| 儲存時發生衝突,或一次載入拉進太多東西 | 設計聚合 |
| 哪些狀態變更是允許的? | 建模生命週期 |
| 一個已取消或已關閉的物件再次發生變更 | 建模生命週期 |
| 領域物件應該如何被儲存和尋找? | 設計儲存庫 |
| 儲存庫有一個針對每個畫面的 find 方法 | 設計儲存庫 |
| 使用案例重複執行相同的商業檢查 | 放置領域邏輯 |
| 領域服務、實體方法還是應用服務? | 放置領域邏輯 |
| 其他部分必須在變更儲存後做出反應 | 發布領域事件 |
| 針對一個已回滾的變更發出了訊息 | 發布領域事件 |
| 一個團隊的模型在哪裡結束,另一個的在哪裡開始? | 界定上下文 |
| 哪些部分是核心,哪些我們可以購買? | 界定上下文 |
| 各層應該朝哪個方向依賴? | 分層系統 |
| 一個畫面或作業無法在沒有其框架的情況下進行測試 | 分層系統 |
| 模組之間形成循環依賴 | 組織模組 |
| 模組化單體還是獨立服務? | 組織模組 |
| 對我們 API 的變更破壞了呼叫它的應用程式 | 跨越系統邊界 |
| 我們該如何對這個 API 或訊息進行版本控制? | 跨越系統邊界 |
| 映射工具塑造了我們的領域類別 | 馴服持續性 |
| 一個列表對每一行執行一次查詢 | 馴服持續性 |
| 畫面需要的資料形狀與模型不同 | 分離讀取與寫入 |
| 讀取模型、投影還是事件溯源? | 分離讀取與寫入 |
| 這個畫面該用 MVC、MVP 還是 MVVM? | 分離呈現層與領域層 |
| 一個畫面直接編輯領域物件 | 分離呈現層與領域層 |
| 哪個設計模式適合這個問題? | 選擇模式 |
| 在問題出現之前就加入了模式 | 選擇模式 |
| 這些模式類別可能只是一個函式 | 語言提供的模式 |
| Visitor,還是帶有匹配的封閉集合? | 語言提供的模式 |
| 一個方法在找不到東西時回傳 null | 設計失敗行為 |
| 拒絕以呼叫者忽略的錯誤碼形式回傳 | 設計失敗行為 |
| 執行緒共享一個物件,遺失或混淆了其變更 | 管理共享狀態 |
| 對此狀態使用鎖還是單一持有者? | 管理共享狀態 |
| 對另一個系統的呼叫掛起或執行到一半失敗 | 使工作可靠 |
| 重試導致工作被執行了兩次 | 使工作可靠 |
| 我們無法根據記錄解釋事件 | 設計要記錄的內容 |
| 我們應該記錄、計數或稽核什麼? | 設計要記錄的內容 |
| 審查發現一個神級類別或熔岩流 | 辨識反模式 |
| 我們應該先修正哪個反模式? | 辨識反模式 |
| 測試雖然行為沒有改變卻失敗了 | 測試物件導向程式碼 |
| 對此協作者使用 Mock、Stub 還是 Fake? | 測試物件導向程式碼 |
| 測試依賴於時間或隨機值 | 設計可測試性 |
| 程式碼僅為了測試可以存取而被設為公開 | 設計可測試性 |
| 我們如何安全地重構這份程式碼? | 重構朝向物件設計 |
| 重構變得太大而無法完成 | 重構朝向物件設計 |
| 我們必須變更沒有測試的程式碼 | 處理舊有程式碼 |
| 逐步取代舊系統 | 處理舊有程式碼 |
| 這次審查應該檢查什麼? | 審查物件設計 |
| 哪些審查發現會阻止合併? | 審查物件設計 |
| 沒有人記得為什麼程式碼會是這個形狀 | 記錄設計決策 |
| 我們應該畫什麼,以及在什麼層級? | 記錄設計決策 |
| 註解重複了程式碼或與之矛盾 | 註解程式碼無法表達的內容 |
| 我們在哪裡解釋為什麼這段程式碼看起來很奇怪? | 註解程式碼無法表達的內容 |
| 一個框架鉤子或標記持有商業規則 | 在框架內工作 |
| 我們應該在多大程度上順應框架? | 在框架內工作 |
| 一個計算也儲存、發送或改變了它的輸入 | 應用函數式技巧 |
| 哪些函數式技巧適合物件程式碼? | 應用函數式技巧 |
語言技能
語言技能將核心規則保留在一個語言中,絕不取代它們。基礎語言技能適用於沒有框架的程式碼。
oo-java,來自object-oriented-design-java:記錄 (records)、密封類型 (sealed types)、equals和hashCode、受檢例外 (checked exceptions)、Optional,以及禁止擴展的類別。oo-kotlin,來自object-oriented-design-kotlin:資料類別 (data classes)、密封類型 (sealed types)、可空類型 (nullable types)、使用by的委派 (delegation),以及擴展函式 (extension functions)。oo-typescript,來自object-oriented-design-typescript:品牌識別字 (branded identifiers)、#private狀態、抽象類別 (abstract classes)、裝飾器 (decorators)、差別聯合 (discriminated unions),以及組合根 (composition root)。oo-javascript,來自object-oriented-design-javascript:c
…
[因篇幅限制而截斷]