contracts
laravel 的契約是一組定義框架提供的核心服務(wù)的接口, 例如我們在介紹用戶認(rèn)證的章節(jié)中到的用戶看守器契約IllumninateContractsAuthGuard 和用戶提供器契約IlluminateContractsAuthUserProvider以及框架自帶的AppUser模型所實(shí)現(xiàn)的IlluminateContractsAuthAuthenticatable契約。
為什么使用契約
通過上面幾個契約的源碼文件我們可以看到,Laravel提供的契約是為核心模塊定義的一組Interface。Laravel為每個契約都提供了相應(yīng)的實(shí)現(xiàn)類,下表列出了Laravel為上面提到的三個契約提供的實(shí)現(xiàn)類。
所以在自己開發(fā)的項(xiàng)目中,如果Laravel提供的用戶認(rèn)證系統(tǒng)無法滿足需求,你可以根據(jù)需求定義看守器和用戶提供器的實(shí)現(xiàn)類,比如我之前做的項(xiàng)目就是用戶認(rèn)證依賴于公司的員工管理系統(tǒng)的API,所以我就自己寫了看守器和用戶提供器契約的實(shí)現(xiàn)類,讓Laravel通過自定義的Guard和UserProvider來完成用戶認(rèn)證。自定義用戶認(rèn)證的方法在介紹用戶認(rèn)證的章節(jié)中我們介紹過,讀者可以去翻閱那塊的文章。
所以Laravel為所有的核心功能都定義契約接口的目的就是為了讓開發(fā)者能夠根據(jù)自己項(xiàng)目的需要自己定義實(shí)現(xiàn)類,而對于這些接口的消費(fèi)者(比如:Controller、或者內(nèi)核提供的 AuthManager這些)他們不需要關(guān)心接口提供的方法具體是怎么實(shí)現(xiàn)的, 只關(guān)心接口的方法能提供什么功能然后去使用這些功能就可以了,我們可以根據(jù)需求在必要的時候?yàn)榻涌诟鼡Q實(shí)現(xiàn)類,而消費(fèi)端不用進(jìn)行任何改動。
定義和使用契約
上面我們提到的都是Laravel內(nèi)核提供的契約, 在開發(fā)大型項(xiàng)目的時候我們也可以自己在項(xiàng)目中定義契約和實(shí)現(xiàn)類,你有可能會覺得自帶的Controller、Model兩層就已經(jīng)足夠你編寫代碼了,憑空多出來契約和實(shí)現(xiàn)類會讓開發(fā)變得繁瑣。我們先從一個簡單的例子出發(fā),考慮下面的代碼有什么問題:
class?OrderController?extends?Controller { ????public?function?getUserOrders() ????{ ????????$orders=?Order::where('user_id',?'=',?Auth::user()->id)->get(); ????????return?View::make('order.index',?compact('orders')); ????} }
這段代碼很簡單,但我們要想測試這段代碼的話就一定會和實(shí)際的數(shù)據(jù)庫發(fā)生聯(lián)系。
也就是說, ORM和這個控制器有著緊耦合。如果不使用Eloquent ORM,不連接到實(shí)際數(shù)據(jù)庫,我們就沒辦法運(yùn)行或者測試這段代碼。這段代碼同時也違背了“關(guān)注分離”這個軟件設(shè)計(jì)原則。
簡單講:這個控制器知道的太多了。?
控制器不需要去了解數(shù)據(jù)是從哪兒來的,只要知道如何訪問就行。控制器也不需要知道這數(shù)據(jù)是從mysql或哪兒來的,只需要知道這數(shù)據(jù)目前是可用的。
Separation Of Concerns 關(guān)注分離
Every class should have a single responsibility, and that responsibility should be entirely encapsulated by the class.
每個類都應(yīng)該只有單一的職責(zé),并且職責(zé)里所有的東西都應(yīng)該由這個類封裝
接下來我們定義一個接口,然后實(shí)現(xiàn)該接口
interface?OrderRepositoryInterface? { ????public?function?userOrders(User?$user); } ? class?OrderRepository?implements?OrderRepositoryInterface { ????public?function?userOrders(User?$user) ????{ ????????Order::where('user_id',?'=',?$user->id)->get(); ????} }
將接口的實(shí)現(xiàn)綁定到Laravel的服務(wù)容器中
App::singleton('OrderRepositoryInterface',?'OrderRespository');
然后我們將該接口的實(shí)現(xiàn)注入我們的控制器
class?UserController?extends?Controller { ????public?function?__construct(OrderRepositoryInterface?$orderRepository) ????{ ????????$this->orders?=?$orderRespository; ????} ??? ????public?function?getUserOrders() ????{ ????????$orders?=?$this->orders->userOrders(); ????????return?View::make('order.index',?compact('orders')); ????} }
現(xiàn)在我們的控制器就完全和數(shù)據(jù)層面無關(guān)了。在這里我們的數(shù)據(jù)可能來自MySQL,mongodb或者redis。我們的控制器不知道也不需要知道他們的區(qū)別。這樣我們就可以獨(dú)立于數(shù)據(jù)層來測試Web層了,將來切換存儲實(shí)現(xiàn)也會很容易。
接口與團(tuán)隊(duì)開發(fā)
當(dāng)你的團(tuán)隊(duì)在開發(fā)大型應(yīng)用時,不同的部分有著不同的開發(fā)速度。
比如一個開發(fā)人員在開發(fā)數(shù)據(jù)層,另一個開發(fā)人員在做控制器層。
寫控制器的開發(fā)者想測試他的控制器,不過數(shù)據(jù)層開發(fā)較慢沒法同步測試。那如果兩個開發(fā)者能先以interface的方式達(dá)成協(xié)議,后臺開發(fā)的各種類都遵循這種協(xié)議。
一旦建立了約定,就算約定還沒實(shí)現(xiàn),開發(fā)者也可以為這接口寫個“假”實(shí)現(xiàn)
class?DummyOrderRepository?implements?OrderRepositoryInterface? { ????public?function?userOrders(User?$user) ????{ ????????return?collect(['Order?1',?'Order?2',?'Order?3']); ????} }
一旦假實(shí)現(xiàn)寫好了,就可以被綁定到IoC容器里
App::singleton('OrderRepositoryInterface',?'DummyOrderRepository');
然后這個應(yīng)用的視圖就可以用假數(shù)據(jù)填充了。接下來一旦后臺開發(fā)者寫完了真正的實(shí)現(xiàn)代碼,比如叫RedisOrderRepository。
那么使用IoC容器切換接口實(shí)現(xiàn),應(yīng)用就可以輕易地切換到真正的實(shí)現(xiàn)上,整個應(yīng)用就會使用從Redis讀出來的數(shù)據(jù)了。
接口與測試
建立好接口約定后也更有利于我們在測試時進(jìn)行Mock
public?function?testIndexActionBindsUsersFromRepository() {???? ????//?Arrange... ????$repository?=?Mockery::mock('OrderRepositoryInterface'); ????$repository->shouldReceive('userOrders')->once()->andReturn(['order1',?'order2]); ????App::instance('OrderRepositoryInterface',?$repository); ????//?Act... ????$response??=?$this->action('GET',?'OrderController@getUserOrders'); ????????? ????//?Assert... ????$this->assertResponseOk(); ????$this->assertViewHas('order',?['order1',?'order2']); ?}
總結(jié)
接口在程序設(shè)計(jì)階段非常有用,在設(shè)計(jì)階段與團(tuán)隊(duì)討論完成功能需要制定哪些接口,然后設(shè)計(jì)出每個接口具體要實(shí)現(xiàn)的方法,方法的入?yún)⒑头祷刂颠@些,每個人就可以按照接口的約定來開發(fā)自己的模塊,遇到還沒實(shí)現(xiàn)的接口完全可以先定義接口的假實(shí)現(xiàn)等到真正的實(shí)現(xiàn)開發(fā)完成后再進(jìn)行切換,這樣既降低了軟件程序結(jié)構(gòu)中上層對下層的耦合也能保證各部分的開發(fā)進(jìn)度不會過度依賴其他部分的完成情況。
更多l(xiāng)aravel框架相關(guān)技術(shù)文章,請?jiān)L問laravel教程欄目!