本章介紹 SOLID 原則中的第一個——單一職責原則(SRP),說明一個類別應該只有一個被變更的理由,並透過多個案例展示如何識別與分離職責。
原則定義#
一個類別應該只有一個被變更的理由。 (A class should have only one reason to change.)
SRP 與 Tom DeMarco 和 Meilir Page-Jones 所提出的**內聚性(Cohesion)**概念密切相關。內聚性描述一個模組內各元素之間的功能關聯程度。SRP 將內聚性的定義與變更的理由聯繫在一起:一個內聚的類別,其所有功能都服務於同一個職責。
違反症狀#
當一個類別承擔了多個職責時:
- 一個職責的變更可能影響另一個職責,導致意料之外的破壞
- 使用者只需要其中一個職責,卻被迫依賴於另一個不需要的職責所帶來的套件
- 類別變得僵化與脆弱——修改某項功能卻波及看似無關的部分
案例一:Rectangle 的雙重職責#

Figure 8.1: More than one responsibility
假設有一個 Rectangle 類別,同時具備兩個功能:
- 計算幾何屬性(如面積
Area())——供計算幾何應用程式使用 - 在螢幕上繪製自己(
Draw())——供圖形界面應用程式使用
這兩個職責來自完全不同的使用情境。如果圖形界面的需求變更導致 Rectangle 被修改,那麼計算幾何應用程式也必須重新編譯、測試與部署——即使它根本不需要繪圖功能。

Figure 8.2: Separated responsibilities
正確的做法是將兩個職責分離到不同的類別中:GeometricRectangle(負責計算)與 Rectangle(負責繪圖),或者讓繪圖功能獨立於幾何計算。
案例二:Modem 介面#
考慮以下的 Modem 介面:
public interface IModem
{
void Dial(string phoneNumber);
void Hangup();
void Send(char c);
char Recv();
}這個介面包含兩個職責:
- 連線管理:
Dial()和Hangup() - 資料通訊:
Send()和Recv()
這兩個職責是否需要分離?取決於應用程式變更的方式。如果連線管理的 signature 會因為某些原因改變,而資料通訊的部分不受影響,那就應該分離:
public interface IDataChannel
{
void Send(char c);
char Recv();
}
public interface IConnection
{
void Dial(string phoneNumber);
void Hangup();
}實作類別 ModemImplementation 可以同時實作兩個介面。這樣一來,即使兩個職責的實作仍在同一個類別中,但至少客戶端不會感知到這種耦合——需要資料通訊的模組只依賴 IDataChannel,需要連線管理的模組只依賴 IConnection。
技巧: 將
ModemImplementation視為一個「不完美但被隔離的妥協」(a kludge behind a fence)。介面的分離已經足以保護客戶端,即使實作內部並未完全解耦。
案例三:持久化耦合#

Figure 8.4: Coupled persistence
一個 Employee 類別同時負責商業邏輯(如 CalculatePay())與持久化(如 Store())。這兩個職責幾乎不應該混在一起——商業規則的變更不應該影響資料存取的方式,反之亦然。
如何判斷是否需要分離?#
重點: 變更的軸線(axis of change)只有在變更確實發生時才具有意義。如果沒有症狀出現,就不應該套用 SRP 來分離職責——這麼做反而會引入不必要的複雜性(Needless Complexity)。
SRP 是最簡單的原則之一,卻也是最難正確運用的。將職責結合在一起是我們自然而然的做法;找出並分離職責,才是軟體設計真正的藝術所在。後續章節將介紹的其他原則,在許多方面都會回歸到這個基本概念。
本章小結#
SRP 告訴我們每個類別應該專注於單一職責。職責被定義為「變更的理由」——如果你能想出一個以上的理由來修改一個類別,那這個類別就承擔了一個以上的職責。識別並分離這些職責,是保持設計清晰與靈活的基礎。