設計原則是指導程式碼設計的「道」,設計模式是具體的「術」。理解原則背後的思想,比死記硬背更重要。
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)#
定義:用戶端不應該被迫依賴它不需要的介面。
「介面」的三種理解:
一組 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);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 的方法:
- 不用同事可能不懂的技術
- 善用現有工具類別
- 不過度最佳化
本身複雜的問題用複雜方法解決,不違反 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 / 最少知識原則
定義:
- 不該有直接依賴關係的類別之間,不要有依賴
- 有依賴關係的類別之間,只依賴必要的介面
目標:高內聚、低耦合
反例:
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 | 只和朋友說話 |