設計原則是指導程式碼設計的「道」,設計模式是具體的「術」。理解原則背後的思想,比死記硬背更重要。

SOLID 原則#

SOLID 是五個設計原則的首字母縮寫,由 Robert C. Martin 提出。

S - 單一職責原則 (SRP)#

定義:一個類別只負責一個職責。

判斷職責是否單一沒有標準答案,取決於業務場景和抽象層次。

判斷指標

  • 類別程式碼行數過多(超過 200 行需警惕)
  • 類別依賴或被依賴的類別過多
  • 私有方法過多
  • 難以取一個準確的類別名稱
  • 大量方法操作同一組屬性

案例分析

// UserInfo 是否違反 SRP?
public class UserInfo {
    private String username;
    private String email;
    private String provinceOfAddress;
    private String cityOfAddress;
    // ...
}

答案取決於場景:

  • 純展示用途 → 不違反
  • 地址需要獨立用於物流系統 → 應拆分

先寫粗粒度的類別,隨業務發展再重構拆分。這就是持續重構。

過度拆分的反例

// 拆分成 Serializer 和 Deserializer 看似更單一
// 但修改協定格式時需要同時改兩處,降低了內聚性
public class Serializer { }
public class Deserializer { }

O - 開放封閉原則 (OCP)#

定義:對擴展開放,對修改關閉。

開閉原則是最重要的設計原則。23 種設計模式大多是為了實現它。

核心思想:添加新功能時,盡量通過擴展程式碼實現,而非修改現有程式碼。

反例

public class Alert {
    public void check(String api, long requestCount,
            long errorCount, long durationOfSeconds) {
        // 每次新增告警規則都要修改這個方法
        if (tps > rule.getMaxTps()) { /* ... */ }
        if (errorCount > rule.getMaxErrorCount()) { /* ... */ }
        // 新增逾時告警需要修改這裡...
    }
}

重構後

public abstract class AlertHandler {
    public abstract void check(ApiStatInfo apiStatInfo);
}

public class TpsAlertHandler extends AlertHandler {
    @Override
    public void check(ApiStatInfo apiStatInfo) { /* ... */ }
}

// 新增告警只需新增 Handler,不修改現有程式碼
public class TimeoutAlertHandler extends AlertHandler {
    @Override
    public void check(ApiStatInfo apiStatInfo) { /* ... */ }
}

實現開閉原則的方法

  • 多型
  • 依賴注入
  • 基於介面而非實作程式設計
  • 設計模式(策略、裝飾、模板方法等)

L - 里氏替換原則 (LSP)#

定義:子類別物件能夠替換父類別物件出現的任何地方,且程式邏輯行為不變。

LSP 與多型不同。多型是語法機制,LSP 是設計原則。

核心:按照協定設計(Design by Contract)

子類別設計時要遵守父類別的「約定」:

  • 函式聲明的功能
  • 輸入、輸出、例外的約定
  • 註釋中的任何特殊說明

違反 LSP 的情況

// 父類別
public class Transporter {
    public Response sendRequest(Request request) {
        // 正常發送,不會拋例外
    }
}

// 子類別違反 LSP
public class SecurityTransporter extends Transporter {
    @Override
    public Response sendRequest(Request request) {
        if (appToken == null) {
            throw new NoAuthorizationException(); // 改變了行為約定
        }
        return super.sendRequest(request);
    }
}

判斷方法:父類別的單元測試應該能通過子類別。

I - 介面隔離原則 (ISP)#

定義:用戶端不應該被迫依賴它不需要的介面。

「介面」的三種理解

  1. 一組 API 集合

    // 部分呼叫者只需要查詢功能
    public interface UserService {
        UserInfo getUserById(long id);
    }
    
    // 刪除功能單獨成介面,只給後台系統
    public interface RestrictedUserService {
        boolean deleteUserById(long id);
    }

2. **單個函式**

   ```java
   // count() 做太多事
   public Statistics count(Collection<Long> dataSet);

   // 拆分成職責單一的函式
   public Long max(Collection<Long> dataSet);
   public Long min(Collection<Long> dataSet);
   public Long average(Collection<Long> dataSet);
  1. OOP 介面

    // 分離熱更新和查看功能
    public interface Updater {
        void update();
    }
    
    public interface Viewer {
        String outputInPlainText();
    }
    
    // 按需實作
    public class RedisConfig implements Updater, Viewer { }
    public class KafkaConfig implements Updater { }
    public class MysqlConfig implements Viewer { }

**與 SRP 的區別**:

- SRP 從設計者角度看職責
- ISP 從呼叫者角度看依賴

### D - 依賴反轉原則 (DIP)

**定義**:高層模組不依賴低層模組,兩者都依賴抽象。

**三個相關概念**:

| 概念           | 性質     | 說明                   |
| -------------- | -------- | ---------------------- |
| 控制反轉 (IoC) | 設計思想 | 程式執行流程由框架控制 |
| 依賴注入 (DI)  | 編碼技巧 | 依賴物件由外部傳入     |
| 依賴反轉 (DIP) | 設計原則 | 依賴抽象而非實作       |

**依賴注入示例**:

```java
// 非依賴注入:內部創建依賴
public class Notification {
    private MessageSender sender = new SmsSender(); // 寫死
}

