SOLID 原則與設計模式參考
在步驟 3(SOLID 審查)與步驟 4(程式碼骨架)時使用本參考。
SOLID 速查清單
SRP(Single Responsibility Principle,單一職責原則)
測試:「我能不能用一句不含『而且』的話描述這個類別在做什麼?」
違反徵兆:
- 類別的方法觸及多個不相關的關注點
- 變更理由來自不同的關係人/領域
- 類別取了像
Manager、Handler、Util這種含糊後綴的名字
常見修法:
- 依關注點拆成多個類別
- 把一組相關欄位萃取成 Value Object
- 把跨實體邏輯萃取成 domain service
OCP(Open/Closed Principle,開放封閉原則)
測試:「我能不能在不修改既有類別的前提下,新增一種行為變體?」
違反徵兆:
- 新增一個 case 就得改
switch或if/else鏈 - 既有類別名稱裡列舉了型別(
UserOrAdminOrGuest) - 修某型別的 bug 卻意外弄壞其他型別
常見修法:
- 用多型取代條件判斷(Strategy / Template Method)
- 萃取抽象 + 子類別
- 對可組合的述詞使用 Specification
LSP(Liskov Substitution Principle,里氏替換原則)
測試:「若我寫 Parent p = new Child(),Parent 的所有契約是否仍成立?」
違反徵兆:
- 子類別丟出
UnsupportedOperationException - 子類別要求更強的前置條件
- 子類別弱化了後置條件
- 程式碼各處散落對子型別的
instanceof檢查
常見修法:
- 重整繼承結構:萃取共通介面、捨棄不合適的繼承
- 用組合取代繼承
- 把變動行為移到獨立介面
ISP(Interface Segregation Principle,介面隔離原則)
測試:「這個介面的所有實作者,真的都需要它全部的方法嗎?」
違反徵兆:
- 方法一大堆的胖介面
- 實作裡有空的/會丟例外的方法
- 客戶端依賴了它用不到的方法
常見修法:
- 把胖介面依角色拆成多個介面
- 用 default method 避免空實作
- 萃取更窄的介面,讓客戶端只依賴那些
DIP(Dependency Inversion Principle,依賴反轉原則)
測試:「我的 domain 邏輯是依賴抽象,還是依賴具體型別?」
違反徵兆:
- domain 類別直接 import
java.sql.Connection、HttpClient、框架註解 - 測試需要真實資料庫或外部服務
- 抽換基礎設施就得重寫 domain 邏輯
常見修法:
- 在 domain 層定義介面、在 infrastructure 層實作(Repository 模式)
- 透過建構子注入依賴
- 採用 ports-and-adapters/hexagonal architecture
設計模式:何時套用
在步驟 3 用這張表來決定哪些模式值得提出。
| 模式 | 何時套用… | 何時別套用… |
|---|---|---|
| Strategy | 多個演算法依資料而異,且你想在執行期抽換它們 | 只有一種變體,或變體永遠不變 |
| Specification | 需要 and/or/not 組合的布林過濾規則 | 單一固定過濾、不需組合 |
| Template Method | 多個子類別共用一套流程、各步驟不同 | 「共用流程」很瑣碎(只有 1-2 行) |
| Repository | domain 需要持久化、但不該依賴儲存技術 | 「儲存」在記憶體中且永不改變 |
| Factory(Method/Abstract) | 物件建構有多個步驟或分支 | 建構子很瑣碎 |
| Builder | 物件有許多選用欄位(4+)或組裝複雜 | 欄位少、且全為必填 |
| Composite | 樹狀物件,葉節點與分枝共用介面 | 平坦的物件清單 |
| Visitor | 需在型別階層上新增多種操作、又不想改型別 | 階層穩定、但操作幾乎不變 |
| Observer | 一對多依賴,狀態變更需傳播 | 直接耦合就好且更簡單 |
| Decorator | 動態為物件增添職責 | 繼承就夠了 |
| Adapter | 橋接不相容的介面 | 兩端都由你掌控——直接改介面就好 |
模式套用:詳細指引
Specification(業務系統最常需要)
何時:過濾規則需要組合。例如:
- 「找出是 CNC 型 AND 能做工序 P001 AND 在 1 樓的機台」
- 「找出今天沒有被指派的工人」
結構:
public interface Specification<T> {
boolean isSatisfiedBy(T candidate);
default Specification<T> and(Specification<T> other) {
return c -> isSatisfiedBy(c) && other.isSatisfiedBy(c);
}
default Specification<T> or(Specification<T> other) {
return c -> isSatisfiedBy(c) || other.isSatisfiedBy(c);
}
default Specification<T> not() {
return c -> !isSatisfiedBy(c);
}
}對於複雜述詞,偏好具名類別(AndSpecification、OrSpecification 等)而非 lambda——較易除錯與檢視。
Repository(DDD 標準)
在 domain 層定義介面:
package mydomain.resource;
public interface ResourceRepository {
Optional<Resource> findById(ResourceId id);
List<Resource> findByCapability(Process process);
// ... 不要 setXxx,不要 save(任意值)。
// Repository 是用於檢索;持久化操作應對應到 domain 操作。
}在 infrastructure 層實作 — JDBC、JPA、in-memory、外部 API。domain 程式只碰介面。
測試時,提供一個 InMemoryRepository 實作當作 test fixture。光是這個模式就能消除 80% 對 mocking 框架的需求。
Template Method + Strategy(當行為有變化)
Template Method:流程固定、步驟可變。
public abstract class CalendarException {
public abstract TimeSlots applyTo(TimeSlots base);
// 流程放在 Schedule;每個子類別插入自己的步驟
}
public class OvertimeException extends CalendarException {
public TimeSlots applyTo(TimeSlots base) { return base.union(...); }
}Strategy:行為在執行期可選。
public interface PricingStrategy {
Money calculate(Order order);
}
// Order 有一個 Strategy 欄位;可在不改 Order 的情況下抽換。Template Method 與 Strategy 之間的界線是模糊的。Template Method 用繼承;Strategy 用組合。Strategy 較有彈性但較重;當子類別穩定時,Template Method 較簡單。
須留意的反模式(Anti-Patterns)
「Manager」類別
取名 XxxManager、XxxHandler、XxxProcessor 的類別,往往是 SRP 違反的偽裝。問:「到底是 manager 什麼?」若答案是好幾件事,就拆。
貧血領域模型(Anemic Domain Model)
只有 getter/setter、沒有行為的類別。所有邏輯都住在 service 裡。常是把 domain 物件當 DTO 的徵兆。
修法:把操作單一實體狀態的邏輯,搬進該實體本身。
上帝物件(God Object)
一個什麼都知道、什麼都做的類別。通常源於「我就再多加這一件事在這裡吧」。
修法:辨識邊界(職責),萃取出來。
基本型別偏執(Primitive Obsession)
對有意義的東西到處用 String、int、long。例如:到處傳 String userId,其實用 UserId 會更安全。
修法:為具備識別或語意約束的 domain 概念引入 Value Object。
依戀情節(Feature Envy)
類別 A 上的某方法,用 B 的資料比用 A 自己的還多。通常代表該方法該屬於 B,而非 A。
修法:把方法搬到資料所在的地方。
何時該拒絕一個模式提案
有時使用者(或你)想套用一個其實沒解決任何問題的模式。拒絕也是良好架構的一環:
- 若只有一種 strategy,別加 Strategy
- 若資料完全在記憶體中且永不改變,別加 Repository
- 若建構子只有 3 個參數,別加 Builder
- 若階層穩定且只有一種操作,別加 Visitor
- 若直接呼叫更清楚,別加 Observer
套用模式是成本,不是好處。 唯有它解決的設計痛點是真實的,才值得付這個成本。