Lesson 15: 解耦的關鍵 - 依賴注入 (DI) 與 IoC Container
從這一章開始,我們要從「把功能做出來 (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("訂單成立");
}
}
這個寫法的災難點:
難以抽換:如果明天老闆說:「Gmail 太貴了,換成 AWS SES。」你需要打開所有用到
GmailService的檔案,一行一行改。難以測試:當寫單元測試 (Unit Test) 時,想測試
OrderService的邏輯,但程式碼一跑就會真的去連 Gmail。無法把 Gmail 替換成一個「假的、不會真的寄信」的測試物件。
救星登場:依賴注入 (Dependency Injection, DI)
DI 的核心原則是「依賴反轉 (DIP)」:
高層模組不應該依賴於低層模組,它們應該依賴於抽象。
抽象不應該依賴於細節,細節應該依賴於抽象。
依賴反轉原則是將高層模組的實現從低層模組中抽象出來,這樣可以使高層模組和低層模組解耦,從而提高系統的靈活性、可維護性和可擴展性。
聽起來很複雜,但核心觀念只有一句話: 「不要自己造工具,讓別人把工具傳進去。」
我們把 new 的動作拿掉,改成從 __construct (建構子) 接收物件。
範例
改良第一步:注入具體類別
class OrderService {
private $mailer;
// ✅ 改由外部「注入」進來
public function __construct(GmailService $mailer) {
$this->mailer = $mailer;
}
}
這樣好了一點,至少 OrderService 不用自己 new 了。但在型別宣告上,我們還是寫死了 GmailService。
配合 Interface 注入
為了徹底解耦,我們需要引入上一篇提到的 Interface
依賴注入 是「手段」,而介面 與抽象 是讓這個手段能發揮最大效果的「前提」。
定義介面 (制定規格):PHP
interface MailerInterface { public function send($msg); }實作介面 (廠商製造符合規格的產品):PHP
class GmailService implements MailerInterface { ... } class AwsSesService implements MailerInterface { ... }依賴注入 (只認規格,不認廠商):PHP
class OrderService { private $mailer; // ✅ 重點:這裡 Type Hint 寫的是 Interface,不是具體 Class public function __construct(MailerInterface $mailer) { $this->mailer = $mailer; } public function processOrder() { $this->mailer->send("訂單成立"); } }現在,
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 去「偷看」程式碼結構。
檢查 Controller: Laravel 檢查
sendMessage方法,發現參數裡有一個型別提示 (Type Hint) 寫著ChatService。 Laravel 心想:「好,這個工程師需要ChatService,我得幫他生一個出來。」檢查 Service (遞迴解析): Laravel 準備去
new ChatService,但在這之前,它會先去偷看ChatService的__construct(建構子)。PHP它看到了:
public function __construct( private readonly UserRepository $userRepository, private readonly MessageRepository $messageRepository, ) { }Laravel 驚覺:「哇,要製造
ChatService之前,我得先製造UserRepository和MessageRepository才行!」檢查 Repository (繼續遞迴): Laravel 接著去看
UserRepository的__construct。如果
UserRepository沒有依賴其他東西,Laravel 就直接new UserRepository()。如果它還有依賴,Laravel 就繼續往下挖(這就是遞迴)。
組裝與回傳
當 Laravel 把最底層的零件都做出來後,它就開始一層一層往回組裝:
先把
UserRepository,MessageRepository... 全部new好。把這些 Repository 塞進
new ChatService(這裡是剛做好的 Repositories)。把做好的
$chatService物件,塞進sendMessage($request, $chatService)。最後,執行 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);
}
只要加了這行設定,自動注入的魔法鏈就能繼續運作下去了。

