跟DI容器類似,引入Service Locator目的也在于解耦。有許多成熟的設計模式也可用于解耦,但在Web應用上, Service Locator絕對占有一席之地。 對于Web開發而言,Service Locator天然地適合使用, 主要就是因為Service Locator模式非常貼合Web這種基于服務和組件的應用的運作特點。 這一模式的優點有:
* Service Locator充當了一個運行時的鏈接器的角色,可以在運行時動態地修改一個類所要選用的服務, 而不必對類作任何的修改。
* 一個類可以在運行時,有針對性地增減、替換所要用到的服務,從而得到一定程度的優化。
* 實現服務提供方、服務使用方完全的解耦,便于獨立測試和代碼跨框架復用。
## Service Locator的基本功能[](http://www.digpage.com/service_locator.html#id2 "Permalink to this headline")
在Yii中Service Locator由?yii\di\ServiceLocator?來實現。 從代碼組織上,Yii將Service Locator放到與DI同一層次來對待,都組織在?yii\di?命名空間下。 下面是Service Locator的源代碼:
~~~
class ServiceLocator extends Component
{
// 用于緩存服務、組件等的實例
private $_components = [];
// 用于保存服務和組件的定義,通常為配置數組,可以用來創建具體的實例
private $_definitions = [];
// 重載了 getter 方法,使得訪問服務和組件就跟訪問類的屬性一樣。
// 同時,也保留了原來Component的 getter所具有的功能。
// 請留意,ServiceLocator 并未重載 __set(),
// 仍然使用 yii\base\Component::__set()
public function __get($name)
{
... ...
}
// 對比Component,增加了對是否具有某個服務和組件的判斷。
public function __isset($name)
{
... ...
}
// 當 $checkInstance === false 時,用于判斷是否已經定義了某個服務或組件
// 當 $checkInstance === true 時,用于判斷是否已經有了某人服務或組件的實例
public function has($id, $checkInstance = false)
{
return $checkInstance ? isset($this->_components[$id]) :
isset($this->_definitions[$id]);
}
// 根據 $id 獲取對應的服務或組件的實例
public function get($id, $throwException = true)
{
... ...
}
// 用于注冊一個組件或服務,其中 $id 用于標識服務或組件。
// $definition 可以是一個類名,一個配置數組,一個PHP callable,或者一個對象
public function set($id, $definition)
{
... ...
}
// 刪除一個服務或組件
public function clear($id)
{
unset($this->_definitions[$id], $this->_components[$id]);
}
// 用于返回Service Locator的 $_components 數組或 $_definitions 數組,
// 同時也是 components 屬性的getter函數
public function getComponents($returnDefinitions = true)
{
... ...
}
// 批量方式注冊服務或組件,同時也是 components 屬性的setter函數
public function setComponents($components)
{
... ...
}
}
~~~
從代碼可以看出,Service Locator繼承自?yii\base\Component?,這是Yii中的一個基礎類, 提供了屬性、事件、行為等基本功能,關于Component的有關知識,可以看看?[_屬性(Property)_](http://www.digpage.com/property.html#property)?、?[_事件(Event)_](http://www.digpage.com/event.html#event)?和?[_行為(Behavior)_](http://www.digpage.com/behavior.html#behavior)?。
Service Locator 通過?__get()?__isset()?has()?等方法, 擴展了?yii\base\Component?的最基本功能,提供了對于服務和組件的屬性化支持。
從功能來看,Service Locator提供了注冊服務和組件的?set()?setComponents()?等方法, 用于刪除的clear()?。用于讀取的?get()?和?getComponents()?等方法。
細心的讀者可能一看到?setComponents()?和?getComponents()?就猜到了, Service Locator還具有一個可讀寫的 components 屬性。
### Service Locator的數據結構[](http://www.digpage.com/service_locator.html#id3 "Permalink to this headline")
從上面的代碼中,可以看到Service Locator維護了兩個數組,?$_components?和?$_definitions?。這兩個數組均是以服務或組件的ID為鍵的數組。
其中,?$_components?用于緩存存Service Locator中的組件或服務的實例。 Service Locator 為其提供了getter和setter。使其成為一個可讀寫的屬性。?$_definitions?用于保存這些組件或服務的定義。這個定義可以是:
* 配置數組。在向Service Locator索要服務或組件時,這個數組會被用于創建服務或組件的實例。 與DI容器的要求類似,當定義是配置數組時,要求配置數組必須要有?class?元素,表示要創建的是什么類。不然你讓Yii調用哪個構造函數?
* PHP callable。每當向Service Locator索要實例時,這個PHP callable都會被調用,其返回值,就是所要的對象。 對于這個PHP callable有一定的形式要求,一是它要返回一個服務或組件的實例。 二是它不接受任何的參數。 至于具體原因,后面會講到。
* 對象。這個更直接,每當你索要某個特定實例時,直接把這個對象給你就是了。
* 類名。即,使得?is_callable($definition,?true)?為真的定義。
從?yii\di\ServiceLocator::set()?的代碼:
~~~
public function set($id, $definition)
{
// 當定義為 null 時,表示要從Service Locator中刪除一個服務或組件
if ($definition === null) {
unset($this->_components[$id], $this->_definitions[$id]);
return;
}
// 確保服務或組件ID的唯一性
unset($this->_components[$id]);
// 定義如果是個對象或PHP callable,或類名,直接作為定義保存
// 留意這里 is_callable的第二個參數為true,所以,類名也可以。
if (is_object($definition) || is_callable($definition, true)) {
// 定義的過程,只是寫入了 $_definitions 數組
$this->_definitions[$id] = $definition;
// 定義如果是個數組,要確保數組中具有 class 元素
} elseif (is_array($definition)) {
if (isset($definition['class'])) {
// 定義的過程,只是寫入了 $_definitions 數組
$this->_definitions[$id] = $definition;
} else {
throw new InvalidConfigException(
"The configuration for the \"$id\" component must contain a \"class\" element.");
}
// 這也不是,那也不是,那么就拋出異常吧
} else {
throw new InvalidConfigException(
"Unexpected configuration type for the \"$id\" component: "
. gettype($definition));
}
}
~~~
服務或組件的ID在Service Locator中是唯一的,用于區別彼此。在任何情況下,Service Locator中同一ID只有一個實例、一個定義。也就是說,Service Locator中,所有的服務和組件,只保存一個單例。 這也是正常的邏輯,既然稱為服務定位器,你只要給定一個ID,它必然返回一個確定的實例。這一點跟DI容器是一樣的。
Service Locator 中ID僅起標識作用,可以是任意字符串,但通常用服務或組件名稱來表示。 如,以db?來表示數據庫連接,以?cache?來表示緩存組件等。
至于批量注冊的?yii\di\ServiceLocator::setCompoents()?只不過是簡單地遍歷數組,循環調用?set()而已。 就算我不把代碼貼出來,像你這么聰明的,一下子就可以自己寫出來了。
向Service Locator注冊服務或組件,其實就是向?$_definitions?數組寫入信息而已。
### 訪問Service Locator中的服務[](http://www.digpage.com/service_locator.html#id4 "Permalink to this headline")
Service Locator重載了?__get()?使得可以像訪問類的屬性一樣訪問已經實例化好的服務和組件。 下面是重載的?__get()?方法:
~~~
public function __get($name)
{
// has() 方法就是判斷 $_definitions 數組中是否已經保存了服務或組件的定義
// 請留意,這個時候服務或組件僅是完成定義,不一定已經實例化
if ($this->has($name)) {
// get() 方法用于返回服務或組件的實例
return $this->get($name);
// 未定義的服務或組件,那么視為正常的屬性、行為,
// 調用 yii\base\Component::__get()
} else {
return parent::__get($name);
}
}
~~~
在注冊好了服務或組件定義之后,就可以像訪問屬性一樣訪問這些服務(組件)。 前提是已經完成注冊,不要求已經實例化。 訪問這些服務或屬性,被轉換成了調用?yii\di\ServiceLocator::get()?來獲取實例。 下面是使用這種形式訪問服務或組件的例子:
~~~
// 創建一個Service Locator
$serviceLocator = new yii\di\ServiceLocator;
// 注冊一個 cache 服務
$serviceLocator->set('cache', [
'class' => 'yii\cache\MemCache',
'servers' => [
... ...
],
]);
// 使用訪問屬性的方法訪問這個 cache 服務
$serviceLocator->cache->flushValues();
// 上面的方法等效于下面這個
$serviceLocator->get('cache')->flushValues();
~~~
在Service Locator中,并未重載?__set()?。所以,Service Locator中的服務和組件看起來就好像只讀屬性一樣。 要向Service Locator中“寫”入服務和組件,沒有 setter 可以使用,需要調用yii\di\ServiceLocator::set()?對服務和組件進行注冊。
## 通過Service Locator獲取實例[](http://www.digpage.com/service_locator.html#id5 "Permalink to this headline")
與注冊服務和組件的簡單之極相反,Service Locator在創建獲取服務或組件實例的過程要稍微復雜一點。 這一點和DI容器也是很像的。 Service Locator通過?yii\di\ServiceLocator::get()?來創建、獲取服務或組件的實例:
~~~
public function get($id, $throwException = true)
{
// 如果已經有實例化好的組件或服務,直接使用緩存中的就OK了
if (isset($this->_components[$id])) {
return $this->_components[$id];
}
// 如果還沒有實例化好,那么再看看是不是已經定義好
if (isset($this->_definitions[$id])) {
$definition = $this->_definitions[$id];
// 如果定義是個對象,且不是Closure對象,那么直接將這個對象返回
if (is_object($definition) && !$definition instanceof Closure) {
// 實例化后,保存進 $_components 數組中,以后就可以直接引用了
return $this->_components[$id] = $definition;
// 是個數組或者PHP callable,調用 Yii::createObject()來創建一個實例
} else {
// 實例化后,保存進 $_components 數組中,以后就可以直接引用了
return $this->_components[$id] = Yii::createObject($definition);
}
} elseif ($throwException) {
throw new InvalidConfigException("Unknown component ID: $id");
// 即沒實例化,也沒定義,萬能的Yii也沒辦法通過一個任意的ID,
// 就給你找到想要的組件或服務呀,給你個 null 吧。
// 表示Service Locator中沒有這個ID的服務或組件。
} else {
return null;
}
}
~~~
Service Locator創建獲取服務或組件實例的過程是:
* 看看緩存數組?$_components?中有沒有已經創建好的實例。有的話,皆大歡喜,直接用緩存中的就可以了。
* 緩存中沒有的話,那就要從定義開始創建了。
* 如果服務或組件的定義是個對象,那么直接把這個對象作為服務或組件的實例返回就可以了。 但有一點要注意,當使用一個PHP callable定義一個服務或組件時,這個定義是一個Closure類的對象。 這種定義雖然也對象,但是可不能把這種對象直接當成服務或組件的實例返回。
* 如果定義是一個數組或者一個PHP callable,那么把這個定義作為參數,調用Yii::createObject()?來創建實例。
這個?Yii::createObject()?在講配置時我們介紹過,當時只是點一點,這里會講得更深一點。但別急,先放一放, 知道他能為Service Locator創建對象就OK了。我們等下還會講這個方法的。
## 在Yii應用中使用Service Locator和DI容器[](http://www.digpage.com/service_locator.html#yiiservice-locatordi "Permalink to this headline")
我們在講DI容器時,提到了Yii中是把Service Locator和DI容器結合起來用的,Service Locator是建立在DI容器之上的。 那么一個Yii應用,是如何使用Service Locator和DI容器的呢?
### DI容器的引入[](http://www.digpage.com/service_locator.html#di "Permalink to this headline")
我們知道,每個Yii應用都有一個入口腳本?index.php?。在其中,有一行不怎么顯眼:
~~~
require(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php');
~~~
這一行看著普通,也就是引入一個 Yii.php 的文件。但是,讓我們來看看這個 Yii.php
~~~
<?php
require(__DIR__ . '/BaseYii.php');
class Yii extends \yii\BaseYii
{
}
spl_autoload_register(['Yii', 'autoload'], true, true);
Yii::$classMap = include(__DIR__ . '/classes.php');
// 重點看這里。創建一個DI 容器,并由 Yii::$container 引用
Yii::$container = new yii\di\Container;
~~~
Yii?是一個工具類,繼承自?yii\BaseYii?。 但這里對父類的代碼沒有任何重載,意味之父類和子類在功能上其實是相同的。 但是,Yii提供了讓你修改默認功能的機會。 就是自己寫一個?Yii?類,來擴展、重載Yii默認的、由?yii\BaseYii?提供的特性和功能。 盡管實際使用中,我們還從來沒有需要改寫過這個類,主要是因為沒有必要在這里寫代碼,可以通過別的方式實現。 但Yii確實提供了這么一個可能。這個在實踐中不常用,有這么個印象就足夠了。
這里重點看最后一句代碼,創建了一個DI容器,并由?Yii::$container?引用。 也就是說,?Yii?類維護了一個DI容器,這是DI容器開始介入整個應用的標志。 同時,這也意味著,在Yii應用中,我們可以隨時使用?Yii::$container?來訪問DI容器。 一般情況下,如無必須的理由,不要自己創建DI容器,使用?Yii::$container?完全足夠。
### Application的本質[](http://www.digpage.com/service_locator.html#application "Permalink to this headline")
再看看入口腳本?index.php?的最后兩行:
~~~
$application = new yii\web\Application($config);
$application->run();
~~~
創建了一個?yii\web\Application?實例,并調用其?run()?方法。 那么,這個?yii\web\Application?是何方神圣? 首先,?yii\web\Application?繼承自?yii\base\Application?,這從?yii\web\Application?的代碼可以看出來
~~~
class Application extends \yii\base\Application
{
... ...
}
~~~
而?yii\base\Application?又繼承自?yii\base\Module?,說明所有的Application都是Module
~~~
abstract class Application extends Module
{
... ...
}
~~~
那么?yii\base\Module?又繼承自哪個類呢?不知道你猜到沒,他繼承自?yii\di\ServiceLocator
~~~
class Module extends ServiceLocator
{
... ...
}
~~~
所有的Module都是服務定位器Service Locator,因此,所有的Application也都是Service Locator。
同時,在Application的構造函數中,?yii\base\Application::__construct()
~~~
public function __construct($config = [])
{
Yii::$app = $this;
... ...
}
~~~
第一行代碼就把Application當前的實例,賦值給?Yii::$app?了。 這意味著Yii應用創建之后,可以隨時通過?Yii::$app?來訪問應用自身,也就是訪問Service Locator。
至此,DI容器有了,Service Locator也出現了。那么Yii是如何擺布這兩者的呢?這兩者又是如何千里姻緣一線牽的呢?
### 實例創建方法[](http://www.digpage.com/service_locator.html#id6 "Permalink to this headline")
Service Locator和DI容器的親密關系就隱藏在?yii\di\ServiceLocator::get()?獲取實例時, 調用的Yii::createObject()?中。 前面我們說到這個?Yii?繼承自?yii\BaseYii?,因此這個函數實際上是BaseYii::createObject()?, 其代碼如下:
~~~
// static::$container就是上面說的引用了DI容器的靜態變量
public static function createObject($type, array $params = [])
{
// 字符串,代表一個類名、接口名、別名。
if (is_string($type)) {
return static::$container->get($type, $params);
// 是個數組,代表配置數組,必須含有 class 元素。
} elseif (is_array($type) && isset($type['class'])) {
$class = $type['class'];
unset($type['class']);
// 調用DI容器的get() 來獲取、創建實例
return static::$container->get($class, $params, $type);
// 是個PHP callable則調用其返回一個具體實例。
} elseif (is_callable($type, true)) {
// 是個PHP callable,那就調用它,并將其返回值作為服務或組件的實例返回
return call_user_func($type, $params);
// 是個數組但沒有 class 元素,拋出異常
} elseif (is_array($type)) {
throw new InvalidConfigException(
'Object configuration must be an array containing a "class" element.');
// 其他情況,拋出異常
} else {
throw new InvalidConfigException(
"Unsupported configuration type: " . gettype($type));
}
}
~~~
這個?createObject()?提供了一個向DI容器獲取實例的接口, 對于不同的定義,除了PHP callable外,createObject()?都是調用了DI容器的?yii\di\Container::get()?, 來獲取實例的。Yii::createObject()?就是Service Locator和DI容器親密關系的證明, 也是Service Locator構建于DI容器之上的證明。而Yii中所有的Module, 包括Application都是Service Locator,因此,它們也都構建在DI容器之上。
同時,在Yii框架代碼中,只要創建實例,就是調用?Yii::createObject()?這個方法來實現。 可以說,Yii中所有的實例(除了Application,DI容器自身等入口腳本中實例化的),都是通過DI容器來獲取的。
同時,我們不難發現,?Yii?的基類?yii\BaseYii?,所有的成員變量和方法都是靜態的, 其中的DI容器是個靜態成員變量?$container?。 因此,DI容器就形成了最常見形式的單例模式,在內存中僅有一份,所有的Service Locator (Module和Application)都共用這個DI容器。 就就節省了大量的內存空間和反復構造實例的時間。
更為重要的是,DI容器的單例化,使得Yii不同的模塊共用組件成為可能。 可以想像,由于共用了DI容器,容器里面的內容也是共享的。因此,你可以在A模塊中改變某個組件的狀態,而B模塊中可以了解到這一狀態變化。 但是,如果不采用單例模式,而是每個模塊(Module或Application)維護一個自己的DI容器, 要實現這一點難度會大得多。
所以,這種共享DI容器的設計,是必然的,合理的。
另外,前面我們講到,當Service Locator中服務或組件的定義是一個PHP callable時,對其形式有一定要求。 一是返回一個實例,二是不接收任何參數。 這在?Yii::createObject()?中也可以看出來。
由于?Yii::createObject()?為?yii\di\ServiceLocator::get()?所調用,且沒有提供第二參數, 因此,當使用 Service Locator獲取實例時,?Yii::createObject()?的?$params?參數為空。 因此,使用call_user_func($type,?$params)?調用這個PHP callable時, 這個PHP callable是接收不到任何參數的。
## Yii創建實例的全過程[](http://www.digpage.com/service_locator.html#yii "Permalink to this headline")
可能有的讀者朋友會有疑問:不對呀,前面講過DI容器的使用是要先注冊依賴,后獲取實例的。 但Service Locator在注冊服務、組件時,又沒有向DI容器注冊依賴。那在獲取實例的時候, DI容器怎么解析依賴并創建實例呢?
請留意,在向DI容器索要一個沒有注冊過依賴的類型時, DI容器視為這個類型不依賴于任何類型可以直接創建, 或者這個類型的依賴信息容器本身可以通過Reflection API自動解析出來,不用提前注冊。
可能還有的讀者會想:還是不對呀,在我開發Yii的過程中,又沒有寫過注冊服務的代碼:
~~~
Yii::$app->set('db', [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=db.digpage.com;dbname=digpage.com',
'username' => 'www.digpage.com',
'password' => 'www.digapge.com',
'charset' => 'utf8',
]);
Yii::$app->set('cache', [
'class' => 'yii\caching\MemCache',
'servers' => [
[
'host' => 'cache1.digpage.com',
'port' => 11211,
'weight' => 60,
],
[
'host' => 'cache2.digpage.com',
'port' => 11211,
'weight' => 40,
],
],
]);
~~~
為何可以在沒有注冊的情況下獲取服務的實例并使用服務呢?
其實,你也不是什么都沒寫,至少肯定是在某個配置文件中寫了有關的內容的:
~~~
return [
'components' => [
'db' => [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=yii2advanced',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
],
'cache' => [
'class' => 'yii\caching\MemCache',
'servers' => [
[
'host' => 'cache1.digpage.com',
'port' => 11211,
'weight' => 60,
],
[
'host' => 'cache2.digpage.com',
'port' => 11211,
'weight' => 40,
],
],
],
... ...
],
];
~~~
只不過,在?[_配置項(Configuration)_](http://www.digpage.com/configuration.html#configuration)?和?[_Object的配置方法_](http://www.digpage.com/property.html#object-config)?部分, 我們了解了配置文件是如何產生作用的,配置到應用當中的。 這個數組會被?Yii::configure($config)?所調用,然后會變成調用Application的 setComponents(), 而Application其實就是一個Service Locator。setComponents()方法又會遍歷傳入的配置數組, 然后使用使用 Service Locator 的set() 方法注冊服務。
到了這里,就可以了解到:每次在配置文件的?components?項寫入配置信息, 最終都是在向Application這個 Service Locator注冊服務。
讓我們回顧一下,DI容器、Service Locator是如何配合使用的:
* Yii?類提供了一個靜態的?$container?成員變量用于引用DI容器。 在入口腳本中,會創建一個DI容器,并賦值給這個?$container?。
* Service Locator通過?Yii::createObject()?來獲取實例, 而這個?Yii::createObject()?是調用了DI容器的?yii\di\Container::get()?來向?Yii::$container?索要實例的。 因此,Service Locator最終是通過DI容器來創建、獲取實例的。
* 所有的Module,包括Application都繼承自?yii\di\ServiceLocator?,都是Service Locator。 因此,DI容器和Service Locator就構成了整個Yii的基礎。
如果覺得《深入理解Yii2.0》對您有所幫助,也請[幫助《深入理解Yii2.0》](http://www.digpage.com/donate.html#donate)。 謝謝!
- 更新記錄
- 導讀
- Yii是什么
- Yii2.0的亮點
- 背景知識
- 如何閱讀本書
- Yii基礎
- 屬性(Property)
- 事件(Event)
- 行為(Behavior)
- Yii約定
- Yii應用的目錄結構和入口腳本
- 別名(Alias)
- Yii的類自動加載機制
- 環境和配置文件
- 配置項(Configuration)
- Yii模式
- MVC
- 依賴注入和依賴注入容器
- 服務定位器(Service Locator)
- 請求與響應(TBD)
- 路由(Route)
- Url管理
- 請求(Reqeust)
- Web應用Request
- Yii與數據庫(TBD)
- 數據類型
- 事務(Transaction)
- AcitveReocrd事件和關聯操作
- 樂觀鎖與悲觀鎖
- 《深入理解Yii2.0》視頻教程
- 第一講:基礎配置
- 第二講:用戶登錄
- 第三講:文章及評論的模型
- 附錄
- 附錄1:Yii2.0 對比 Yii1.1 的重大改進
- 附錄2:Yii的安裝
- 熱心讀者