結合thinkphp,淺談反射、控制反轉和依賴注入

個人博客

文章出自 Inn的博客https://www.gog5.cn/archives/8/
第一次開通博客,歡迎小夥伴們點贊、交換友情連接php

IoC是什麼

建議先看看 IoC基礎,寫的很好。sql

Inversion of Control,即「控制反轉」,不是什麼技術,而是一種設計思想。Ioc意味着將你設計好的對象交給容器控制,而不是傳統的在你的對象內部直接控制。傳統程序設計,咱們直接在對象內部經過new進行建立對象,是程序主動去建立依賴對象;而IoC是有專門一個容器來建立這些對象,即由Ioc容器來控制對象的建立。thinkphp

傳統應用程序是由咱們本身在對象中主動控制去直接獲取依賴對象,也就是正轉,而反轉則是由容器來幫忙建立及注入依賴對象。爲什麼是反轉?由於由容器幫咱們查找及注入依賴對象,對象只是被動的接受依賴對象,依賴對象的獲取被反轉了。數據庫

<?php

interface Database
{
    public function insert();
}

class Mysql implements Database
{
    public function insert()
    {
        echo 'Mysql insert';
    }
}
class Oracle implements Database
{
    public function insert()
    {
        echo 'Oracle insert';
    }
}

class App
{
    protected $database;

    public function __construct()
    {
        $this->database = new Mysql();
    }

    public function insertLog()
    {
        $this->database->insert();
    }
}

$app = new App();
$app->insertLog();

上面的代碼實現了寫入數據庫日誌功能,看起來沒什麼問題,可是若是我要把Mysql改爲Oracle數據庫,那就得從新修改App類了。實際上這並無達到解耦。咱們能夠稍做修改。數組

class App
{
    protected $database;

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

    public function insertLog()
    {
        $this->database->insert();
    }
}

$app = new App(new Oracle());
$app->insertLog();

如今經過構造函數傳遞參數就能夠實現修改數據庫了,由外部注入依賴對象,App對象只是被動的接受依賴對象,因此是反轉;哪些方面反轉了?依賴對象的獲取被反轉了,咱們能夠稱其爲控制反轉。app

IoC和DI

DI—Dependency Injection,即「依賴注入」:是組件之間依賴關係由容器在運行期決定,形象的說,即由容器動態的將某個依賴關係注入到組件之中。依賴注入的目的並不是爲軟件系統帶來更多功能,而是爲了提高組件重用的頻率,併爲系統搭建一個靈活、可擴展的平臺。經過依賴注入機制,咱們只須要經過簡單的配置,而無需任何代碼就可指定目標須要的資源,完成自身的業務邏輯,而不須要關心具體的資源來自何處,由誰實現。函數

理解DI的關鍵是:「誰依賴誰,爲何須要依賴,誰注入誰,注入了什麼」,那咱們來深刻分析一下:ui

  • 誰依賴於誰:固然是應用程序依賴於IoC容器;
  • 爲何須要依賴:應用程序須要IoC容器來提供對象須要的外部資源;
  • 誰注入誰:很明顯是IoC容器注入應用程序某個對象,應用程序依賴的對象;
  • 注入了什麼:就是注入某個對象所須要的外部資源(包括對象、資源、常量數據)。

咱們來看看thinkphp6的獲取容器中的實例方法this

$arrayItem = app('org\utils\ArrayItem');

這裏實際上在底層調用了think\ContainerinvokeClass方法spa

public function invokeClass(string $class, array $vars = [])
{
    try {
        $reflect = new ReflectionClass($class);
    } catch (ReflectionException $e) {
        throw new ClassNotFoundException('class not exists: ' . $class, $class, $e);
    }

    if ($reflect->hasMethod('__make')) {
        $method = $reflect->getMethod('__make');
        if ($method->isPublic() && $method->isStatic()) {
            $args = $this->bindParams($method, $vars);
            return $method->invokeArgs(null, $args);
        }
    }

    $constructor = $reflect->getConstructor();

    $args = $constructor ? $this->bindParams($constructor, $vars) : [];

    $object = $reflect->newInstanceArgs($args);

    $this->invokeAfter($class, $object);

    return $object;
}

原來tp是用反射來實現依賴注入的,咱們看到這行,正是實例化了一個反射類。

