如何理解 Laravel 的 IoC 容器

jwan046發表於2017-03-09

學習laravel快小一年了,到現在才去研究laravel 的核心 '容器 IOC' 這些概念. 寫專案的時候有大概看看關於IOC 文章, 但是沒有深入理解,只是有個概念,趕著寫程式碼, 雖然程式碼也寫的很菜 · - ·

這幾天花了點時間研究了一下laravel 的核心 ‘服務容器’ 然後理解了一下關於IOC 的概念. 不敢說百分百掌握了,但是比之前是有一定加深. 所以決定把自己理解的分享一下, 把自己的第一次博文獻給laravel. 理解不到位的還請各位大牛多多指正.

IOC( inversion of controller )叫做控制反轉模式,也可以稱為(dependency injection ) 依賴注入模式。要理解依賴注入的概念我們先理解下什麼依賴

//支付寶支付
class Alipay {
      public function __construct(){}

      public function pay()
      {
          echo 'pay bill by alipay';
      }
}
//微信支付
class Wechatpay {
      public function __construct(){}

      public function pay()
      {
          echo 'pay bill by wechatpay';
      }
}
//銀聯支付
class Unionpay{
      public function __construct(){}

      public function pay()
      {
          echo 'pay bill by unionpay';
      }
}

//支付賬單
class PayBill {

      private $payMethod;

      public function __construct( )
      {
          $this->payMethod= new Alipay ();
      }

      public function  payMyBill()
      {
           $this->payMethod->pay();
      }
}

$pb = new PayBill ();
$pb->payMyBill();

透過上面的程式碼我們知道,當我們建立一個class PayBill 的例項的時候, PayBill 的建構函式里面有{ $this->payMethod= new Alipay (); }, 也就是例項化了一個class Alipay . 這個時候依賴就產生了, 這裡可以理解為當我想用支付寶支付的時候, 那我首先要獲取到一個支付寶的例項,或者理解為獲取支付寶的功能支援. 當用我們完 new 關鍵字的時候, 依賴其實已經解決了,因為我們獲取了Alipay 的例項.

其實在我知道ioc概念之前,我的程式碼中大部分都是這種模式 ~ _ ~ . 這種有什麼問題呢, 簡單來說, 比如當我想用的不是支付寶而是微信的時候怎麼辦, 你能做的就是修改Payment 的建構函式的程式碼,例項化一個微信支付Wechatpay.

如果我們的程式不是很大的時候可能還感覺不出什麼,但是當你的程式碼非常複雜,龐大的時候,如果我們的需求經常改變,那麼修改程式碼就變的非常麻煩了。所以ioc 的思想就是不要在 class Payment 裡面用new 的方式去例項化解決依賴, 而且轉為由外部來負責,簡單一點就是內部沒有new 的這個步驟,透過依賴注入的方式同樣的能獲取到支付的例項.

依賴我們知道了是什麼意思,那依賴注入又是什麼意思呢,我們把上面的程式碼擴充一下


//支付類介面
interface Pay
{
    public function pay();
}

//支付寶支付
class Alipay implements Pay {
      public function __construct(){}

      public function pay()
      {
          echo 'pay bill by alipay';
      }
}
//微信支付
class Wechatpay implements Pay  {
      public function __construct(){}

      public function pay()
      {
          echo 'pay bill by wechatpay';
      }
}
//銀聯支付
class Unionpay implements Pay  {
      public function __construct(){}

      public function pay()
      {
          echo 'pay bill by unionpay';
      }
}

//付款
class PayBill {

      private $payMethod;

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

      public function  payMyBill()
      {
           $this->payMethod->pay();
      }
}

//生成依賴
$payMethod =  new Alipay();
//注入依賴
$pb = new PayBill( $payMethod );
$pb->payMyBill();

上面的程式碼中,跟之前的比較的話,我們加入一個Pay 介面, 然後所有的支付方式都繼承了這個介面並且實現了pay 這個功能. 可能大家會問為什麼要用介面,這個我們稍後會講到.

