# 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，但它管得太寬了。

```php
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 個改變的理由：

1. 行銷部門想改「滿千送百」規則 ➡️ 要改這個 Class。
    
2. IT 部門想換資料庫 ORM ➡️ 要改這個 Class。
    
3. 營運部門想改 Email 標題 ➡️ 要改這個 Class。
    
4. 前端要求 API 格式改變 ➡️ 要改這個 Class。
    
5. 驗證規則變嚴格 ➡️ 要改這個 Class。
    

這導致了「低內聚」與「高耦合」。改 A 壞 B 的風險極高。

這種寫法在功能「第一次完成時」通常最快，但在需求開始變動後，維護成本會呈指數成長。

## 重構實戰：職責分離

我們將上述的「大雜燴」拆解成數個專職的類別。

### 步驟一：拆分職責 (Extract Class)

1. **Validator**: 負責檢查資料。
    
2. **PriceCalculator**: 負責商業邏輯（計算金額、折扣）。
    
3. **OrderRepository**: 負責與資料庫溝通。
    
4. **NotificationService**: 負責寄信。
    

此時的 OrderService 仍然「有一個職責」：協調訂單建立的流程。 SRP 並不要求「沒有邏輯」，而是要求「邏輯的角色單一」。

### 步驟二：重組 OrderService

現在的 `OrderService` 不再親自做所有事情，而是變成一個 **指揮官**。

```php
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。
