SOLID 原則與設計模式參考

在步驟 3(SOLID 審查)與步驟 4(程式碼骨架)時使用本參考。


SOLID 速查清單

SRP(Single Responsibility Principle,單一職責原則)

測試:「我能不能用一句不含『而且』的話描述這個類別在做什麼?」

違反徵兆

  • 類別的方法觸及多個不相關的關注點
  • 變更理由來自不同的關係人/領域
  • 類別取了像 ManagerHandlerUtil 這種含糊後綴的名字

常見修法

  • 依關注點拆成多個類別
  • 把一組相關欄位萃取成 Value Object
  • 把跨實體邏輯萃取成 domain service

OCP(Open/Closed Principle,開放封閉原則)

測試:「我能不能在不修改既有類別的前提下,新增一種行為變體?」

違反徵兆

  • 新增一個 case 就得改 switchif/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.ConnectionHttpClient、框架註解
  • 測試需要真實資料庫或外部服務
  • 抽換基礎設施就得重寫 domain 邏輯

常見修法

  • 在 domain 層定義介面、在 infrastructure 層實作(Repository 模式)
  • 透過建構子注入依賴
  • 採用 ports-and-adapters/hexagonal architecture

設計模式:何時套用

在步驟 3 用這張表來決定哪些模式值得提出。

模式何時套用…何時別套用…
Strategy多個演算法依資料而異,且你想在執行期抽換它們只有一種變體,或變體永遠不變
Specification需要 and/or/not 組合的布林過濾規則單一固定過濾、不需組合
Template Method多個子類別共用一套流程、各步驟不同「共用流程」很瑣碎(只有 1-2 行)
Repositorydomain 需要持久化、但不該依賴儲存技術「儲存」在記憶體中且永不改變
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);
    }
}

對於複雜述詞,偏好具名類別(AndSpecificationOrSpecification 等)而非 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」類別

取名 XxxManagerXxxHandlerXxxProcessor 的類別,往往是 SRP 違反的偽裝。問:「到底是 manager 什麼?」若答案是好幾件事,就拆。

貧血領域模型(Anemic Domain Model)

只有 getter/setter、沒有行為的類別。所有邏輯都住在 service 裡。常是把 domain 物件當 DTO 的徵兆。

修法:把操作單一實體狀態的邏輯,搬進該實體本身。

上帝物件(God Object)

一個什麼都知道、什麼都做的類別。通常源於「我就再多加這一件事在這裡吧」。

修法:辨識邊界(職責),萃取出來。

基本型別偏執(Primitive Obsession)

對有意義的東西到處用 Stringintlong。例如:到處傳 String userId,其實用 UserId 會更安全。

修法:為具備識別或語意約束的 domain 概念引入 Value Object。

依戀情節(Feature Envy)

類別 A 上的某方法,用 B 的資料比用 A 自己的還多。通常代表該方法該屬於 B,而非 A。

修法:把方法搬到資料所在的地方。


何時該拒絕一個模式提案

有時使用者(或你)想套用一個其實沒解決任何問題的模式。拒絕也是良好架構的一環

  • 若只有一種 strategy,別加 Strategy
  • 若資料完全在記憶體中且永不改變,別加 Repository
  • 若建構子只有 3 個參數,別加 Builder
  • 若階層穩定且只有一種操作,別加 Visitor
  • 若直接呼叫更清楚,別加 Observer

套用模式是成本,不是好處。 唯有它解決的設計痛點是真實的,才值得付這個成本。