PHP DIY 系列------框架篇:8. 依賴注入和控制反轉

13sai發表於2020-02-20

依賴倒置原則(Dependence Inversion Principle)

DIP是物件導向設計原則之一。
傳統軟體設計中,上層程式碼依賴於下層程式碼,當下層出現變動時, 上層程式碼也要相應變化,維護成本較高。而DIP的核心思想是上層定義介面,下層實現這個介面, 從而使得下層依賴於上層,降低耦合度,提高整個系統的彈性。這是一種經實踐證明的有效策略。

控制反轉(Inversion of Control)

IoC則是DIP的一種具體思路,DIP只是一種理念、思想,而IoC是一種實現DIP的方法。 IoC的核心是將類(上層)所依賴的單元(下層)的例項化過程交由第三方來實現。

當呼叫者需要被呼叫者的協助時,在傳統的程式設計過程中,通常由呼叫者來建立被呼叫者的例項,但在這裡,建立被呼叫者的工作不再由呼叫者來完成,而是將被呼叫者的建立移到呼叫者的外部,從而反轉被呼叫者的建立,消除了呼叫者對被呼叫者建立的控制,因此稱為控制反轉。

依賴注入(Dependence Injection)

DI是IoC的一種設計模式,按照DI的模式,就可以實現IoC。 DI的實質就是把一個類不可能更換的部分和可更換的部分分離開來,通過注入的方式來使用,從而達到解耦的目的。

這裡我們舉個例子(旅行的介面)說明一下:

interface Travel
{
    public function travelAlgorithm();
}

/**
 *乘坐飛機
 */
class AirPlanelStrategy implements Travel
{
    public function travelAlgorithm()
    {
        echo"travelbyAirPlain\r\n";
    }
}

/**
 *乘坐火車
 */
class TrainStrategy implements Travel
{
    public function travelAlgorithm()
    {
        echo"travelbyTrain\r\n";
    }
}

/**
 *
 *演算法解決類,以提供客戶選擇使用何種解決方案:
 */
class PersonContext
{
    private $strategy = null;

    public function __construct(Travel $travel)
    {
        $this->strategy=$travel;
    }

    /**
     *旅行
     */
    public function travel()
    {
        return$this->strategy->travelAlgorithm();
    }

}
// 乘坐火車旅行
$person = new PersonContext(new TrainStrategy());
$person->travel();

// 改乘飛機
$person =PersonContext(new AirPlanelStrategy());
$person->travel();

當我們更換交通工具時,只需要去增加Travel介面的實現,修改下實現的程式碼介面,無需去改動核心程式碼。

控制反轉容器(IoC Container)

當專案比較大時,依賴關係可能會十分複雜。 而IoC Container提供了動態地建立、注入依賴單元,對映依賴關係等功能,方便開發者使用,並大大縮減了許多程式碼量。

因為我們的框架比較簡單,我們不妨實現下Ioc容器(主要參考了Yii的di容器)。

程式碼實現用到了PHP的反射Api,有疑問的不妨先看看手冊:

還記得PSR嗎?PSR11是關於依賴注入容器介面規範:

然後我們利用composer執行

composer require psr/container

我們在library/Components新建Container實現ContainerInterface,在library/Exceptions下新建ContainerException實現Psr\Container\ContainerExceptionInterface,新建ContainerNotFoundException實現Psr\Container\NotFoundExceptionInterface。

我們主要來實現一下Container程式碼,我們預先定義三個屬性,用以儲存物件、依賴及依賴的定義資訊。

<?php

namespace Library\Components;

use Library\Exceptions\ContainerException;
use Library\Exceptions\ContainerNotFoundException;
use Psr\Container\ContainerInterface;
use ReflectionClass;

class Container implements ContainerInterface
{
    // 用於儲存依賴的定義,以物件名稱為鍵
    private $definitions = [];

    // 用於快取ReflectionClass物件,以物件名稱為鍵
    private $reflections = [];

    // 用於快取依賴資訊,以物件名稱為鍵
    private $dependencies = [];

    public function has($class)
    {
        return isset($this->definitions[$class]);
    }

    public function get($class)
    {
        ...
    }
}

我們先新增一個set方法,用以定義,

    public function set($class, $definition = [])
    {
        $this->definitions[$class] = $this->normalizeDefinition($class, $definition);
        return $this;
    }

    protected function normalizeDefinition($class, $definition)
    {
        // $definition 是空的轉換成 ['class' => $class] 形式
        if (empty($definition)) {
            return ['class' => $class];

            // $definition 是字串,轉換成 ['class' => $definition] 形式
        } elseif (is_string($definition)) {
            return ['class' => $definition];

            // $definition 是物件,則直接將其作為依賴的定義
        } elseif (is_object($definition)) {
            return $definition;

            // $definition 是陣列則確保該陣列定義了 class 元素
        } elseif (is_array($definition)) {
            if (!isset($definition['class'])) {
                $definition['class'] = $class;
            }
            return $definition;
            // 這也不是,那也不是,那就丟擲異常算了
        } else {
            throw new ContainerException(
                "不支援的型別: \"$class\": " . gettype($definition));
        }
    }

