搞懂依賴注入, 用 PHP 手寫簡易 IOC 容器

前言

好的設計會提高程序的可複用性和可維護性,也間接的提高了開發人員的生產力。今天,我們就來說一下在很多框架中都使用的依賴注入。

一些概念

要搞清楚什麼是依賴注入如何依賴注入,首先我們要明確一些概念。

DIP (Dependence Inversion Principle) 依賴倒置原則:

程序要依賴於抽象接口,不要依賴於具體實現。

IOC (Inversion of Control) 控制反轉:

遵循依賴倒置原則的一種代碼設計方案,依賴的創建 (控制) 由主動變爲被動 (反轉)。

DI (Dependency Injection) 依賴注入:

控制反轉的一種具體實現方法。通過參數的方式從外部傳入依賴,將依賴的創建由主動變爲被動 (實現了控制反轉)。

光說理論有點不好理解,我們用代碼舉個例子。

首先,我們看依賴沒有倒置時的一段代碼:


class Controller
{
    protected $service;

    public function __construct()
    {
        // 主動創建依賴
        $this->service = new Service(12, 13); 
    }       
}

class Service
{
    protected $model;
    protected $count;

    public function __construct($param1, $param2)
    {
        $this->count = $param1 + $param2;
        // 主動創建依賴
        $this->model = new Model('test_table'); 
    }
}

class Model
{
    protected $table;

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

$controller = new Controller;

上述代碼的依賴關係是 Controller 依賴 Service,Service 依賴 Model。從控制的角度來看,Controller 主動創建依賴 Service,Service 主動創建依賴 Model。依賴是由需求方內部產生的,需求方需要關心依賴的具體實現。這樣的設計使代碼耦合性變高,每次底層發生改變(如參數變動),頂層就必須修改代碼。

接下來,我們使用依賴注入實現控制反轉,使依賴關係倒置:

class Controller
{
    protected $service;
    // 依賴被動傳入。申明要 Service 類的實例 (抽象接口)
    public function __construct(Service $service)
    {
        $this->service = $service; 
    }       
}

class Service
{
    protected $model;
    protected $count;
    // 依賴被動傳入
    public function __construct(Model $model, $param1, $param2)
    {
        $this->count = $param1 + $param2;
        $this->model = $model; 
    }
}

class Model
{
    protected $table;

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

$model = new Model('test_table');
$service = new Service($model, 12, 13);
$controller = new Controller($service);

將依賴通過參數的方式從外部傳入(即依賴注入),控制的角度上依賴的產生從主動創建變爲被動注入,依賴關係變爲了依賴於抽象接口而不依賴於具體實現。此時的代碼得到了解耦,提高了可維護性。

從單元測試的角度看,依賴注入更方便 stub 和 mock 操作,方便了測試人員寫出質量更高的測試代碼。

如何依賴注入,自動注入依賴

有了上面的一些理論基礎,我們大致瞭解了依賴注入是什麼,能幹什麼。

不過雖然上面的代碼可以進行依賴注入了,但是依賴還是需要手動創建。我們可不可以創建一個工廠類,用來幫我們進行自動依賴注入呢?OK,我們需要一個 IOC 容器。

實現一個簡單的 IOC 容器

依賴注入是以構造函數參數的形式傳入的,想要自動注入:

  • 我們需要知道需求方需要哪些依賴,使用反射來獲得
  • 只有類的實例會被注入,其它參數不受影響

如何自動進行注入呢?當然是 PHP 自帶的反射功能!

注:關於反射是否影響性能,答案是肯定的。但是相比數據庫連接、網絡請求的時延,反射帶來的性能問題在絕大多數情況下並不會成爲應用的性能瓶頸。

1.雛形

首先,創建 Container 類,getInstance 方法:

class Container
{
    public static function getInstance($class_name, $params = [])
    {
        // 獲取反射實例
        $reflector = new ReflectionClass($class_name);
        // 獲取反射實例的構造方法
        $constructor = $reflector->getConstructor();
        // 獲取反射實例構造方法的形參
        $di_params = [];
        if ($constructor) {
            foreach ($constructor->getParameters() as $param) {
                $class = $param->getClass();
                if ($class) { // 如果參數是一個類,創建實例
                    $di_params[] = new $class->name;
                }
            }
        }
        
        $di_params = array_merge($di_params, $params);
        // 創建實例
        return $reflector->newInstanceArgs($di_params);
    }
}

這裏我們獲取構造方法參數時用到了 ReflectionClass 類,大家可以到官方文檔瞭解一下該類包含的方法和用法,這裏就不再贅述。

ok,有了 getInstance 方法,我們可以試一下自動注入依賴了:

class A
{
    public $count = 100;
}

class B
{
    protected $count = 1;

    public function __construct(A $a, $count)
    {
        $this->count = $a->count + $count;
    }

