寫一個簡單的IoC容器案例,理解什麼是依賴注入和控制反轉

zxr615發表於2021-04-08

簡介

依賴注入(DI),控制反轉(IoC),容器(Container) 經常都經常會提到,但很長一段時間都是一知半解,現在抽空把自己淺顯理解的內容記錄下來,與大家探討。

引子

Route::get('/{id}','\App\Http\Controllers\IndexController@index');
class IndexController extends Controller
{
    public function index(Request $request, $id){
        app(UserService::class)->getUserNameById($id);// TypeError: Too few arguments to function App/Services/UserService::__construct(), 0 passed in Psy Shell code on line 1 and exactly 1 expected
        (new UserService())->getUserNameById($id);
    }
}
class UserService
{
    public $cache;

    public function __construct(Cache $cache)
    {
        $this->cache = $cache;
    }

    public function getUserNameById($id)
    {
        return $this->cache->get('user:id:' . $id);
    }
}

很早之前就很好奇:

  1. 為什麼方法的引數位置①是需要傳入 2 個引數的,一個是 Request 型別的引數,一個是不定型別的 id 引數,但路由只有一個 id 引數,那 $request 引數是哪裡來的?
  2. UserService__construct 方法明確例項化需要一個 Cache 型別的引數,但②中並沒有傳入,為什麼能使用呢?③為什麼使用 new 如果不傳引數就會報錯呢?

不知道大家開發時有沒有好奇過這兩個問題呢?後來聽說這叫 依賴注入 ,也不知道是個啥,那就抱著這兩個疑問開始尋找答案。

貫穿全文

接下來會圍繞這 3 個點來講

  1. 依賴控制

    1. 依賴:誰依賴誰
    2. 注入:注入什麼
  2. 控制反轉

    1. 控制:誰控制誰
    2. 反轉:反轉什麼
  3. 什麼是容器

常規程式碼

Controller1

class Index1Controller
{
    public $userService;

    public function __construct() {
        /**
         * 因為我需要(依賴) UserService() 給我提供資料, 所以建立了一個 UserService() 物件
         *
         * 控制:我 (IndexController) 控制了 UserService() 物件的建立
         * 反轉:我 (IndexController) 絕對控制 UserService() 物件的權利,建立物件的控制權沒有發生轉移,所以沒有反轉,一切都是親力親為。
         */
        $this->userService = new UserService();
    }

    public function index() {
        // 我 (index) 控制了 UserService() 物件的建立
        $userService = new UserService();

        $userName = $userService->getUserName();
        $userName2 = $this->userService->getUserName();

        return [$userName, $userName2];
    }
}

(new IndexController())->index();

Index2.php

<?php
    (new Index1Controller())->index();

生活比喻:

依賴:我要吃麵包,麵包需要(依賴)麵粉才能製作

注入:買麵粉 -> 注入水 -> 製作麵包 -> 吃

控制:我控制了麵包的製作

反轉:無

依賴注入和控制反轉

Controller2

class Index2Controller
{
    public $userService;

    /**
     * 因為我需要(依賴) UserService() 給我提供資料, 所以我需要接收一個 UserService 型別的引數
     * 把依賴從外部傳入進來,把需要的依賴傳入進來了,就是依賴注入
     *
     * 控制:呼叫者控制了 UserService() 物件的建立
     * 反轉:我 (IndexController) 控制 UserService 建立的權利已經沒有了(轉移了),那轉移給誰了?這裡的控制權轉移給呼叫者了。
     */
    public function __construct(UserService $userService)
    {
        $this->userService = $userService;
    }

    public function index()
    {
       /**
         * 在方法中建立物件
         * 我 (index) 控制了 UserService() 物件的建立
         */
        $userService = new UserService();
        $userName  = $userService->getUserName();

        $userName2 = $this->userService->getUserName();

        return [$userName, $userName2];
    }
}

// __construct() 中建立 new UserService() 轉移到了這裡
$userService = new UserService();
// 將 $userService 傳入(注入) controller 中
(new Index2Controller($userService))->index();

Index2.php

<?php
// __construct() 中建立 new UserService() 轉移到了這裡
$userService = new UserService();
// 將 $userService 傳入(注入) controller 的建構函式中
$rs = (new Index2Controller($userService))->index();
var_dump($rs);

生活比喻:

依賴:我要吃麵包,依賴麵包店

注入:告訴麵包店老闆要吃什麼 -> 老闆給你(注入) -> 吃

控制:麵包店老闆控制麵包的製作

反轉:原來我控制麵包的製作的權利沒有了,轉移給了麵包店的老闆

IoC 容器自動注入

上面的 依賴注入和控制反轉 並沒有解決開頭引出的兩個問題的答案,依賴還是需要手動建立,然後手動注入,如何實現依賴的自動注入呢?這個時候就需要一個 IoC 容器了

  • 如何注入

    使用 PHP 提供的 反射(Reflection) 功能

  • 我們需要注入哪裡的引數

    依賴注入是以建構函式引數的形式傳入,所以我們需要自動注入建構函式指定的引數

  • 我們需要注入哪些引數

    我們只注入類例項,其他引數原樣傳入

