Skip to main content

Lesson 16: SOLID 實戰篇 (1)-單一職責 (SRP) 與 高內聚

Updated 2 min readMarkdown
Part of series從Rookie到Junior,一個後端成長的30堂課

接下來我們要進到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 個改變的理由:

  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 不再親自做所有事情,而是變成一個 指揮官。

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。

About the author

  • 一個很忙的工程師,比起寫code更喜歡股票,QQQ買起來,謝謝!

No comments yet. Be the first to comment.

從Rookie到Junior,一個後端成長的30堂課

Part 20 of 31

“從 Rookie 到 Junior:一個後端成長的 30 堂課” 是一套專為後端新手所設計的成長型技術系列文章。內容以實務為導向,逐步拆解後端工程的核心能力,包括程式語言基礎、架構思維、框架運作原理、業務邏輯設計、資料庫操作、以及常見的開發模式。 本系列的目標是協助讀者從零散的學習堆疊,建立成體系的後端知識框架。讀者能夠理解各語言背後不變的工程思維與設計原則。這套內容旨在讓學習者從「會寫程式」進入「能理解系統設計」的階段,逐步具備勝任 Junior Backend Engineer 的能力。

Up nextLesson 17: SOLID 實戰篇 (2)-開放封閉 (OCP) - 擁抱變化而不修改舊碼接下來我們來講講SOLID的O - OCP,開放封閉原則。 核心觀念定義 如果說 SRP 是為了整理程式碼,那麼 OCP 就是為了保護程式碼。它是防止「新需求搞壞舊功能」的最強盾牌。 “Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.” — Bertrand Meyer 「軟體實體應該對『擴充』開放,但對『修改』封閉。...

More from this blog