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

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

# 什麼是「耦合」

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

這就是 「高耦合」。

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

### 範例

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

```php
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` (建構子) 接收物件。

## 範例

### 改良第一步：注入具體類別

```php
class OrderService {
    private $mailer;

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

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

### 配合 Interface 注入

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

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

1. 定義介面 (制定規格)：PHP
    
    ```php
    interface MailerInterface {
        public function send($msg);
    }
    ```
    
2. 實作介面 (廠商製造符合規格的產品)：PHP
    
    ```php
    class GmailService implements MailerInterface { ... }
    class AwsSesService implements MailerInterface { ... }
    ```
    
3. 依賴注入 (只認規格，不認廠商)：PHP
    
    ```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 方法」可以用。這就達成了 解耦。
    
    ## 範例
    
    ```php
    // 定義 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)：
    
    ```php
    // 如果 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` 告訴管家該怎麼做：
    
    ```php
    // app/Providers/AppServiceProvider.php
    
    public function register() {
        // 告訴 Laravel：
        // 當有人要 MailerInterface 時，請給他 AwsSesService 的實例
        $this->app->bind(MailerInterface::class, AwsSesService::class);
    }
    ```
    
    之後在 Controller 或其他地方使用時：
    
    ```php
    class OrderController extends Controller {
        // Laravel 的魔法：自動依賴注入 (Auto-wiring)
        // 不需要自己 new，Laravel 會知道需要 OrderService
        // 然後自動去解析 OrderService 需要 MailerInterface
        // 然後自動 new 出 AwsSesService 塞進去
        public function store(OrderService $service) {
            $service->processOrder();
        }
    }
    ```
    
    ### IOC + 建構子依賴注入範例
    
    ```php
    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
        
        它看到了：
        
        ```php
        public function __construct(
                private readonly UserRepository                $userRepository,
                private readonly MessageRepository                $messageRepository,
        )
        {
        }
        ```
        
        Laravel 驚覺：「哇，要製造 `ChatService` 之前，我得先製造 `UserRepository` 和 `MessageRepository`才行！」
        
    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
    
    ```php
    class ChatService {
        // 這裡依賴的是介面，不是具體類別
        public function __construct(UserRepositoryInterface $repo) { ... }
    }
    ```
    
    這時 Laravel 的 Reflection 會卡住：「呃... `UserRepositoryInterface` 只是一個合約，它不是一個可以 `new` 的類別啊！我到底要給他 `SqlUserRepository` 還是 `MongoUserRepository`？」
    
    這時候，就必須在 `AppServiceProvider` 裡手動告訴 Laravel：
    
    ```php
    // app/Providers/AppServiceProvider.php
    public function register()
    {
        // 告訴 Laravel：只要有人討要在 UserRepositoryInterface
        // 請給他 UserRepository (具體的實作)
        $this->app->bind(UserRepositoryInterface::class, UserRepository::class);
    }
    ```
    
    只要加了這行設定，自動注入的魔法鏈就能繼續運作下去了。
