Lesson 16: SOLID 實戰篇 (1)-單一職責 (SRP) 與 高內聚
接下來我們要進到SOLID的實戰,這堂課我們將專注於最基礎、但也最重要的第一條原則:SRP (單一職責原則),並探討它與 高內聚 (High Cohesion) 的緊密關係。
核心觀念定義
關於單一職責原則
"A class should have one, and only one, reason to change." — Robert C. Martin (Uncle Bob)
「一個類別應該只有一個『改變的理由』。」
很多人誤以為 SRP 意思是「一個類別只做一件事 (Do one thing)」,這其實不完全精確。更準確的說法是:一個類別應該只負責「一類」業務邏輯,並且只對「一種」角色負責。
如果會計部門要求修改薪資計算邏輯,你不應該動到負責「資料庫存取」的程式碼。
如果 DBA 要求更換資料庫欄位,你不應該動到負責「生成報表」的程式碼。
SRP 的核心不是「功能數量」,而是「變更來源是否單一」。
關於高內聚
內聚是指模組內的元素(屬性、方法)彼此之間的關聯程度。
高內聚: 類別內的程式碼都是為了完成同一個目的而緊密協作的。
低內聚: 類別像是一個大雜燴,裡面放了毫不相關的功能(例如:一個類別同時負責算數學、寄 Email 和連線資料庫)。
SRP 與高內聚的關係
遵守 SRP 的類別,自然會傾向於高內聚;因為你把不相關的東西拆出去了,剩下的都是高度相關的。
反面範例 - 上帝物件
讓我們看一個後端常見的「壞味道」範例。這是一個負責處理訂單的 Service,但它管得太寬了。
class OrderService
{
public function placeOrder($orderData)
{
// 1. 驗證邏輯 (責任:資料正確性)
if (empty($orderData['items'])) {
throw new Exception("Order must have items.");
}
// 2. 計算邏輯 (責任:業務規則)
$total = 0;
foreach ($orderData['items'] as $item) {
$total += $item['price'] * $item['qty'];
}
if ($total > 1000) { // 滿千送百邏輯
$total -= 100;
}
// 3. 資料庫邏輯
$db = new Database();
$db->query("INSERT INTO orders ...");
// 4. 通知邏輯
$mailer = new Mailer();
$mailer->send($orderData['email'], "Order Placed!");
// 5. 格式化邏輯
return json_encode(['status' => 'success', 'total' => $total]);
}
}
為什麼這樣不好?
這個 OrderService 有 5 個改變的理由:
行銷部門想改「滿千送百」規則 ➡️ 要改這個 Class。
IT 部門想換資料庫 ORM ➡️ 要改這個 Class。
營運部門想改 Email 標題 ➡️ 要改這個 Class。
前端要求 API 格式改變 ➡️ 要改這個 Class。
驗證規則變嚴格 ➡️ 要改這個 Class。
這導致了「低內聚」與「高耦合」。改 A 壞 B 的風險極高。
這種寫法在功能「第一次完成時」通常最快,但在需求開始變動後,維護成本會呈指數成長。
重構實戰:職責分離
我們將上述的「大雜燴」拆解成數個專職的類別。
步驟一:拆分職責 (Extract Class)
Validator: 負責檢查資料。
PriceCalculator: 負責商業邏輯(計算金額、折扣)。
OrderRepository: 負責與資料庫溝通。
NotificationService: 負責寄信。
此時的 OrderService 仍然「有一個職責」:協調訂單建立的流程。 SRP 並不要求「沒有邏輯」,而是要求「邏輯的角色單一」。
步驟二:重組 OrderService
現在的 OrderService 不再親自做所有事情,而是變成一個 指揮官。
namespace App\Services;
use App\Validators\OrderValidator;
use App\Calculators\PriceCalculator;
use App\Repositories\OrderRepository;
use App\Services\NotificationService;
class OrderService
{
public function __construct(
private readonly OrderValidator $validator,
private readonly PriceCalculator $calculator,
private readonly OrderRepository $repository,
private readonly NotificationService $notifier
) {
}
public function placeOrder(array $orderData)
{
// 1. 驗證
$this->validator->validate($orderData);
// 2. 計算
$order = $this->calculator->calculateTotal($orderData);
// 3. 儲存
$savedOrder = $this->repository->save($order);
// 4. 通知
$this->notifier->sendOrderConfirmation($savedOrder);
return $savedOrder;
}
}
| 優點 | 說明 |
| 可讀性 (Readability) | placeOrder 方法現在讀起來像是一個清楚的「流程圖」,而不是一堆細節程式碼。 |
| 可維護性 (Maintainability) | 行銷要改折扣規則?你只需要去 PriceCalculator 修改,完全不用擔心會弄壞 Database 的程式碼。 |
| 可重用性 (Reusability) | NotificationService 可以被其他功能(如「註冊成功」)重複使用,因為它不再綁死在訂單邏輯裡。 |
| 可測試性 (Testability) | 你可以輕鬆地針對 PriceCalculator 寫單元測試,而不需要真的連線資料庫或寄出 Email。 |
常見誤區:SRP 不等於「碎屍萬段」
在學習 SRP 時,最容易犯的錯誤就是 「拆分過細」 (Over-engineering)。
錯誤觀念: 每個 Function 只能寫一行程式碼,或者每個 Class 只能有一個 Function。
正確觀念: 內聚性是關鍵。如果兩個功能總是「一起被使用」且「一起被修改」,它們就應該在一起。
判斷標準: 問自己:「這個類別是否負責了不同的角色 (Actors) 的需求?」 如果一個類別同時包含了「會計需要的邏輯」和「DBA 需要的邏輯」,那就是違反 SRP。