$reflect = new ReflectionClass($class);

反射指在PHP運行狀態中,擴展分析PHP程序,導出或提取出關於類,方法,屬性,參數等詳細信息,包括註釋。這種動態獲取信息以及動態調用對象方法的功能稱爲反射API,反射類的方法有:

https://www.php.net/manual/zh/book.reflection.php

ReflectionClass::__construct — 初始化 ReflectionClass 類
ReflectionClass::export — 導出一個類
ReflectionClass::getConstant — 獲取定義過的一個常量
ReflectionClass::getConstants — 獲取一組常量
ReflectionClass::getConstructor — 獲取類的構造函數
ReflectionClass::getDefaultProperties — 獲取默認屬性
ReflectionClass::getDocComment — 獲取文檔註釋
ReflectionClass::getEndLine — 獲取最後一行的行數
ReflectionClass::getExtension — 根據已定義的類獲取所在擴展的 ReflectionExtension 對象
ReflectionClass::getExtensionName — 獲取定義的類所在的擴展的名稱
ReflectionClass::getFileName — 獲取定義類的文件名
ReflectionClass::getInterfaceNames — 獲取接口(interface)名稱
ReflectionClass::getInterfaces — 獲取接口
ReflectionClass::getMethod — 獲取一個類方法的 ReflectionMethod。
ReflectionClass::getMethods — 獲取方法的數組
ReflectionClass::getModifiers — 獲取類的修飾符
ReflectionClass::getName — 獲取類名
ReflectionClass::getNamespaceName — 獲取命名空間的名稱
ReflectionClass::getParentClass — 獲取父類
ReflectionClass::getProperties — 獲取一組屬性
ReflectionClass::getProperty — 獲取類的一個屬性的 ReflectionProperty
ReflectionClass::getReflectionConstant — Gets a ReflectionClassConstant for a class's constant
ReflectionClass::getReflectionConstants — Gets class constants
ReflectionClass::getShortName — 獲取短名
ReflectionClass::getStartLine — 獲取起始行號
ReflectionClass::getStaticProperties — 獲取靜態(static)屬性
ReflectionClass::getStaticPropertyValue — 獲取靜態(static)屬性的值
ReflectionClass::getTraitAliases — 返回 trait 別名的一個數組
ReflectionClass::getTraitNames — 返回這個類所使用 traits 的名稱的數組
ReflectionClass::getTraits — 返回這個類所使用的 traits 數組
ReflectionClass::hasConstant — 檢查常量是否已經定義
ReflectionClass::hasMethod — 檢查方法是否已定義
ReflectionClass::hasProperty — 檢查屬性是否已定義
ReflectionClass::implementsInterface — 接口的實現
ReflectionClass::inNamespace — 檢查是否位於命名空間中
ReflectionClass::isAbstract — 檢查類是不是抽象類(abstract)
ReflectionClass::isAnonymous — 檢查類是不是匿名類
ReflectionClass::isCloneable — 返回了一個類是否可複製
ReflectionClass::isFinal — 檢查類是否聲明爲 final
ReflectionClass::isInstance — 檢查類的實例
ReflectionClass::isInstantiable — 檢查類是否可實例化
ReflectionClass::isInterface — 檢查類是不是一個接口(interface)
ReflectionClass::isInternal — 檢查類是否由擴展或核心在內部定義
ReflectionClass::isIterable — Check whether this class is iterable
ReflectionClass::isIterateable — 檢查是否可迭代(iterateable)
ReflectionClass::isSubclassOf — 檢查是否爲一個子類
ReflectionClass::isTrait — 返回了是否爲一個 trait
ReflectionClass::isUserDefined — 檢查是否由用戶定義的
ReflectionClass::newInstance — 從指定的參數建立一個新的類實例
ReflectionClass::newInstanceArgs — 從給出的參數建立一個新的類實例。
ReflectionClass::newInstanceWithoutConstructor — 建立一個新的類實例而不調用它的構造函數
ReflectionClass::setStaticPropertyValue — 設置靜態屬性的值
ReflectionClass::__toString — 返回 ReflectionClass 對象字符串的表示形式。

知道反射類的基本方法了,咱們來實現一個不用new對應class就能夠實例化對象的方法:

function make($class)
{
    //創建這個類的反射類
    $reflect = new ReflectionClass($class);
    //拿到構造方法
    $constructor = $reflect->getConstructor();
    //沒有構造方法,直接返回實例
    if (is_null($constructor)) {
        return $reflect->newInstance();
    }
    //獲取構造方法的參數
    $parameters = $constructor->getParameters();
    //傳入參數實例化class
    return $reflect->newInstanceArgs($parameters);
}

$std = new stdClass();
var_dump($std);
$std2 = make('stdClass');
var_dump($std2);

上面的$std$std2 對象都是同樣的。可是咱們還沒實現依賴注入,接着再來改造一下。

...

class App
{
    protected $database;

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

    public function insertLog()
    {
        $this->database->insert();
    }
}

function make($class)
{
    $reflect = new ReflectionClass($class);
    $constructor = $reflect->getConstructor();
    if (is_null($constructor)) {
        return $reflect->newInstance();
    }

    //返回構造函數裏的依賴實例
    $instances = getParameters($constructor);
    return $reflect->newInstanceArgs($instances);
}

function getParameters($constructor)
{
    $parameters = $constructor->getParameters();
    $dependencies = [];
    foreach ($parameters as $parameter) {
        //調用make方法遞歸獲取實例
        //$parameter->getClass()->name  得到類型提示類名,好比 function Test(App $app) ,就是App
        $dependencies[] = make($parameter->getClass()->name);
    }
    return $dependencies;
}
$app = make('App');
$app->insertLog();

上面程序裏IoC容器自動實例化了Oracle類,並注入到App類裏,因此咱們不須要手動傳入,咱們能夠稱其爲依賴注入。

解耦合

上面的代碼仍是沒有徹底達到解偶。若是不少頁面的構造裏都注入了Mysql,可是有一天要把系統換成Oracle記錄日誌,那咱們豈不是所有頁面都要改爲Oracle?
並且以前課程 淺談thinkphp的服務容器 (https://www.gog5.cn/archives/6/)
咱們尚未實現IoC和DI。

因此會結合上一節 淺談thinkphp的服務容器 實現依賴注入。

interface Database
{
    public function insert();
}
class Mysql implements Database
{
    public function insert()
    {
        echo 'Mysql insert';
    }
}
class Oracle implements Database
{
    public function insert()
    {
        echo 'Oracle insert';
    }
}

class App
{
    protected $database;

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

    public function insertLog()
    {
        $this->database->insert();
    }
}

class Container
{
    protected $bind = [];

    /**
     * @param string $abstract 類標識、接口
     * @param string $concrete 要綁定的類
     */
    public function bind($abstract, $concrete)
    {
        $this->bind[$abstract] = $this->build($concrete);
    }

    public function make($abstract)
    {
        if (!isset($this->bind[$abstract])) {
            $this->bind($abstract, $abstract);
        }
        return $this->bind[$abstract];
    }

    //至關於以前的make方法
    public function build($abstract)
    {
        $reflect = new ReflectionClass($abstract);
        $constructor = $reflect->getConstructor();
        if (is_null($constructor)) {
            return $reflect->newInstance();
        }
        $instances = $this->getParameters($constructor);
        return $reflect->newInstanceArgs($instances);
    }

    protected function getParameters($constructor)
    {
        $parameters = $constructor->getParameters();
        $dependencies = [];
        foreach ($parameters as $parameter) {
            $dependencies[] = $this->make($parameter->getClass()->name);
        }
        return $dependencies;
    }
}

$container = new Container();
$container->bind('Database', 'Mysql');
$app = $container->make('App');
$app->insertLog();

咱們把 App 構造的類提示從Oracle類改爲 Database接口,這樣就可讓容器決定注入數據庫類了:

public function __construct(Database $database)

怎麼讓容器決定注入的類,bind方法裏,先是把Mysql類實例化,而後映射到Database接口。
make解析實例的時候,會把Database換成Mysql實例,注入到參數裏。

$container->bind('Database', 'Mysql');

之後須要更換數據庫時候,只要在綁定容器的地方,把映射類替換掉,全部頁面也都生效了。

$container->bind('Database', 'Oracle');

到這裏容器、依賴注入、控制反轉也就講完了。

相關文章
相關標籤/搜索