    public function getCount()
    {
        return $this->count;
    }
}

$b = Container::getInstance(B::class, [10]);
var_dump($b->getCount()); // result is 110

2.進階

雖然上面的代碼可以進行自動依賴注入了,但是問題是隻能構注入一層。如果 A 類也有依賴怎麼辦呢?

ok,我們需要修改一下代碼:

class Container
{
    public static function getInstance($class_name, $params = [])
    {
        // 獲取反射實例
        $reflector = new ReflectionClass($class_name);
        // 獲取反射實例的構造方法
        $constructor = $reflector->getConstructor();
        // 獲取反射實例構造方法的形參
        $di_params = [];
        if ($constructor) {
            foreach ($constructor->getParameters() as $param) {
                $class = $param->getClass();
                if ($class) { // 如果參數是一個類,創建實例,並對實例進行依賴注入
                    $di_params[] = self::getInstance($class->name);
                }
            }
        }
        
        $di_params = array_merge($di_params, $params);
        // 創建實例
        return $reflector->newInstanceArgs($di_params);
    }
}

測試一下:


class C 
{
    public $count = 20;
}
class A
{
    public $count = 100;

    public function __construct(C $c)
    {
        $this->count += $c->count;
    }
}

class B
{
    protected $count = 1;

    public function __construct(A $a, $count)
    {
        $this->count = $a->count + $count;
    }

    public function getCount()
    {
        return $this->count;
    }
}

$b = Container::getInstance(B::class, [10]);
var_dump($b->getCount()); // result is 130
上述代碼使用遞歸完成了多層依賴的注入關係,程序中依賴關係層級一般不會特別深,遞歸不會造成內存遺漏問題。

3.單例

有些類會貫穿在程序生命週期中被頻繁使用,爲了在依賴注入中避免不停的產生新的實例,我們需要 IOC 容器支持單例模式,已經是單例的依賴可以直接獲取,節省資源。

爲 Container 增加單例相關方法:

class Container
{
    protected static $_singleton = []; 

    // 添加一個實例到單例
    public static function singleton($instance)
    {
        if ( ! is_object($instance)) {
            throw new InvalidArgumentException("Object need!");
        }
        $class_name = get_class($instance);
        // singleton not exist, create
        if ( ! array_key_exists($class_name, self::$_singleton)) {
            self::$_singleton[$class_name] = $instance;
        }
    }
    // 獲取一個單例實例
    public static function getSingleton($class_name)
    {
        return array_key_exists($class_name, self::$_singleton) ?
                self::$_singleton[$class_name] : NULL;
    }
    // 銷燬一個單例實例
    public static function unsetSingleton($class_name)
    {
        self::$_singleton[$class_name] = NULL;
    }

}

改造 getInstance 方法:


public static function getInstance($class_name, $params = [])
{
    // 獲取反射實例
    $reflector = new ReflectionClass($class_name);
    // 獲取反射實例的構造方法
    $constructor = $reflector->getConstructor();
    // 獲取反射實例構造方法的形參
    $di_params = [];
    if ($constructor) {
        foreach ($constructor->getParameters() as $param) {
            $class = $param->getClass();
            if ($class) { 
                // 如果依賴是單例,則直接獲取
                $singleton = self::getSingleton($class->name);
                $di_params[] = $singleton ? $singleton : self::getInstance($class->name);
            }
        }
    }
    
    $di_params = array_merge($di_params, $params);
    // 創建實例
    return $reflector->newInstanceArgs($di_params);
}

4.以依賴注入的方式運行方法

類之間的依賴注入解決了,我們還需要一個以依賴注入的方式運行方法的功能,可以注入任意方法的依賴。這個功能在實現路由分發到控制器方法時很有用。

增加 run 方法

public static function run($class_name, $method, $params = [], $construct_params = [])
{
    if ( ! class_exists($class_name)) {
        throw new BadMethodCallException("Class $class_name is not found!");
    }

    if ( ! method_exists($class_name, $method)) {
        throw new BadMethodCallException("undefined method $method in $class_name !");
    }
    // 獲取實例
    $instance = self::getInstance($class_name, $construct_params);

    // 獲取反射實例
    $reflector = new ReflectionClass($class_name);
    // 獲取方法
    $reflectorMethod = $reflector->getMethod($method);
    // 查找方法的參數
    $di_params = [];
    foreach ($reflectorMethod->getParameters() as $param) {
        $class = $param->getClass();
        if ($class) { 
            $singleton = self::getSingleton($class->name);
            $di_params[] = $singleton ? $singleton : self::getInstance($class->name);
        }
    }

    // 運行方法
    return call_user_func_array([$instance, $method], array_merge($di_params, $params));
}

測試:

class A
{
    public $count = 10;
}

class B
{
    public function getCount(A $a, $count)
    {
        return $a->count + $count;
    }
}

$result = Container::run(B::class, 'getCount', [10]);
var_dump($result); // result is 20

ok,一個簡單好用的 IOC 容器完成了,動手試試吧!

完整代碼

IOC Container 的完整代碼請見 wazsmwazsm/IOCContainer, 原先是在我的框架 wazsmwazsm/WorkerA 中使用,現在已經作爲單獨的項目,有完善的單元測試,可以使用到生產環境。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章