在開始之前要明確一個概念,不管是設計模式,還是依賴注入等等,都是為了實現模組化.所謂模組化就是希望一個軟體是由很多子模組組成的,這些模組之間的依賴程度儘量的低,也就是如果系統中不需要某一個功能,那麼只要移除這個功能所對應的模組就可以了.
那麼,我們今天要說的服務容器就是為了實現上面的功能.你應該聽過,Laravel中的服務容器其本質上是一個IoC容器,但是好像隊IoC又不是很瞭解,講來講去優點很多,功能很強勁.但是不懂原理怎麼用都不踏實啊.所以,這裡我們自己來實現一個IoC容器,洞察其本質.
在開始之前,先說明一點,閱讀本篇文章至少要保證有一下的基礎知識:
php反射用法
閉包的use用法
如果不懂上面的內容,請先補充.避免閱讀程式碼時候產生的不適感.
<?php
/**
* Created by PhpStorm.
* User: jiayao
* Date: 2016/9/1
* Time: 21:41
*/
class Container
{
public $binding = [];
/**
* @param $abstract
* @param null $concrete
* @param bool $shared
*/
public function bind($abstract, $concrete = null, $shared = false)
{
if (!$concrete instanceof Closure) {
$concrete = $this->getClosure($abstract, $concrete);
}
$this->binding[$abstract] = compact(`concrete`, `shared`);
}
protected function getClosure($abstract, $concrete)
{
return function ($c) use ($abstract, $concrete) {
$method = ($abstract == $concrete) ? `build` : `make`;
return $c->$method($concrete);
};
}
/**
* @param $abstract
* @return object
*
*/
public function make($abstract)
{
$concrete = $this->getConcrete($abstract);
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
return $object;
}
/**
* @param $concrete
* @param $abstract
* @return bool
*/
public function isBuildable($concrete, $abstract)
{
return $concrete === $abstract || $concrete instanceof Closure;
}
/**
* @param $abstract
* @return mixed
*/
protected function getConcrete($abstract)
{
if (!isset($this->binding[$abstract])) {
return $abstract;
}
return $this->binding[$abstract][`concrete`];
}
/**
* @param $concrete
* @return object
*/
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);
}
/**
* @param $paramters
* @return array
* 獲取構建類所需要的所有依賴,級建構函式所需要的引數 ,
*/
protected function getDependencies($paramters) {
$dependencies = [];
foreach ($paramters as $paramter) {
//獲取到引數名稱.
$dep = $paramter->getClass();
if(is_null($dep)){
$dependencies = null;
}else{
$dependencies[] = $this->resolveClass($paramter);
}
}
return (array)$dependencies;
}
/**
* @param ReflectionParameter $parameter
* @return object
* 例項化 建構函式中所需要的引數.
*/
protected function resolveClass(ReflectionParameter $parameter) {
$name = $parameter->getClass()->name;
return $this->make($name);
}
}
這就是一個IoC容器的實現程式碼.乍一看,很麻煩.其實真的蠻麻煩的 =_=,如果是第一次接觸的話,並不是那麼好消化,這裡再給出使用IoC容器的程式碼
<?php
/**
* Created by PhpStorm.
* User: jiayao
* Date: 2016/9/1
* Time: 21:37
*/
require __DIR__ . `/Container.php`;
interface TrafficTool
{
public function go();
}
class Train implements TrafficTool
{
public function go()
{
echo "train....";
}
}
class Leg implements TrafficTool
{
public function go()
{
echo "leg..";
}
}
class Traveller
{
/**
* @var Leg|null|Train
* 旅行工具
*/
protected $_trafficTool;
public function __construct(TrafficTool$trafficTool)
{
$this->_trafficTool = $trafficTool;
}
public function visitTibet()
{
$this->_trafficTool->go();
}
}
//例項化IoC容器
$app = new Container();
//繫結某一功能到IoC
$app->bind(`TrafficTool`, `Train`);
$app->bind(`travellerA`, `Traveller`);
// 例項化物件
$tra = $app->make(`travellerA`);
$tra->visitTibet();
執行例子發現會輸出:train..
.這個例子假設旅行者去青藏旅行,可以坐火車(train)或者走路(leg)去青藏.
好了,其實這樣子本篇文章就可以結束了,因為所有的答案都在IoC容器的實現中, 但是為了可以更好的理解上面的程式碼,我們繼續往下分析.
首先,希望你可以執行一下上面的程式碼,雖然簡單的執行程式碼並不會幫助你理解程式碼,但是一個可以執行的例子會讓人比較踏實,能夠更有把握的理解程式碼.
在深入每一行程式碼之前,我們從整體上來分析,IoC解決了一個什麼問題?簡單點說,就是我們再例項化物件的時候不用使用new了,有了IoC容器之後,我們呼叫make函式就可以例項化出一個物件了.然而,你發現,Traveller的建構函式是需要一個引數的,可是我們好像並沒有提供這個引數?
這就是IoC強大之處了, 呼叫make例項化物件的時候,容器會使用反射功能,去分析我們要例項化物件的建構函式,獲取建構函式所需的每個引數,然後分別去例項化這些引數,如果例項化這些引數也要引數,那麼就再去例項化引數的引數…..=_=.到最後成功例項化我們所需要的traveller了.在Container的build函式就是使用反射來例項化物件.
但是,有一個問題了,IoC容器怎麼知道例項化Traveller的時候需要的引數train,而不是leg?
其實,IoC容器什麼都不知道,IoC會例項化哪些物件都是通過bind
函式告訴IoC的,上面的例子兩次呼叫bind函式,就是告訴Ioc可以例項化的物件有Train
和Traveller
. 再通俗講就是:當需要當我們需要TrafficTool
這個服務的時候去例項化Train
這個類,需要一個travellerA
的旅行者的時候去例項化Traveller
類.而Train
這個就是travellerA
就是去青藏的方式. 這樣子如果想要走路去青藏的話只要把$app->bind(`Visit`, `Train`);
改為$app->bind(`Visit`, `Leg`);
就可以.
可是,這上面的這些有什麼意義?直接$tra = new Traveller($trafficTool)
來例項化物件好像也沒有什麼不好的.
使用new來例項化物件的時候,會產生依賴.比如上面
$tra = new Traveller($trafficTool)
,這說明我們要建立一個Traveller之前得有一個$trafficTool
,即Traveller
依賴於trafficTool
.當使用new來例項化Traveller
的時候,Traveller
和trafficTool
之間就產生了耦合.這樣,這兩個元件就沒辦法分開了.
而使用IoC是怎麼解決這個問題的,之前說過,如果想要如果想要走路去青藏的話只要把$app->bind(`Visit`, `Train`);
改為$app->bind(`Visit`, `Leg`);
就可以.這樣子,使用何種方式去青藏,我們可以自由的選擇.
我們站在Laravel框架設計者的角度去想,設計者肯定希望一個框架提供的功能越多越好,但是又要保證強大的同時又不會限制使用者.最好可以保證使用者想實現什麼奇怪的需求都可以.那麼功能強大但是又不侷限的最好方法就是什麼都不做,提供一個強大的IoC容器.所有需要實現的功能都變成一個個服務,需要什麼服務就把服務註冊(即呼叫bind函式)到IoC中,然後讓IoC去管理依賴.
開發者想到一個變態的需求:走路去青藏,那麼只要你實現了走路去青藏這個功能,然後把這個功能當做一個服務註冊到IoC中,以後你需要這個服務的時候IoC就幫你例項化這個服務.當開發者回歸正常之後覺得還是坐火車去吧,於是不註冊走路這個功能,實現坐火車的功能,然後註冊這個功能.下次IoC例項化的時候就是例項化坐火車這個功能了.
好了,剩下的部分就是一行一行的閱讀Container的程式碼了,Laravel框架中的服務容器程式碼也是這個樣子,只是功能更加強悍.但是核心是一樣的,上面的程式碼懂了以後再使用Laravel框架就會更加遊刃有餘了.
文章雖短.但是內容很多.尤其是程式碼,雖然可能只是短短的一個例子,但是包含了很多內容.值得好好分析,這裡放個彩蛋:Traveller中建構函式引數類似為TrafficTool,是一個介面.但是例項化的是Train.這裡體現了設計模式的一個原則
面對介面程式設計,而不是面對實現程式設計.