當我們例項化PayBill的之前, 我們首先是例項化了一個Alipay,這個步驟就是生成了依賴了,然後我們需要把這個依賴注入到PayBill 的例項當中,透過程式碼我們可以看到 { $pb = new PayBill( payMethod ); }, 我們是透過了建構函式把這個依賴注入了PayBill 裡面. 這樣一來 $pb 這個PayBill 的例項就有了支付寶支付的能力了.

把class Alipay 的例項透過constructor注入的方式去例項化一個 class PayBill. 在這裡我們的注入是手動注入, 不是自動的. 而Laravel 框架實現則是自動注入.

在介紹IOC 的容器之前我們先來理解下反射的概念(reflection),因為IOC 容器也是要透過反射來實現的.從網上抄了一段來解釋反射是什麼意思

"反射它指在PHP執行狀態中,擴充套件分析PHP程式,匯出或提取出關於類、方法、屬性、引數等的詳細資訊,包括註釋。這種動態獲取的資訊以及動態呼叫物件的方法的功能稱為反射API。反射是操縱物件導向範型中元模型的API,其功能十分強大,可幫助我們構建複雜,可擴充套件的應用。其用途如:自動載入外掛,自動生成文件,甚至可用來擴充PHP語言"

舉個簡單的例子


class B{

}

class A {

    public function __construct(B $args)
    {
    }

    public function dosomething()
    {
        echo 'Hello world';
    }
}

//建立class A 的反射
$reflection = new ReflectionClass('A');

$b = new B();

//獲取class A 的例項
$instance = $reflection ->newInstanceArgs( [ $b ]);

$instance->dosomething(); //輸出 ‘Hellow World’

$constructor = $reflection->getConstructor();//獲取class A 的建構函式

$dependencies = $constructor->getParameters();//獲取class A 的依賴類

dump($constructor);

dump($dependencies);

dump 的得到的$constructor 和 $dependencies 結果如下

//constructor
ReflectionMethod {#351 ▼
        +name: "__construct" 
        +class: "A" 
        parameters: array:1 [▶] 
        extra: array:3 [▶] 
        modifiers: "public"
}

//$dependencies
array:1 [▼
        0 => ReflectionParameter {#352 ▼
         +name: "args"
          position: 0
          typeHint: "B"
      }
]

透過上面的程式碼我們可以獲取到 class A 的建構函式,還有建構函式依賴的類,這個地方我們依賴一個名字為 'args' 的量,而且透過TypeHint可以知道他是型別為 Class B; 反射機制可以讓我去解析一個類,能過獲取一個類裡面的屬性,方法 ,建構函式, 建構函式需要的引數。 有個了這個才能實現Laravel 的IOC 容器.

接下來介紹一下Laravel 的IOC服務容器概念. 在laravel框架中, 服務容器是整個laravel的核心,它提供了整個系統功能及服務的配置, 呼叫. 容器按照字面上的理解就是裝東西的東西,比如冰箱, 當我們需要冰箱裡面的東西的時候直接從裡面拿就行了. 服務容器也可以這樣理解, 當程式開始執行的時候,我們把我們需要的一些服務放到或者註冊到(bind)到容器裡面,當我需要的時候直接取出來(make)就行了. 上面提到的 bind 和 make 就是註冊 和 取出的 兩個動作.

好了,說了這麼多,下面要上一段容器的程式碼了. 下面這段程式碼不是laravel 的原始碼, 而是來自一本書《laravel 框架關鍵技術解析》. 這段程式碼很好的還原了laravel 的服務容器的核心思想. 程式碼有點長, 小夥伴們要耐心看. 當然小夥伴完全可以試著執行一下這段程式碼,然後除錯一下,這樣會更有助於理解.

<?php 

//容器類裝例項或提供例項的回撥函式
class Container {

    //用於裝提供例項的回撥函式,真正的容器還會裝例項等其他內容
    //從而實現單例等高階功能
    protected $bindings = [];

