Skip to main content

Command Palette

Search for a command to run...

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

Updated
4 min read
B

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

從這一章開始,我們要從「把功能做出來 (Make it work)」進階到「把程式寫好 (Make it right)」。而區分 Rookie 與 Junior 最明顯的分水嶺,就在於是否懂得 「解耦 (Decoupling)」。

什麼是「耦合」

想像買了一台桌機,但它的滑鼠是「焊死」在主機板上的。如果想換成電競滑鼠?抱歉,要把整台主機板拆換掉。如果滑鼠壞了?抱歉,整台電腦送修。

這就是 「高耦合」。

在程式碼中,最常見的高耦合就是 在類別內部直接 new 另一個物件。

範例

假設我們有一個 OrderService (訂單服務),它在結帳後需要寄信通知用戶:

class GmailService {
    public function send($msg) {
        echo "使用 Gmail 寄出: $msg";
    }
}

class OrderService {
    private $mailer;

    public function __construct() {
        // ❌ 在這裡直接綁死了 GmailService
        // OrderService 現在對 GmailService 有「強依賴」
        $this->mailer = new GmailService();
    }

    public function processOrder() {
        // 處理訂單...
        $this->mailer->send("訂單成立");
    }
}

這個寫法的災難點:

  1. 難以抽換:如果明天老闆說:「Gmail 太貴了,換成 AWS SES。」你需要打開所有用到 GmailService 的檔案,一行一行改。

  2. 難以測試:當寫單元測試 (Unit Test) 時,想測試 OrderService 的邏輯,但程式碼一跑就會真的去連 Gmail。無法把 Gmail 替換成一個「假的、不會真的寄信」的測試物件。

救星登場:依賴注入 (Dependency Injection, DI)

DI 的核心原則是「依賴反轉 (DIP)」:

  1. 高層模組不應該依賴於低層模組,它們應該依賴於抽象。

  2. 抽象不應該依賴於細節,細節應該依賴於抽象。

依賴反轉原則是將高層模組的實現從低層模組中抽象出來,這樣可以使高層模組和低層模組解耦,從而提高系統的靈活性、可維護性和可擴展性。

聽起來很複雜,但核心觀念只有一句話: 「不要自己造工具,讓別人把工具傳進去。」

我們把 new 的動作拿掉,改成從 __construct (建構子) 接收物件。

範例

改良第一步:注入具體類別

class OrderService {
    private $mailer;

    // ✅ 改由外部「注入」進來
    public function __construct(GmailService $mailer) {
        $this->mailer = $mailer;
    }
}

這樣好了一點,至少 OrderService 不用自己 new 了。但在型別宣告上,我們還是寫死了 GmailService

配合 Interface 注入

為了徹底解耦,我們需要引入上一篇提到的 Interface

依賴注入 是「手段」,而介面 與抽象 是讓這個手段能發揮最大效果的「前提」。

  1. 定義介面 (制定規格):PHP

     interface MailerInterface {
         public function send($msg);
     }
    
  2. 實作介面 (廠商製造符合規格的產品):PHP

     class GmailService implements MailerInterface { ... }
     class AwsSesService implements MailerInterface { ... }
    
  3. 依賴注入 (只認規格,不認廠商):PHP

     class OrderService {
         private $mailer;
    
         // ✅ 重點:這裡 Type Hint 寫的是 Interface,不是具體 Class
         public function __construct(MailerInterface $mailer) {
             $this->mailer = $mailer;
         }
    
         public function processOrder() {
             $this->mailer->send("訂單成立");
         }
     }
    
  4. 現在,OrderService 不知道也不在乎它是用 Gmail 還是 AWS,它只知道傳進來的東西「有一個 send 方法」可以用。這就達成了 解耦。

    範例

     // 定義 UserRepository 介面
     interface UserRepository {
         public function saveUser($user);
         public function getUser($id);
     }
    
     // 實現 UserRepository 介面,使用 MySQL 存儲用戶資料
     class MysqlUserRepository implements UserRepository {
         public function saveUser($user) {
             // 將用戶資料存儲到 MySQL 資料庫中
             // ...
         }
         public function getUser($id) {
             // 從 MySQL 資料庫中獲取用戶資料
             // ...
         }
     }
    
     // UserController 類別依賴 UserRepository 介面
     class UserController {
         private $userRepository;
         //這裡依賴注入 透過UserRepository Interface所產生的repository
         public function __construct(UserRepository $userRepository) {
             $this->userRepository = $userRepository;
         }
         public function registerUser($userData) {
             // 驗證用戶資料
             // ...
             // 將用戶資料存儲到資料庫中
             $this->userRepository->saveUser($userData);
             // 發送註冊成功的通知
             // ...
         }
         public function getUser($id) {
             // 根據用戶 ID 獲取用戶資料
             $user = $this->userRepository->getUser($id);
             // 返回用戶資料
             return $user;
         }
     }
    
     // 建立 MySQLUserRepository 並注入到 UserController 中
     $userController = new UserController( new MySQLUserRepository() );
     $userController->getUser(123);
    

    在這個例子中,使用了依賴注入的手法(模式)來達到依賴反轉的核心概念。

    UserController 類別依賴 UserRepository 介面,而不是具體的資料存儲實現。

    這樣,當需要更改資料存儲方式時,只需要創建一個新的實現,並將其注入到 UserController 類別中即可。這符合依賴反轉原則。

    什麼是 IoC Container (控制反轉容器)

    可能會有疑惑:「原本在裡面 new 很方便,現在改成 DI,變成我在使用 OrderService 的時候,要自己手動 new 依賴傳進去,豈不是很麻煩?」

    手動注入的痛苦 (Dependency Hell):

     // 如果 OrderService 依賴 Mailer,Mailer 又依賴 Logger...
     $logger = new FileLogger();
     $mailer = new GmailService($logger);
     $orderService = new OrderService($mailer); // 累死人
    

    這時候就需要 IoC Container (在 Laravel 中稱為 Service Container)。

    它就像是一個 「超級管家」 或 「自動工廠」。只需要設定一次配置,之後它會自動解決所有的依賴關係。

    IoC 控制反轉的意思:

    • 原本:控制權在 OrderService 手上,它主動去 new 依賴。

    • 現在:控制權交給了 Container,Container 決定要給 OrderService 什麼物件。

