從這一章開始,我們要從「把功能做出來 (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() {
$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;
public function __construct(MailerInterface $mailer) {
$this->mailer = $mailer;
}
public function processOrder() {
$this->mailer->send("訂單成立");
}
}
現在,OrderService 不知道也不在乎它是用 Gmail 還是 AWS,它只知道傳進來的東西「有一個 send 方法」可以用。這就達成了 解耦。
範例
interface UserRepository {
public function saveUser($user);
public function getUser($id);
}
class MysqlUserRepository implements UserRepository {
public function saveUser($user) {
}
public function getUser($id) {
}
}
class UserController {
private $userRepository;
public function __construct(UserRepository $userRepository) {
$this->userRepository = $userRepository;
}
public function registerUser($userData) {
$this->userRepository->saveUser($userData);
}
public function getUser($id) {
$user = $this->userRepository->getUser($id);
return $user;
}
}
$userController = new UserController( new MySQLUserRepository() );
$userController->getUser(123);
在這個例子中,使用了依賴注入的手法(模式)來達到依賴反轉的核心概念。
UserController 類別依賴 UserRepository 介面,而不是具體的資料存儲實現。
這樣,當需要更改資料存儲方式時,只需要創建一個新的實現,並將其注入到 UserController 類別中即可。這符合依賴反轉原則。
什麼是 IoC Container (控制反轉容器)
可能會有疑惑:「原本在裡面 new 很方便,現在改成 DI,變成我在使用 OrderService 的時候,要自己手動 new 依賴傳進去,豈不是很麻煩?」
手動注入的痛苦 (Dependency Hell):
$logger = new FileLogger();
$mailer = new GmailService($logger);
$orderService = new OrderService($mailer);
這時候就需要 IoC Container (在 Laravel 中稱為 Service Container)。
它就像是一個 「超級管家」 或 「自動工廠」。只需要設定一次配置,之後它會自動解決所有的依賴關係。
IoC 控制反轉的意思:
Laravel 中的實戰 (Service Provider)
在 Laravel,我們通常在 AppServiceProvider 告訴管家該怎麼做:
public function register() {
$this->app->bind(MailerInterface::class, AwsSesService::class);
}
之後在 Controller 或其他地方使用時:
class OrderController extends Controller {
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。
組裝與回傳
當 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:
public function register()
{
$this->app->bind(UserRepositoryInterface::class, UserRepository::class);
}
只要加了這行設定,自動注入的魔法鏈就能繼續運作下去了。