Skip to main content

Command Palette

Search for a command to run...

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

Updated
2 min read
B

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

接下來我們要進到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。

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

Part 12 of 31

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

Up next

Lesson 15: 解耦的關鍵 - 依賴注入 (DI) 與 IoC Container

從這一章開始,我們要從「把功能做出來 (Make it work)」進階到「把程式寫好 (Make it right)」。而區分 Rookie 與 Junior 最明顯的分水嶺,就在於是否懂得 「解耦 (Decoupling)」。 什麼是「耦合」 想像買了一台桌機,但它的滑鼠是「焊死」在主機板上的。如果想換成電競滑鼠?抱歉,要把整台主機板拆換掉。如果滑鼠壞了?抱歉,整台電腦送修。 這就是 「高耦合」。 在程式碼中,最常見的高耦合就是 在類別內部直接 new 另一個物件。 範例 假設我們有一個 O...

More from this blog

Lesson 26 : 系統韌性的守護者-限流、熔斷與背壓的設計模式

當這幾個名詞出現後,代表我們進到了一個高併發/大流量的系統了。在這個章節中,我們一起來看看如何透過一些方式來避免高併發導致我們的系統crash掉。 限流(Rate Limiting) 相信大家對這個名詞並不陌生,限流其實就是字面上的含意,限制流量。 限流的目的是保護「接收方」,確保系統不會因為瞬間的高併發請求而癱瘓。 限流通常發生在 API Gateway 或服務的最前端。它像是一個夜店門口的保全

Mar 26, 20262 min read

Lesson 25: 淺談 單體架構、微服務架構與單/多租戶架構

過去我們討論了 要把程式寫在哪、程式要怎麼拆的題目,接下來我們來看看「如何服務不同客戶」,這些架構反映了軟體開發在擴充性與複雜度之間的權衡。 軟體架構深度解析:從系統拆分到商業規模化 在軟體工程的演進中,架構的選擇往往是在「開發效率」、「系統擴充性」與「營運成本」之間尋求平衡。我們可以從兩個核心維度來觀察這些架構:系統如何運行(單體 vs. 分散式) 以及 如何服務客戶(單租戶 vs. 多租戶)。

Mar 26, 20262 min read

Lesson 24: 資料庫擴展術-讀寫分離、複寫機制與快取一致性挑戰

為什麼要讀寫分離? 大多數的 Web 應用都是 「讀多寫少」(例如:看文的人多,發文的人少,Heavy Read System)。當所有的請求都塞給同一台資料庫時,磁碟 I/O 和連線數會成為瓶頸。 Master (主庫): 負責寫入 (Insert/Update/Delete),確保數據一致性。 Slave (從庫): 負責讀取 (Select),可以有多個從庫來分擔讀取壓力。 為什麼讀

Mar 25, 20262 min read

面試經驗談 2025-2026

從2025年3月開始,我陸陸續續參與了從新創到上市櫃公司的Senior - Tech Lead的相關面試,其中有不乏 尊重面試者、展現高度專業的企業(公司),當然也有遇到幾場面試鬼故事,這篇文章主要分享我對於軟體工程師面試的方向分享,以及部分鬼故事,以此警惕自己不要成為這樣的面試官。 AI的洪流,改變了SWE的生態 LLM的發展確確實實的影響到了軟體工程師的生態。 過去受限於算力與資料規模,深度學

Mar 23, 20262 min read

Lesson 23: 系統的緩衝區-Queue 佇列與非同步處理 (Asynchronous)

佇列 佇列的實作工具非常多,舉凡AWS SQS、RabbitMQ、Kafka…等。 佇列的特性,其實是一個非常強大的系統緩衝區,應用層面非常廣。 什麼是佇列? 佇列可以想像成,在既有流程中外,有另一個”水管”,來連接原有的資料流(或邏輯過程),其中 呼叫方將資料 推(Push)到水管中,接受方(監聽) 從水管中將資料拉(Pull)出處理 為什麼佇列是「強大的緩衝區」? 在同步處理中,系統像是一

Mar 23, 20262 min read

Bennett's Tech Blog | 後端架構、系統設計

32 posts

來自台灣的軟體工程師,相信軟體可以改變世界