Laravel 中的實戰 (Service Provider)

在 Laravel,我們通常在 AppServiceProvider 告訴管家該怎麼做:

    // app/Providers/AppServiceProvider.php

    public function register() {
        // 告訴 Laravel:
        // 當有人要 MailerInterface 時,請給他 AwsSesService 的實例
        $this->app->bind(MailerInterface::class, AwsSesService::class);
    }

之後在 Controller 或其他地方使用時:

    class OrderController extends Controller {
        // Laravel 的魔法:自動依賴注入 (Auto-wiring)
        // 不需要自己 new,Laravel 會知道需要 OrderService
        // 然後自動去解析 OrderService 需要 MailerInterface
        // 然後自動 new 出 AwsSesService 塞進去
        public function store(OrderService $service) {
            $service->processOrder();
        }
    }

IOC + 建構子依賴注入範例

    class ChatService
    {
        public function __construct(
            private readonly UserRepository                $userRepository,
            private readonly MessageRepository                $messageRepository,
        )
        {
        }
    }

Q:沒有在 Controller 裡 new ChatService,也沒有手動傳那些 Repository 進去,那它們到底是怎麼出現的?

A:Laravel 的 Service Container (服務容器) 利用 PHP 的 Reflection (反射機制) 自動完成的。

Reflection

Laravel 著使用 PHP 內建的 Reflection API 去「偷看」程式碼結構。

  1. 檢查 Controller: Laravel 檢查 sendMessage 方法,發現參數裡有一個型別提示 (Type Hint) 寫著 ChatService。 Laravel 心想:「好,這個工程師需要 ChatService,我得幫他生一個出來。」

  2. 檢查 Service (遞迴解析): Laravel 準備去 new ChatService,但在這之前,它會先去偷看 ChatService__construct (建構子)。PHP

    它看到了:

     public function __construct(
             private readonly UserRepository                $userRepository,
             private readonly MessageRepository                $messageRepository,
     )
     {
     }
    

    Laravel 驚覺:「哇,要製造 ChatService 之前,我得先製造 UserRepositoryMessageRepository才行!」

  3. 檢查 Repository (繼續遞迴): Laravel 接著去看 UserRepository__construct

    • 如果 UserRepository 沒有依賴其他東西,Laravel 就直接 new UserRepository()

    • 如果它還有依賴,Laravel 就繼續往下挖(這就是遞迴)。

組裝與回傳

當 Laravel 把最底層的零件都做出來後,它就開始一層一層往回組裝:

  1. 先把 UserRepository, MessageRepository... 全部 new 好。

  2. 把這些 Repository 塞進 new ChatService(這裡是剛做好的 Repositories)

  3. 把做好的 $chatService 物件,塞進 sendMessage($request, $chatService)

  4. 最後,執行 Controller 程式碼。

這整個過程發生在 毫秒之間,而且完全自動化。這就是為什麼我們說 Laravel 的 Container 是一個強大的 「自動依賴解析器 (Automatic Dependency Resolution)」。

什麼時候「自動注入」會失效

如果這樣寫:

PHP

    class ChatService {
        // 這裡依賴的是介面,不是具體類別
        public function __construct(UserRepositoryInterface $repo) { ... }
    }

這時 Laravel 的 Reflection 會卡住:「呃... UserRepositoryInterface 只是一個合約,它不是一個可以 new 的類別啊!我到底要給他 SqlUserRepository 還是 MongoUserRepository?」

這時候,就必須在 AppServiceProvider 裡手動告訴 Laravel:

    // app/Providers/AppServiceProvider.php
    public function register()
    {
        // 告訴 Laravel:只要有人討要在 UserRepositoryInterface
        // 請給他 UserRepository (具體的實作)
        $this->app->bind(UserRepositoryInterface::class, UserRepository::class);
    }

只要加了這行設定,自動注入的魔法鏈就能繼續運作下去了。

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

Part 13 of 31

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

Up next

Lesson 14: 物件導向的靈魂-介面與抽象類別

很多剛轉職或自學的工程師,最討厭看到的兩個關鍵字就是 interface 和 abstract。 心中一定有過這個疑問:「為什麼要寫一個『空的』函式放在那邊?直接把程式碼寫在 Class 裡面不好嗎?」 這堂課我們要來解開這個誤會。這不是為了增加程式碼行數,而是為了 「多型 (Polymorphism)」 與 「抽換 (Swap)」 的彈性。 繼承的盲點:為什麼不能只用 extends 假設我們在做一個「物流系統」,有「黑貓 (BlackCat)」和「新竹物流 (HCT)」。 直覺寫法通常是這樣...

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

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