Container

IoC 容器其實就是一個普通的 class 類,實現了某些功能而已,不必想的太複雜。

class Container
{
      // 在 laravel 中這個方法是 `make()`, 這裡為了方便和常用的 new xxx() 理解,所以命名成了「自動注入的new」
    public static function autoInjectNew($className, $params = [])
    {
        $reflect = new \ReflectionClass($className);
        // 獲取建構函式
        $construct = $reflect->getConstructor();

        // 儲存例項化需要的引數
        $args = [];
        if ($construct) {
            /**
             * 獲取建構函式的引數
             * array(2) {
             *  [0] => object(ReflectionParameter)#3 (1) {["name"]=> string(11) "userService"}
             *  [1] => object(ReflectionParameter)#4 (1) {["name"]=> string(3) "uid"}
             *  }
             */
            $consParams = $construct->getParameters();
            foreach ($consParams as $param) {
                $class = $param->getClass();
                if ($class) {
                    // $args[] = new $class->name();
                    // 如果這樣處理依賴的的 UserService() 還有依賴的話則無法兼顧,所以需要遞迴處理

                    // demo 中這裡相當於就是 new Study\Di\Services\UserService()
                    $args[] = self::autoInjectNew($class->name);
                }
            }
        }

        // 合併引數
        $args = array_merge($args, $params);

        /**
         * IoC 控制反轉:
         *  控制:容器控制了物件的建立
         *  反轉:建立物件的權利已經轉移到了容器中來了,不再是 IndexController() 中的 __construct() 了。
         * DI 依賴注入:
         *  依賴:$args 儲存了儲存了需要那些依賴
         *  注入:把 $args 中的依賴作為引數傳入(注入),返回例項
         */
        // 相當於:$instance = new Index3Controller(new UserService)
        $instance = $reflect->newInstanceArgs($args);

        return $instance;
    }
}

驗證一下

Controller3

class Index3Controller
{
    protected $userService;

    public function __construct(UserService $userService)
    {
        $this->userService = $userService;
    }

    public function index()
    {
        $userName = $this->userService->getUserName();
        return $userName;
    }
}

index3.php

<?php
$index3Instance = Container::autoInjectNew(Index3Controller::class);
$rs = $index3Instance->index();
var_dump($rs);

現在再看看是不是沒有主動傳入 new UserService() 引數也可以成功呼叫啦

回顧問題

  1. 路由中的 Request $request 引數是哪裡來的

    答:請求進入框架之後,框架解析 url 找到相對應的控制器類,呼叫容器寫好的自動注入方法(案例中是autoInjectNew()),進行注入引數,這樣就可以愉快又方便的使用啦。

  2. 使用 app()new 有什麼不同

    答:其實 laravelapp() 就是使用 Container 例項化的一個助手函式,我們可以來寫一個助手函式

    先看看 laravel 中的助手函式

     function app($abstract = null, array $parameters = [])
     {
         if (is_null($abstract)) {
             return Container::getInstance();
         }
    
           // 這裡的 make 就相當於當前專案中的 autoInjectNew()
         return Container::getInstance()->make($abstract, $parameters);
     }

    實現助手函式 app()

    index3.php

     <?php
     $index3Instance = Container::autoInjectNew(Index3Controller::class);
     $rs = $index3Instance->index();
     var_dump("indexRs: ", $rs);
    
     // 使用助手函式
     $appRs = app(Index3Controller::class)->index();
     var_dump("appRs: ", $appRs);
    
     // 助手函式
     function app($class, $params = []) {
         return Container::autoInjectNew($class, $params);
     }

總結

剛開始的時候在網上找了很多相關的文章,但看下來說的似乎都大同小異,但還是不理解,很是苦惱。經常看到「服務容器是 Laravel 的核心」這樣的說法,所以就去從 laravelindex.php 開始一步一步過,但 laravel 的原始碼看的確實也有點頭大,所以我轉了個彎,把 ThinkPHP 的的框架 clone 下來看了看,確實看的輕鬆許多,再回頭看 laravel 的原始碼,還是很複雜,但理解起來相對直接看 laravel 就簡單多了。

文章很多都是作者自己的理解,文章提供的大多也只是很少一部分的程式碼,要弄清楚還是得閱讀原始碼。

這個案例的 Container 中似乎沒有太體現出 容器 這個詞,因為還沒有實現例項化物件的儲存,具體可以看看相關的原始碼。

案例demo

github.com/zxr615/study-ioc

參考

github.com/top-think/framework/blo...

github.com/laravel/framework/blob/...

segmentfault.com/a/119000001894890...

blog.csdn.net/bestone0213/article/...

www.cnblogs.com/DebugLZQ/archive/2...

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章