解析Laravel框架下的Contracts契約
- 2020 年 3 月 16 日
- 筆記
Contracts
Laravel 的契約是一組定義框架提供的核心服務的介面, 例如我們在介紹用戶認證的章節中到的用戶看守器契約IllumninateContractsAuthGuard
和用戶提供器契約IlluminateContractsAuthUserProvider
以及框架自帶的AppUser模型所實現的IlluminateContractsAuthAuthenticatable
契約。
為什麼使用契約
通過上面幾個契約的源碼文件我們可以看到,Laravel提供的契約是為核心模組定義的一組interface。Laravel為每個契約都提供了相應的實現類,下表列出了Laravel為上面提到的三個契約提供的實現類。
所以在自己開發的項目中,如果Laravel提供的用戶認證系統無法滿足需求,你可以根據需求定義看守器和用戶提供器的實現類,比如我之前做的項目就是用戶認證依賴於公司的員工管理系統的API,所以我就自己寫了看守器和用戶提供器契約的實現類,讓Laravel通過自定義的Guard和UserProvider來完成用戶認證。自定義用戶認證的方法在介紹用戶認證的章節中我們介紹過,讀者可以去翻閱那塊的文章。
所以Laravel為所有的核心功能都定義契約介面的目的就是為了讓開發者能夠根據自己項目的需要自己定義實現類,而對於這些介面的消費者(比如:Controller、或者內核提供的 AuthManager這些)他們不需要關心介面提供的方法具體是怎麼實現的, 只關心介面的方法能提供什麼功能然後去使用這些功能就可以了,我們可以根據需求在必要的時候為介面更換實現類,而消費端不用進行任何改動。
定義和使用契約
上面我們提到的都是Laravel內核提供的契約, 在開發大型項目的時候我們也可以自己在項目中定義契約和實現類,你有可能會覺得自帶的Controller、Model兩層就已經足夠你編寫程式碼了,憑空多出來契約和實現類會讓開發變得繁瑣。我們先從一個簡單的例子出發,考慮下面的程式碼有什麼問題:
class OrderController extends Controller { public function getUserOrders() { $orders= Order::where('user_id', '=', Auth::user()->id)->get(); return View::make('order.index', compact('orders')); } }
這段程式碼很簡單,但我們要想測試這段程式碼的話就一定會和實際的資料庫發生聯繫。
也就是說, ORM和這個控制器有著緊耦合。如果不使用Eloquent ORM,不連接到實際資料庫,我們就沒辦法運行或者測試這段程式碼。這段程式碼同時也違背了“關注分離”這個軟體設計原則。
簡單講:這個控制器知道的太多了。
控制器不需要去了解數據是從哪兒來的,只要知道如何訪問就行。控制器也不需要知道這數據是從MySQL或哪兒來的,只需要知道這數據目前是可用的。
Separation Of Concerns 關注分離
Every class should have a single responsibility, and that responsibility should be entirely encapsulated by the class.
每個類都應該只有單一的職責,並且職責里所有的東西都應該由這個類封裝
接下來我們定義一個介面,然後實現該介面
interface OrderRepositoryInterface { public function userOrders(User $user); } class OrderRepository implements OrderRepositoryInterface { public function userOrders(User $user) { Order::where('user_id', '=', $user->id)->get(); } }
將介面的實現綁定到Laravel的服務容器中
App::singleton('OrderRepositoryInterface', 'OrderRespository');
然後我們將該介面的實現注入我們的控制器
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')); } }
現在我們的控制器就完全和數據層面無關了。在這裡我們的數據可能來自MySQL,MongoDB或者Redis。我們的控制器不知道也不需要知道他們的區別。這樣我們就可以獨立於數據層來測試Web層了,將來切換存儲實現也會很容易。
介面與團隊開發
當你的團隊在開發大型應用時,不同的部分有著不同的開發速度。
比如一個開發人員在開發數據層,另一個開發人員在做控制器層。
寫控制器的開發者想測試他的控制器,不過數據層開發較慢沒法同步測試。那如果兩個開發者能先以interface的方式達成協議,後台開發的各種類都遵循這種協議。
一旦建立了約定,就算約定還沒實現,開發者也可以為這介面寫個“假”實現
class DummyOrderRepository implements OrderRepositoryInterface { public function userOrders(User $user) { return collect(['Order 1', 'Order 2', 'Order 3']); } }
一旦假實現寫好了,就可以被綁定到IoC容器里
App::singleton('OrderRepositoryInterface', 'DummyOrderRepository');
然後這個應用的視圖就可以用假數據填充了。接下來一旦後台開發者寫完了真正的實現程式碼,比如叫RedisOrderRepository
。
那麼使用IoC容器切換介面實現,應用就可以輕易地切換到真正的實現上,整個應用就會使用從Redis讀出來的數據了。
介面與測試
建立好介面約定後也更有利於我們在測試時進行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']); }
總結
介面在程式設計階段非常有用,在設計階段與團隊討論完成功能需要制定哪些介面,然後設計出每個介面具體要實現的方法,方法的入參和返回值這些,每個人就可以按照介面的約定來開發自己的模組,遇到還沒實現的介面完全可以先定義介面的假實現等到真正的實現開發完成後再進行切換,這樣既降低了軟體程式結構中上層對下層的耦合也能保證各部分的開發進度不會過度依賴其他部分的完成情況。
更多學習內容請訪問:
怎麼從一名碼農成為架構師的必看知識點:目錄大全(不定期更新)