當前位置:
首頁 > 最新 > PHP設計模式—服務定位器

PHP設計模式—服務定位器

跟DI容器類似,引入Service Locator目的也在於解耦。有許多成熟的設計模式也可用於解耦,但在Web應用上, Service Locator絕對佔有一席之地。 對於Web開發而言,Service Locator天然地適合使用, 主要就是因為Service Locator模式非常貼合Web這種基於服務和組件的應用的運作特點。 這一模式的優點有:

Service Locator充當了一個運行時的鏈接器的角色,可以在運行時動態地修改一個類所要選用的服務, 而不必對類作任何的修改。

一個類可以在運行時,有針對性地增減、替換所要用到的服務,從而得到一定程度的優化。

實現服務提供方、服務使用方完全的解耦,便於獨立測試和代碼跨框架復用。


在Yii中Service Locator由 yiidiServiceLocator 來實現。 從代碼組織上,Yii將Service Locator放到與DI同一層次來對待,都組織在 yiidi 命名空間下。 下面是Service Locator的源代碼:

從代碼可以看出,Service Locator繼承自 yiiaseComponent ,這是Yii中的一個基礎類, 提供了屬性、事件、行為等基本功能,關於Component的有關知識,可以看看 屬性(Property) 、 事件(Event) 和 行為(Behavior) 。

Service Locator 通過 __get() __isset() has() 等方法, 擴展了 yiiaseComponent 的最基本功能,提供了對於服務和組件的屬性化支持。

從功能來看,Service Locator提供了註冊服務和組件的 set() setComponents() 等方法, 用於刪除的 clear() 。用於讀取的 get() 和 getComponents() 等方法。

細心的讀者可能一看到 setComponents() 和 getComponents() 就猜到了, Service Locator還具有一個可讀寫的 components 屬性。


從上面的代碼中,可以看到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) 為真的定義。

從 yiidiServiceLocator::set() 的代碼:

服務或組件的ID在Service Locator中是唯一的,用於區別彼此。在任何情況下,Service Locator中同一ID只有一個實例、一個定義。也就是說,Service Locator中,所有的服務和組件,只保存一個單例。 這也是正常的邏輯,既然稱為服務定位器,你只要給定一個ID,它必然返回一個確定的實例。這一點跟DI容器是一樣的。

Service Locator 中ID僅起標識作用,可以是任意字元串,但通常用服務或組件名稱來表示。 如,以 db 來表示資料庫連接,以 cache 來表示緩存組件等。

至於批量註冊的 yiidiServiceLocator::setCompoents() 只不過是簡單地遍曆數組,循環調用 set() 而已。 就算我不把代碼貼出來,像你這麼聰明的,一下子就可以自己寫出來了。

向Service Locator註冊服務或組件,其實就是向 $_definitions 數組寫入信息而已。


Service Locator重載了 __get() 使得可以像訪問類的屬性一樣訪問已經實例化好的服務和組件。 下面是重載的 __get() 方法:

在註冊好了服務或組件定義之後,就可以像訪問屬性一樣訪問這些服務(組件)。 前提是已經完成註冊,不要求已經實例化。 訪問這些服務或屬性,被轉換成了調用 yiidiServiceLocator::get() 來獲取實例。 下面是使用這種形式訪問服務或組件的例子:

在Service Locator中,並未重載 __set() 。所以,Service Locator中的服務和組件看起來就好像只讀屬性一樣。 要向Service Locator中「寫」入服務和組件,沒有 setter 可以使用,需要調用 yiidiServiceLocator::set() 對服務和組件進行註冊。


與註冊服務和組件的簡單之極相反,Service Locator在創建獲取服務或組件實例的過程要稍微複雜一點。 這一點和DI容器也是很像的。 Service Locator通過 yiidiServiceLocator::get() 來創建、獲取服務或組件的實例:

Service Locator創建獲取服務或組件實例的過程是:

看看緩存數組 $_components 中有沒有已經創建好的實例。有的話,皆大歡喜,直接用緩存中的就可以了。

緩存中沒有的話,那就要從定義開始創建了。

如果服務或組件的定義是個對象,那麼直接把這個對象作為服務或組件的實例返回就可以了。 但有一點要注意,當使用一個PHP callable定義一個服務或組件時,這個定義是一個Closure類的對象。 這種定義雖然也對象,但是可不能把這種對象直接當成服務或組件的實例返回。

如果定義是一個數組或者一個PHP callable,那麼把這個定義作為參數,調用 Yii::createObject() 來創建實例。

這個 Yii::createObject() 在講配置時我們介紹過,當時只是點一點,這裡會講得更深一點。但別急,先放一放, 知道他能為Service Locator創建對象就OK了。我們等下還會講這個方法的。

我們在講DI容器時,提到了Yii中是把Service Locator和DI容器結合起來用的,Service Locator是建立在DI容器之上的。 那麼一個Yii應用,是如何使用Service Locator和DI容器的呢?


我們知道,每個Yii應用都有一個入口腳本 index.php 。在其中,有一行不怎麼顯眼:

這一行看著普通,也就是引入一個 Yii.php 的文件。但是,讓我們來看看這個 Yii.php

Yii 是一個工具類,繼承自 yiiBaseYii 。 但這裡對父類的代碼沒有任何重載,意味之父類和子類在功能上其實是相同的。 但是,Yii提供了讓你修改默認功能的機會。 就是自己寫一個 Yii 類,來擴展、重載Yii默認的、由 yiiBaseYii 提供的特性和功能。 儘管實際使用中,我們還從來沒有需要改寫過這個類,主要是因為沒有必要在這裡寫代碼,可以通過別的方式實現。 但Yii確實提供了這麼一個可能。這個在實踐中不常用,有這麼個印象就足夠了。

這裡重點看最後一句代碼,創建了一個DI容器,並由 Yii::$container 引用。 也就是說, Yii 類維護了一個DI容器,這是DI容器開始介入整個應用的標誌。 同時,這也意味著,在Yii應用中,我們可以隨時使用 Yii::$container 來訪問DI容器。 一般情況下,如無必須的理由,不要自己創建DI容器,使用 Yii::$container 完全足夠。


再看看入口腳本 index.php 的最後兩行:

創建了一個 yiiwebApplication 實例,並調用其 run() 方法。 那麼,這個 yiiwebApplication 是何方神聖? 首先, yiiwebApplication 繼承自 yiiaseApplication ,這從 yiiwebApplication 的代碼可以看出來

而 yiiaseApplication 又繼承自 yiiaseModule ,說明所有的Application都是Module

那麼 yiiaseModule 又繼承自哪個類呢?不知道你猜到沒,他繼承自 yiidiServiceLocator

所有的Module都是服務定位器Service Locator,因此,所有的Application也都是Service Locator。

同時,在Application的構造函數中, yiiaseApplication::__construct()

第一行代碼就把Application當前的實例,賦值給 Yii::$app 了。 這意味著Yii應用創建之後,可以隨時通過 Yii::$app 來訪問應用自身,也就是訪問Service Locator。

至此,DI容器有了,Service Locator也出現了。那麼Yii是如何擺布這兩者的呢?這兩者又是如何千里姻緣一線牽的呢?


Service Locator和DI容器的親密關係就隱藏在 yiidiServiceLocator::get() 獲取實例時, 調用的 Yii::createObject() 中。 前面我們說到這個 Yii繼承自 yiiBaseYii ,因此這個函數實際上是 BaseYii::createObject() , 其代碼如下:

這個 createObject() 提供了一個向DI容器獲取實例的介面, 對於不同的定義,除了PHP callable外, createObject() 都是調用了DI容器的 yiidiContainer::get() , 來獲取實例的。 Yii::createObject() 就是Service Locator和DI容器親密關係的證明, 也是Service Locator構建於DI容器之上的證明。而Yii中所有的Module, 包括Application都是Service Locator,因此,它們也都構建在DI容器之上。

同時,在Yii框架代碼中,只要創建實例,就是調用 Yii::createObject() 這個方法來實現。 可以說,Yii中所有的實例(除了Application,DI容器自身等入口腳本中實例化的),都是通過DI容器來獲取的。

同時,我們不難發現, Yii 的基類 yiiBaseYii ,所有的成員變數和方法都是靜態的, 其中的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() 為 yiidiServiceLocator::get() 所調用,且沒有提供第二參數, 因此,當使用 Service Locator獲取實例時, Yii::createObject() 的 $params 參數為空。 因此,使用 call_user_func($type, $params) 調用這個PHP callable時, 這個PHP callable是接收不到任何參數的。


可能有的讀者朋友會有疑問:不對呀,前面講過DI容器的使用是要先註冊依賴,後獲取實例的。 但Service Locator在註冊服務、組件時,又沒有向DI容器註冊依賴。那在獲取實例的時候, DI容器怎麼解析依賴並創建實例呢?

請留意,在向DI容器索要一個沒有註冊過依賴的類型時, DI容器視為這個類型不依賴於任何類型可以直接創建, 或者這個類型的依賴信息容器本身可以通過Reflection API自動解析出來,不用提前註冊。

可能還有的讀者會想:還是不對呀,在我開發Yii的過程中,又沒有寫過註冊服務的代碼:

為何可以在沒有註冊的情況下獲取服務的實例並使用服務呢?

其實,你也不是什麼都沒寫,至少肯定是在某個配置文件中寫了有關的內容的:

只不過,在 配置項(Configuration) 和 Object的配置方法 部分, 我們了解了配置文件是如何產生作用的,配置到應用當中的。 這個數組會被 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容器的 yiidiContainer::get() 來向 Yii::$container 索要實例的。 因此,Service Locator最終是通過DI容器來創建、獲取實例的。

所有的Module,包括Application都繼承自 yiidiServiceLocator ,都是Service Locator。 因此,DI容器和Service Locator就構成了整個Yii的基礎

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 網頁設計輕鬆學 的精彩文章:

TAG:網頁設計輕鬆學 |