知識點:

  • gettype — 獲取變數的型別

然後我們重點實現get方法:

    public function get($class)
    {
        // 加入未作set操作,我們依舊可以構建
        if (!isset($this->definitions[$class])) {
            return $this->build($class);
        }

        $definition = $this->definitions[$class];
        if (is_array($definition)) {
            $concrete = $definition['class'];
            unset($definition['class']);

            if ($concrete === $class) {
                $object = $this->build($class, $definition);
            } else {
                $object = $this->get($concrete);
            }
        } elseif (is_object($definition)) {
            return $this->_singletons[$class] = $definition;
        } else {
            throw new ContainerNotFoundException('不能識別的物件型別: ' . gettype($definition));
        }

        return $object;

    }

build方法如下,主要是構建出物件並實現注入,

    public function build($class, $params = [])
    {
        try {
            // 通過反射api獲取物件
            $reflector = $this->getReflectionClass($class);

            // 獲取依賴關係陣列
            $dependencies = $this->getDependencies($class, $reflector);

            // 建立一個類的新例項,給出的引數將傳遞到類的建構函式.
            $reflector =  $reflector->newInstanceArgs($dependencies);

            return $reflector;
        } catch (\Throwable $t) {
            throw new ContainerException('反射出錯');
        }
    }

獲取物件:

    public function getReflectionClass($class)
    {
        if (isset($this->reflections[$class])) {
            return $this->reflections[$class];
        }

        $reflector = new ReflectionClass($class);
        if (!$reflector->isInstantiable()) {
            throw new ContainerException("不能例項化".$class);
        }

        return $this->reflections[$class] = $reflector;
    }

獲取依賴關係:

    public function getDependencies($class, $reflector)
    {
        // 判斷是否有快取依賴關係
        if (isset($this->dependencies[$class])) {
            return $this->dependencies[$class];
        }
        $constructor = $reflector->getConstructor();

        #如果沒有建構函式, 直接例項化並返回
        if (is_null($constructor)) {
            return $this->dependencies[$class] = [];
        }

        $parameters = $constructor->getParameters();

        $dependencies = [];
        foreach ($parameters as $className) {
            $dependency = $className->getClass();

            if (is_null($dependency)) {
                $dependencies[] = $this->resolveNoneClass($className);
            } else {
                // 先取出容器中繫結的類 否則自動繫結
                $dependencies[] = $this->get($dependency->getName());
            }
        }

        $this->dependencies[$class] = $dependencies;

        return $dependencies;
    }

    public function resolveNoneClass($class)
    {
        // 有預設值則返回預設值
        if ($class->isDefaultValueAvailable()) {
            return $class->getDefaultValue();
        }
        throw new ContainerException('不能解析引數');
    }

到這裡,我們基本就完成了一個完整的IOC Container的程式碼。

我們來寫一個demo:

<?php

namespace App\Https\Controllers;

class C
{

}
<?php

namespace App\Https\Controllers;

class B
{
    public function __construct(C $c)
    {
        $this->ccc = $c;
    }
}
<?php

namespace App\Https\Controllers;

class A
{
    public function __construct(B $b, C $c)
    {
        $this->bbb = $b;
        $this->ccc = $c;
    }
}
<?php

namespace App\Https\Controllers;

use Library\Components\Container;
use Library\Https\Controller;

class IndexController extends Controller
{
    public function index()
    {
        $contain = new Container();
        $contain->set('App\\Https\\Controllers\\A');
        p($contain->get('App\\Https\\Controllers\\A'));
    }
}

我們可以看到的結果:

image

是不是程式碼簡潔很多了,不願因為需要建立一個A物件,而先去例項化B和C,這些都是由我們完成的IOC Container去實現了。

總結

這一節我們實現了IOC容器,然而你如果仔細去想,我們會發現我們可以讓容器更加強大,比如單例物件的實現,比如依賴的擴充套件(相容物件引數注入,陣列引數注入等)。這些你可以自行實現,我也在原始碼做了簡單的擴充套件,大家可以思考試著實現一下,當然也可以看看開源框架Laravel、Yii的服務容器的實現。

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

分享開發知識,歡迎交流。qq957042781

相關文章