    //繫結介面和生成相應例項的回撥函式
    public function bind($abstract, $concrete=null, $shared=false) {

        //如果提供的引數不是回撥函式,則產生預設的回撥函式
        if(!$concrete instanceof Closure) {
            $concrete = $this->getClosure($abstract, $concrete);
        }

        $this->bindings[$abstract] = compact('concrete', 'shared');
    }

    //預設生成例項的回撥函式
    protected function getClosure($abstract, $concrete) {

        return function($c) use ($abstract, $concrete) {
            $method = ($abstract == $concrete) ? 'build' : 'make';
            return $c->$method($concrete);
        };

    }

    public function make($abstract) {
        $concrete = $this->getConcrete($abstract);

        if($this->isBuildable($concrete, $abstract)) {
            $object = $this->build($concrete);
        } else {
            $object = $this->make($concrete);
        }

        return $object;
    }

    protected function isBuildable($concrete, $abstract) {
        return $concrete === $abstract || $concrete instanceof Closure;
    }

    //獲取繫結的回撥函式
    protected function getConcrete($abstract) {
        if(!isset($this->bindings[$abstract])) {
            return $abstract;
        }

        return $this->bindings[$abstract]['concrete'];
    }

    //例項化物件
    public function build($concrete) {

        if($concrete instanceof Closure) {
            return $concrete($this);
        }

        $reflector = new ReflectionClass($concrete);
        if(!$reflector->isInstantiable()) {
            echo $message = "Target [$concrete] is not instantiable";
        }

        $constructor = $reflector->getConstructor();
        if(is_null($constructor)) {
            return new $concrete;
        }

        $dependencies = $constructor->getParameters();
        $instances = $this->getDependencies($dependencies);

        return $reflector->newInstanceArgs($instances);
    }

    //解決透過反射機制例項化物件時的依賴
    protected function getDependencies($parameters) {
        $dependencies = [];
        foreach($parameters as $parameter) {
            $dependency = $parameter->getClass();
            if(is_null($dependency)) {
                $dependencies[] = NULL;
            } else {
                $dependencies[] = $this->resolveClass($parameter);
            }
        }

        return (array)$dependencies;
    }

    protected function resolveClass(ReflectionParameter $parameter) {
        return $this->make($parameter->getClass()->name);
    }

}

上面的程式碼就生成了一個容器,下面是如何使用容器

$app = new Container();
$app->bind("Pay", "Alipay");//Pay 為介面, Alipay 是 class Alipay
$app->bind("tryToPayMyBill", "PayBill"); //tryToPayMyBill可以當做是Class PayBill 的服務別名

//透過字元解析,或得到了Class PayBill 的例項
$paybill = $app->make("tryToPayMyBill"); 

//因為之前已經把Pay 介面繫結為了 Alipay,所以呼叫pay 方法的話會顯示 'pay bill by alipay '
$paybill->payMyBill(); 

當我們例項化一個Container得到 $app 後, 我們就可以向其中填充東西了

$app->bind("Pay", "Alipay");
$app->bind("tryToPayMyBill", "PayBill"); 

當執行完這兩行繫結碼後, $app 裡面的屬性 $bindings 就已經有了array 值,是啥樣的呢,我們來看下

