相信無論是初學PHP的新手, 還是多年經(jīng)驗的PHP老司機, 在學習Laravel這套框架的時候, 總是會被依賴注入和控制反轉這兩個概念絞的頭暈. 但是這兩個概念又是這個框架的核心思想, 而手冊中對這兩個概念的描述要么是蜻蜓點水, 隔靴搔癢的點到為止, 要么是類關系中引用一些暫時沒法實踐的代碼, 讓人在純概念上反復折騰也不得其解. 所以有很多人反復閱讀手冊中的兩個概念, 卻總是無法抓住這兩個概念的精髓.?
這其實不奇怪, 首先, 這些工程化的概念本身不屬于PHP或Javascript的, 而是來自于Java和.net這樣的強類型和純面向對象的語言, 實際上, 用PHP和Javascript這樣的弱類型語言, 并不能在控制反轉所要達到的效果進行嚴格的約束, 至少說方法的返回值類型上沒法限制, 控制反轉就不是那么真正徹底的控制反轉(暫時看不懂這句話沒關系, 看完這篇文章后面部分, 你就能懂我說的了).?
其次, 無論是PHP程序員還是Javascript程序員, 在選擇這門語言作為飯碗的時候, 很多人的考量是不想去燒腦Java中那些復雜的概念, 入門和選擇這些語言, 就是因為他們理念簡單, 復雜概念少, 能夠能夠快速做網(wǎng)站, 只是項目到達一定的代碼量后, 在工程層面上任何編程語言的組織是殊途同歸的, 因此, 最初選擇PHP的逃避理由終究成為一道坎(這也就是有10年經(jīng)驗PHP程序員看不懂手冊中核心觀念的原因), 那么, 我們今天就來看看這道坎的第一步: 依賴注入, 控制反轉, 容器, 這到底是怎么來的.
這兩個概念其實所指的是一件事情的兩個層面, 只是這種設計模式同時又是很多現(xiàn)代框架的核心, 不僅僅Laravel, PHP另一套框架Phalcon, python的Django, 以及前端的Angularjs和Angular(這是兩個不同的框架)都在用, 這就好比觀察者模式在rxjs中的位置一樣, 當然, 有一種感覺就是, 不懂他們也能用框架, 但是怎么用怎么覺得不是很有底. 不過, 無論是前端后端, 既定的事實是他們已經(jīng)是核心了,? 那么我們就從頭開始來研究它吧.
本文會從幾個Laravel很基礎的概念講起, 契約式編程, 服務, 服務提供者, 以及看不見摸不著的IOC容器(也叫服務容器), 和依賴注入怎么使用, 控制反轉到底控制什么反轉什么, 等等...... 一步一步的講述這幾個核心的概念.? 如果您明白了這些基礎概念所描述的東西是怎么一回事, 那么基本上再理解核心實現(xiàn)就會簡單多了.當然, 我并不會按照學究式的方式給一堆概念, 而是直觀的形象描述.
廢話不多說, 我們開始.............(本文的代碼基于Laravel, 其實其他框架在理解上也沒差......)
1.契約式編程:
以定義接口方式組織類別的一種編程方式.?
就是說, 以前, 我要做一個數(shù)據(jù)庫操作的模塊, 怎么辦?翻看手冊, 把PDO操作相關的函數(shù)啪啦啪啦看一遍, 然后就寫一個DB class, 把這些函數(shù)用一個類給它封裝起來, 順手封裝一個單例工廠, 要用的時候, 實例化一個對象, DB對象->方法(), 搞定!??
而契約式編程方法只是做了稍微拆分, 同樣要做一個數(shù)據(jù)庫操作的模塊, 我不著急先寫實現(xiàn)類, 先寫一個接口(interface)或這抽象類, 把這個模塊要實現(xiàn)的功能先定義出來, 然后再寫一個數(shù)據(jù)庫操作的實現(xiàn)類去繼承抽象類或者實現(xiàn)接口. 其實就這么回事兒.?
不同的是, 在要用到數(shù)據(jù)庫操作模塊的地方(比如構造方法中或調用的方法中), 直接用接口聲明一個實現(xiàn)數(shù)據(jù)庫操作的對象, 而不是在調用中實例化實現(xiàn)類的對象. 這里有些人會犯糊涂了, 接口不能實例化啊, 抽象類也不能實例化啊, 為什么我又能在方法中聲明一個實現(xiàn)接口的對象, 不著急, 后面會詳細解釋, 現(xiàn)在先理解契約式編程這個思路就好了.
契約式編程說白了就是, 我要寫一個類, 先寫一個接口定義操作的規(guī)范, 然后再寫一個類去實現(xiàn)這個接口, 這個過程就叫做契約式編程. 在Laravel中vendor/laravel/framework/src/Illuminate/Contracts目錄下面規(guī)范了一大堆接口, 然后實際類都是實現(xiàn)了這些接口. 也就是說, 契約式編程就是通過定義接口的方式告訴你, 類不要亂寫, 要寫某個功能的實現(xiàn)類, 得按照接口的規(guī)范來寫, 至于怎么使用實現(xiàn)類, 我們后面再說.
2.服務(service):?
Larave官方?jīng)]有定義, Angular官方倒是給出一堆定義, 但是看不懂^_^...........其實所謂服務, 就是我能提供什么, 能做什么, 簡單說就是功能類啊! 這就是服務.?
比如在laravel中要數(shù)據(jù)庫操作, 我把數(shù)據(jù)庫操作的邏輯都寫在Model中, 感覺Model太胖了, 那可以單獨寫一個Repository的類, 這個類去定義數(shù)據(jù)庫進行增刪改查的邏輯方法, 然后通過方法把結果返回去. 而這個單獨寫出來的Repository功能類,?就是一個服務.?
前面契約式編程里說到, 類不是不讓隨便定義嗎? 不是要符合接口規(guī)范嗎?其實這是針對在Laravel核心中, laravel團隊開發(fā)的所有類都依據(jù)Contracts的規(guī)范來寫, 而我們業(yè)務需求上的自定義類嘛, 不一定非得先寫一個RepositoryInterface再寫一個Repository類來實現(xiàn), 這里涉及到一個過度設計的問題, 就是說我目前只是要一個操作數(shù)據(jù)庫邏輯的類, 就只有一個作用也不存在什么統(tǒng)一規(guī)范的多個邏輯需求類, 那我就簡單的直接先寫個類就好. 當然, 也可以先寫個接口規(guī)范然后再實現(xiàn)以各類, 這個看需求.?
3.服務提供商(service provider)與依賴注入:
有了服務, 我們怎么提供服務呢? 這里就分為兩種情況:?
一個是我們只是寫了一個功能類(服務), 這個功能類沒有實現(xiàn)任何接口, 那么我要用的時候怎么辦?非常簡單, 比如我在PostController中要用到前面的Repository類, 那在控制器方法或構造方法中直接聲明一個對象就好了, 比如 index(Repository $repository), 這樣, 我們就能夠用這個功能類所提供的public方法了.換一種說法就是: 我們能夠利用Repository所提供的服務.?
也可以這么理解: 這個功能類(服務)沒有實現(xiàn)任何接口, 是這個功能領域中唯一的一個功能類(服務), 沒有同類型(實現(xiàn)同一接口)的服務, 因此它自身的這個服務就不需要注冊服務提供商了, 我們需要這個服務直接使用依賴注入的方式, 通過注入點(調用的index方法)注入到服務容器中就可以了.?
在這里, 只是在方法的參數(shù)中聲明一個類型的對象, 這個對象就可以在類中使用, 并不需要在需要使用對象的方法中進行實例化, 這個聲明的對象會由服務容器自動實例化并提供方法使用, 這就是依賴注入. 這種依賴注入, 雖然服務使用者(也就是上面的控制器)已經(jīng)不用自己實例化對象, 但是因為依賴的功能類有且只有一個, 需要的服務只是由控制器聲明后就沒有選擇使用哪個對象的余地(只能是Repository類的對象), 但是也就是說控制器依賴于服務類, 或者說被服務類所控制.
生活中的一個案例是: 假如我(上文中的控制器)需要家政服務, 然而這個地方整個家政行業(yè)有且只有一家公司提供家政服務(功能類), 那么我要找家政(注入點聲明)也就直接打個電話(依賴注入)給這個家政公司就能使用家政服務.(想想看, 只有一家家政公司, 沒選擇啊, 家政公司修改了業(yè)務, 不打掃衛(wèi)生了, 因為只有一家家政公司嘛, 我依賴它, 那我自己不會打掃, 我只能修改需求把打掃衛(wèi)生這個服務取消, 這就是我依賴于家政公司, 也可以說, 我要的這個服務被這個家政公司控制了)
4.服務提供商(service provider)和控制反轉:
另外一種情況是: 假如我們聲明了一個RepositoryInterface接口, 然后寫了兩個接口實現(xiàn)類:PostRepository類和UserRepository類, 在控制器中, 我們并沒有直接用類聲明, 而是用接口聲明對象:??index(RepositoryInterface $repository), 可是這個接口聲明的$repository依賴的接口會有不同的類來實現(xiàn)啊(也就是說, $repository可以是多種服務, 到底要用哪個服務?), ?這時候, 我們需要通過某種方式來告訴Laravel我們需要實現(xiàn)RepositoryInterface的對象的具體實現(xiàn)類是哪個, 怎么告訴它我們要用哪個類來實現(xiàn)呢??
這就要通過注冊服務提供商的方法, 我們可以通過 php artisan make:provider RepositoryServiceProvider的方式來生成一個服務提供商, 并且在app.config中配置這個服務提供商(也就是告訴laravel, 在一個生命周期中, 存在RepositoryServiceProvider這個服務提供商, 在我這里注冊了實現(xiàn)RepositoryInterface接口的具體服務是哪一個), 在這個服務提供商register()方法中綁定RepositoryInterface的具體實現(xiàn)類為PostRepository類或是UserRepository類, 這樣依賴于這個功能的控制器中就能明白我們要需要的服務是哪個了.
還是上面的生活案例: 現(xiàn)在這個地方的家政公司有很多, 提供的都是同一類的家政服務, 自然這就會有一家中介公司, 那么現(xiàn)在我需要家政服務的時候就學聰明了, 我在需要服務的地方(依賴注入點)聲明一個要求規(guī)范(接口聲明對象RepositoryInterface?$repository), 然后我要服務也不去找家政公司了, 我直接找中介公司(服務提供商,?RepositoryServiceProvider), 告訴他我要的服務規(guī)范(接口), 那么中介公司就去查看自己的花名冊(register()方法)中注冊了符合這個規(guī)范的家政公司(服務, 即功能類), 然后我再找到已經(jīng)注冊的家政公司(再register()方法中綁定的實現(xiàn)功能類)來為我提供服務. 這樣, 我需要家政服務的時候, 不再依賴于具體哪家家政公司, 而是服務提供商,同時我已經(jīng)聲明了規(guī)范, 要換一家家政公司的時候, 我自己不用做任何事, 中介的花名冊修改成另一個符合我聲明規(guī)范的家政公司, 我就能找到那個家政公司并讓它為我服務. 也就是, 原先家政公司只有一家, 我被家政公司綁架了, 是家政公司控制了我, 現(xiàn)在家政公司多了, 我就可以聲明標準來選擇家政公司, 控制權在于這個標準(接口)里了, 這個就是控制反轉!!!
High-level modules should not depend on low-level modules. Both should depend on abstractions.
高級模塊不要依賴于低級模塊, 應都依賴于抽象.
5.服務容器:?
現(xiàn)在我們大體上知道了依賴注入和控制反轉, 那么這里有一個核心的問題是, 依賴注入為什么不用實例化就可以使用對象??接口不是不能實例化對象嗎? 控制反轉為什么我們用接口聲明一個對象就可以直接使用這個對象? 這個服務容器在哪兒? 這就需要了解一下Laravel的服務容器了. 首先說明的是, 本文不會對服務容器怎么實現(xiàn)的具體細節(jié)進行描述, 而是在整體層面上來解釋.
整體層面弄清楚了, 具體的實現(xiàn)可以參考一下的鏈接:
https://www.cnblogs.com/lishanlei/p/7627367.html
在這里, 我主要是從框架整體的角度來探討服務容器, 這個在laravel4.0的時候稱之為IOC Container, 在laravel5.0以后稱之為Service Container, 不管怎么稱呼, 其實就是一個能實現(xiàn)依賴注入和控制反轉的對象! 那么這個對象到底在哪兒?這是一個很讓人迷惑的問題!?
要弄明白這個問題, 我們就要從MVC架構的原理上來說:
按照一個簡單的MVC框架的簡單原理來說, 一般會有一個index.php的文件作為整個框架的入口, 然后在這個入口文件中載入路由判斷的邏輯, 根據(jù)瀏覽器中傳入的路由參數(shù), 調用相關的控制器并實例化后調控制器方法, 控制器方法中引用模型的數(shù)據(jù), 并將模型數(shù)據(jù)按邏輯處理后引入模版將變量傳給視圖后顯示. 這是一個最簡單的原理流程, 在框架應用的過程中, 入口文件一般我們不會去改動, 也就是控制器的實例化我們基本不管, 我們只做控制器中間的邏輯, 但如果只是這樣的過程, 那么我們在控制器邏輯中要用到模型數(shù)據(jù), 我們必須去new一個對象來, 需要其他依賴類, 我們也需要去new一個來, 我們除了控制器由入口文件幫我們new好之外, 其他的相關對象都需要自己去new, 這可能看起來沒有什么, 不過就是需要對象的時候多一行new而已, 沒多少工作量啊^_^.......然而, 假如我們控制器依賴的一個類修改了構造函數(shù), 那么我們在控制器方法內new的那一行也需要修改相應的構造函數(shù), 這也還好........再復雜一點哪?多個控制器同時依賴?那就是每一個依賴的地方都需要修改, 依賴的依賴還需要接連著修改, 這樣修改一個依賴的功能類, 導致要連帶著修改控制器,而控制器本身又承擔著視圖和模型的邏輯, 這樣改起來就很痛苦了...........
既然在入口文件能幫我們實例化一個控制器, 那么有沒有這樣一種可能 , 我們在index.php實例化控制器的地方加一些自動化實例化對象的代碼, 寫一個類也行, 做一個函數(shù)也行, 總之就是幫我們把控制器相關依賴的類集中實例化, 這樣我們在控制器中碰到依賴的時候就不用一個個調用構造方法實例化了, 只要先寫好聲明就好, 聲明后寫正常的使用邏輯代碼, 然后最后程序運行的時候, 實例化控制器的時候同時也自動的實例化控制器依賴的其他類. 這就是最初的依賴注入方式的需求, 那么, 要實現(xiàn)這樣的過程, 就得在index.php中寫這個集中實例化并自動識別類依賴的功能代碼, 在程序員這個最高智商的群體的摸索之下, 終于通過反射機制寫出了實現(xiàn)這個目標的代碼, 而實現(xiàn)這個功能的代碼寫成一個類, 給他一個命名叫做: 容器(Container).由于這個容器可以自動識別相關的依賴, 并實例化所需要的類, 有了這個神器, 我們之前在index.php中寫控制器實例化的代碼也不需要了, 把控制器實例化的事情也一并交給容器就好了.
而這個容器類, 再laravel中就是Illuminate\Foundation\Application這個類, 因此,在Laravel的index中, 一開始就啥都不管, 按PSR標準把所有的依賴整理好autoload之后, 直接先實例化一個容器出來賦給$app, 然后給這個容器初始綁定內核, 內核再依賴其他的類, 一層層依賴下去, 實例化內核的時候就會把這一層層依賴的類, 就通過容器的自動實例化機制把內核依賴的類一個個實例化出來, 再遞歸解決依賴的依賴.(由于laravel比我們前面提的簡單框架考慮的更多, 簡單框架里的控制器實例化這個動作, 也就安排到內核加載之后的一系列實例化步驟的某一個環(huán)節(jié)中去了).
所以, 什么是容器? 在Laravel中, 第一個啟動實例化的動作, 只有Illuminate\Foundation\Application這個類, 這個類就是容器類, 實例化出來的$app就是一個容器, 這個$app綁定kernel之后, 所有的控制器和模型方法以及相關依賴的類, 都會由$app實例化的過程連鎖實例化出來, 也可以這么認為, 整個Laravel生命周期開始到結束依賴的所有類, 都是從$app的實例化之后連鎖自動實例化, 也就是許多文章常提到的說法, 這些類都在$app這個容器中.
現(xiàn)在, 我們容易理解一種說法了, 比如, 我自己寫了一個類A, 現(xiàn)在我在控制器中依賴注入A的對象$a, 控制器方法中的代碼可以寫為:public function index(A $a), 既然我們的控制器也是在這個容器實例化之后的連鎖實例化, 也就可以形象的說成是控制器在容器中, 這時候我們就可以這么描述這件事了, $a通過依賴注入注入到容器中(理解了這句話, 就理解了容器了). 當然, $app雖然是在最開始的時候實例化, 那我們在容器里面的代碼也可以引用這個$app(也就是容器的實例化引用)來在中途動態(tài)的綁定一些自定義想要綁定的服務, 比如在服務提供者中調用$app->bind()綁定相應的接口和實現(xiàn)類, 實際上在容器內的任何一個位置都可以調用, 比如在控制器中, 模型中, 依賴類中, 只是最常用的還是在服務提供商中調用而已.
現(xiàn)在明白了什么是容器到底是個什么東西, 容器到底在哪兒了吧?^_^.
其實我前面寫的內容, 已經(jīng)把這寫核心概念講的比較明白了, 如果前面的概念您沒看明白, 那么實際跟著我把下面的代碼敲一遍, 再回頭去看看前面的敘述, 相信您就會對Laravel的依賴注入和控制反轉有一個本質的掌握, 至于這兩個概念具體實現(xiàn)的源代碼細節(jié), 我個人認為看具體能力, 因為看懂這些細節(jié)的全部實現(xiàn)邏輯, 確實是一件比較燒腦的事情, 同時也不是有那么強烈的必要.