// 依賴注入:外部傳入依賴
public class Notification {
    private MessageSender sender;

    public Notification(MessageSender sender) {
        this.sender = sender; // 可替換
    }
}

依賴反轉示例

Tomcat(高層)和 Web 應用(低層)都依賴 Servlet 規範(抽象)。

其他重要原則#

KISS 原則#

Keep It Simple and Stupid - 保持簡單

程式碼行數少 ≠ 簡單

反例:用正則表達式驗證 IP

// 看似簡短,實則複雜
public boolean isValidIp(String ip) {
    return ip.matches("^(1\\d{2}|2[0-4]\\d|...)$");
}

遵循 KISS 的方法

  1. 不用同事可能不懂的技術
  2. 善用現有工具類別
  3. 不過度最佳化

本身複雜的問題用複雜方法解決,不違反 KISS。KMP 演演算法複雜但用在字串匹配核心是合理的。

YAGNI 原則#

You Ain’t Gonna Need It - 你不會需要它

核心:不要做過度設計。

// 目前只用 Redis,不要提前寫 ZooKeeper 支援
// 但要預留擴展點
public interface ConfigSource {
    String getValue(String key);
}

public class RedisConfigSource implements ConfigSource { }
// ZookeeperConfigSource 等需要時再實作

KISS vs YAGNI

  • KISS:如何做(保持簡單)
  • YAGNI:要不要做(不需要的不做)

DRY 原則#

Don’t Repeat Yourself - 不要重複自己

重複的程式碼不一定違反 DRY;看似不重複的程式碼可能違反 DRY。

三種重複

類型說明是否違反 DRY
實作邏輯重複程式碼相似但語義不同不違反
功能語義重複程式碼不同但功能相同違反
執行重複相同邏輯執行多次違反

案例

// 實作相似但語義不同 - 不違反 DRY
boolean isValidUsername(String s) { /* 驗證規則 */ }
boolean isValidPassword(String s) { /* 相同規則 */ }
// 未來 username 和 password 的規則可能分化

// 功能相同但實作不同 - 違反 DRY
boolean isValidIp(String ip) { /* 正則實作 */ }
boolean checkIfIpValid(String ip) { /* 字串解析實作 */ }
// 應該統一成一個函式

迪米特法則 (LOD)#

Law of Demeter / 最少知識原則

定義

  1. 不該有直接依賴關係的類別之間,不要有依賴
  2. 有依賴關係的類別之間,只依賴必要的介面

目標:高內聚、低耦合

反例

public class NetworkTransporter {
    // 底層通訊類別不應依賴具體的 HtmlRequest
    public byte[] send(HtmlRequest request) { }
}

// 應改為
public byte[] send(String address, byte[] data) { }

介面隔離實現 LOD

// 只需要序列化功能的類別,不應該看到反序列化
public interface Serializable {
    String serialize(Object obj);
}

public interface Deserializable {
    Object deserialize(String str);
}

public class Serialization implements Serializable, Deserializable {
    // 完整實作
}

// 呼叫者只依賴需要的介面
public class DemoClass {
    private Serializable serializer;
}

原則之間的關係#

flowchart TD
    GOAL[高內聚、低耦合] --> SRP[SRP<br>類別設計]
    GOAL --> DIP[DIP<br>依賴設計]
    GOAL --> LOD[LOD<br>通訊設計]

    SRP --> ISP[ISP<br>介面設計]
    DIP --> OCP[OCP<br>擴展設計]

    ISP -.->|支持| GOAL
    OCP -.->|最終目標| GOAL

這些原則相互支持:

  • SRP + ISP → 高內聚
  • DIP + LOD → 低耦合
  • OCP 是最終目標
設計原則速查表
原則一句話總結
SRP一個類別只做一件事
OCP擴展開放,修改關閉
LSP子類別可替換父類別
ISP介面小而專
DIP依賴抽象不依賴實作
KISS保持簡單
YAGNI不需要的不做
DRY不要重複
LOD只和朋友說話