array:2 [▼
  "App\Http\Controllers\Pay" => array:2 [▼
      "concrete" => Closure {#355 ▼
        class: "App\Http\Controllers\Container" 
        this:Container{[#354](http://127.0.0.4/ioc#sf-dump-254248394-ref2354) …} 
        parameters: array:1 [▼
          "$c" => []
        ] 
        use: array:2 [▼
          "$abstract" => "App\Http\Controllers\Pay"
         "$concrete" => "App\Http\Controllers\Alipay"
        ] 
        file: "C:\project\test\app\Http\Controllers\IOCController.php" line:       "119 to 122"
    } 
    "shared" => false 
  ]

 "tryToPayMyBill" => array:2 [▼
      "concrete" => Closure {#359 ▼
          class: "App\Http\Controllers\Container" 
          this:Container{[#354](http://127.0.0.4/ioc#sf-dump-254248394-ref2354) …} 
          parameters: array:1 [▼
                "$c" => []
          ] 
          use: array:2 [▼
                "$abstract" => "tryToPayMyBill" 
                "$concrete" => "\App\Http\Controllers\PayBill"
          ] 
          file: "C:\project\test\app\Http\Controllers\IOCController.php" line: "119 to 122"
    } 
      "shared" => false 
  ]
]

當執行 $paybill = $app->make("tryToPayMyBill"); 的時候, 程式就會用make方法透過閉包函式的回撥開始解析了.

解析'tryToPayBill' 這個字串, 程式透過閉包函式 和build方法會得到 'PayBill' 這個字串,該字串儲存在$concrete 上. 這個是第一步. 然後程式還會以類似於遞迴方式 將$concrete 傳入 build() 方法. 這個時候build裡面就獲取了$concrete = 'PayBill'. 這個時候反射就派上了用場, 大家有沒有發現,PayBill 不就是 class PayBill 嗎? 然後在透過反射的方法ReflectionClass('PayBill') 獲取PayBill 的例項. 之後透過getConstructor(),和getParameters() 等方法知道了 Class PayBill 和 介面Pay 存在依賴

//$constructor = $reflector->getConstructor();
ReflectionMethod {#374 ▼
    +name: "__construct" 
    +class: "App\Http\Controllers\PayBill" 
    parameters: array:1 [▼
          "$payMethod" => ReflectionParameter {#371 ▼
              +name: "payMethod" 
              position: 0 typeHint: "App\Http\Controllers\Pay"
          }
    ]
     extra: array:3 [▼
          "file" => "C:\project\test\app\Http\Controllers\IOCController.php"
          "line" => "83 to 86" 
          "isUserDefined" => true 
      ] 
    modifiers: "public"
}

//$dependencies = $constructor->getParameters();
array:1 [▼
    0 => ReflectionParameter {#370 ▼
        +name: "payMethod" 
        position: 0 
        typeHint: "App\Http\Controllers\Pay"
        }
]

接著,我們知道了有'Pay'這個依賴之後呢,我們要做的就是解決這個依賴,透過 getDependencies($parameters), 和 resolveClass(ReflectionParameter $parameter) ,還有之前的繫結$app->bind("Pay", "Alipay"); 在build 一次的時候,透過 return new $concrete;到這裡我們得到了這個Alipay 的例項

        if(is_null($constructor)) {
            return new $concrete;
        }

到這裡我們總算結局了這個依賴, 這個依賴的結果就是例項化了一個 Alipay. 到這裡還沒結束

        $instances = $this->getDependencies($dependencies);

上面的$instances 陣列只有一個element 那就是 Alipay 例項

  array:1 [▼0 =>Alipay
      {#380}
 ]

最終透過 newInstanceArgs() 方法, 我們獲取到了 PayBill 的例項。

 return $reflector->newInstanceArgs($instances);

到這裡整個流程就結束了, 我們透過 bind 方式繫結了一些依賴關係, 然後透過make 方法 獲取到到我們想要的例項. 在make中有牽扯到了閉包函式,反射等概念.

好了,當我們把容器的概念理解了之後,我們就可以理解下為什麼要用介面這個問題了. 如果說我不想用支付寶支付,我要用微信支付怎麼辦,too easy.

$app->bind("Pay", "Wechatpay");
$app->bind("tryToPayMyBill", "PayBill");
$paybill = $app->make("tryToPayMyBill"); 
$paybill->payMyBill();

是不是很簡單呢, 只要把繫結從'Alipay' 改成 'Wechatpay' 就行了,其他的都不用改. 這就是為什麼我們要用介面. 只要你的支付方式繼承了pay 這個介面,並且實現pay 這個方法,我們就能夠透過繫結正常的使用. 這樣我們的程式就非常容易被擴充,因為以後可能會出現成百上千種的支付方式.

好了,到這裡不知道小夥伴有沒有理解呢,我建議大家可以試著執行下這些程式碼, 這樣理解起來會更快.同時推薦大家去看看 《laravel 框架關鍵技術解析》這本書,寫的還是不錯的.


轉載請註明:作者[[哎喲我的巴扎黑]

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

相關文章