laravel實現-依賴注入


問題引出

整個 Laravel 框架的基石是一個功能強大的 IoC 容器(控制反轉容器),如果你想真正從底層理解 Laravel 框架,就必須好好掌握它。不過,也不要被這個名頭嚇住,要知道 IoC 容器只不過是一種用於方便我們實現「依賴注入」這種軟件設計模式的工具。而且要實現依賴注入並不一定非要通過 IoC 容器,只是使用 IoC 容器會更容易一點兒。

首先,來看看我們為何要使用依賴注入,或者說它能為我們的軟件開發帶來什么好處。考慮下列代碼中的類和方法:

class UserController extends BaseController
{
    public function getIndex()
    {
        $users = User::all();
        return View::make('users.index', compact('users'));
    }
}

這段代碼看起來很簡潔,但是不與數據庫打交道的話,我們將無法測試這段代碼。也就是說,Eloquent ORM 和該控制器有着緊耦合關系。如果不使用 Eloquent ORM,不連接到實際數據庫,我們就沒辦法運行或者測試這段代碼。同時,這段代碼也違背了「關注點分離」這個軟件設計原則。簡單來講:控制器知道的太多了。控制器不需要去了解數據是從哪兒來的,只要知道如何訪問就行。控制器也不需要知道數據在 MySQL 中是否有效,只需要知道它目前是可用的。

關注點分離:每一個類都應該是單一職責的,並且這個職責應該完全被這個類封裝。

所以,如果可以完全解耦 Web 控制器層和數據訪問層解耦,將會給我們帶來諸多便利:這會使得遷移數據存儲實現更容易;也會使得代碼測試更容易。「Web控制器」的職責就是真實應用的傳輸層:僅負責收集用戶請求數據,然后將其傳遞給處理方。

假設你有一個類似於監控器的應用程序,該應用有很多線纜接口,你可以通過這些接口來訪問監控器的功能,接口包括 HDMI,VGA,DVI 等。把互聯網想象成另一個插進應用的線纜接口,顯示器的大部分功能都是與線纜接口無關的、互相獨立的。線纜接口只是一種傳輸機制,就像 HTTP 只是你程序的一種傳輸機制一樣。所以,我們不想把傳輸機制(控制器)和業務邏輯混在一起。這樣做的好處是很多其他的傳輸層比如 API 接口、移動 App 等都可以訪問我們的業務邏輯。

因此,以后開發代碼就別再將控制器和 Eloquent ORM 耦合在一起了,咱們來注入一個倉庫類吧。

建立約定

首先,我們來定義一個接口,然后實現該接口。

interface UserRepositoryInterface
{
    public function all(): array;
}

class DbUserRepository implements UserRepositoryInterface
{
    public function all(): array
    {
        return User::all()->toArray();
    }
}

然后,我們將該接口的實現注入到我們的控制器。

class UserController extends BaseController
{
    public function __construct(UserRepositoryInterface $users)
    {
        $this->users = $users;
    }

    public function getIndex()
    {
        $users=$this->users->all();
        return View::make('users.index', compact('users'));
    }
}

現在,我們的控制器就完全不知道數據存儲在哪了。在這里,無知是福!我們的數據可能來自 MySQL、MongoDB 或者 Redis,我們的控制器不知道也不需要知道到底用的是什么數據庫,以及它們是如何存儲數據的,在具體實現上有什么區別。僅僅做出了這么小小的改變,我們就可以獨立於數據層來測試 Web 層了,將來如果需要的話,切換存儲實現也會很容易,兩者相互獨立,只要調用方法名不改,我們的控制器代碼不用做任何改動。

嚴守邊界:始終牢記保持明確的責任邊界,控制器和路由是作為 HTTP 和應用程序之間的中介者來提供服務的(用戶瀏覽應用的時候,路由/控制器作為中介將其引導到對應的服務)。當編寫大型應用程序時,不要將你的領域邏輯混雜在控制器或路由中。

為了鞏固你對這一理念的理解,我們來寫一個測試案例。首先,我們要通過 Mockery 動態模擬一個倉庫類實例,並將其綁定到應用的 IoC 容器里。然后,發起一個請求,通過斷言判定控制器是否正確地調用了這個倉庫類:

public function testUserTest()
{
    $repository = \Mockery::mock(UserRepositoryInterface::class);
    $repository->shouldReceive('all')->once()->andReturn(['學院君']);
    $this->instance(UserRepositoryInterface::class, $repository);
    $response = $this->get('/users');

    $response->assertStatus(200);
    $response->assertViewHas('users', ['學院君']);
}

運行結果如下:

更進一步

讓我們考慮另一個例子來鞏固理解。當付費會員訂閱的某項服務周期快結束了,可能需要去提醒用戶該續費了。我們會定義兩個接口,或者叫契約(這些契約使我們在更改實際實現時更加靈活),一個是支付接口,一個是通知接口:

interface BillerInterface 
{
    public function bill(array $user, $amount);
}

interface BillingNotifierInterface 
{
    public function notify(array $user, $amount);
}

接下來我們要寫一個 BillerInterface 接口的實現:

class StripeBiller implements BillerInterface
{
    public function __construct(BillingNotifierInterface $notifier)
    {
        $this->notifier = $notifier;
    }
    public function bill(array $user, $amount)
    {
        // Bill the user via Stripe...
        $this->notifier->notify($user, $amount);
    }
}

通過將責任划分到不同類中,我們現在可以很容易將不同的通知實現類注入到賬單類里面。比如,我們可以注入一個 SmsNotifier 或者 EmailNotifier。賬單類只需遵守了自己的契約即可(實現了賬單接口方法),不需要考慮如何實現通知功能。只要是遵守賬單通知契約(接口)的類,賬單類都可以用。這不僅讓我們的開發維護更加靈活,而且還可以通過模擬BillingNotifierInterface 實現類來進行賬單類的隔離測試,就像我們在上一個測試用例里做的那樣。

面向接口開發:編寫接口看上去好像要多寫一些代碼,但是磨刀不誤砍柴工,對於大型項目而言實際上反而能提升你的開發效率,這就是軟件設計領域經常說的面向接口開發,而不是面向對象開發。從測試角度來說,你不用實現任何接口,就能通過 Mockery 庫模擬接口實現實例,進而測試整個后端邏輯!

前面說了這么多,回到我們的主題,我們要如何做依賴注入呢?很簡單:

$biller = new StripeBiller(new SmsNotifier);

這就是一個依賴注入。賬單類 StripeBiller 不用考慮如何通知用戶,我們直接傳遞給它一個通知實現類 SmsNotifier 的實例。從代碼角度來說,這可能只是個微小的變動,但這種設計模式的引入,絕對會使你的整個應用架構煥然一新:因為明確指定了類的職責邊界,實現了不同層和服務之間的解耦,你的代碼變得更加容易維護;此外,從面向接口編程的角度來看,代碼變得更加容易測試,你只需通過模擬注入依賴即可,不同類之間的測試完全可以隔離開來。

那么 IoC 容器呢?難道依賴注入不需要 IoC 容器了么?當然不需要!在接下來的章節里面你會了解到,IoC 容器使得依賴注入更易於管理,但是容器本身不是依賴注入所必須的。只要遵循本章提出的原則,你可以在任何項目里面實現依賴注入,而不必管該項目是否提供了容器。

太像 Java?

在 PHP 中使用接口的一個常見批評就是代碼看上去太像 Java —— 意思是讓代碼顯得太冗長,你必須定義接口然后實現它,要多按好多次鍵盤。

對於小而簡單的應用來說,以上說法也對,在這種規模的應用中,接口通常是不必要的。將代碼耦合到那些你認為不會改變的地方也是可以的,比如都放在控制器方法中。在你確定以后不會發生改變的地方就沒有必要使用接口了,比如一次性的任務,或者一些原型或演示項目,畢竟這種靈活性會帶來更多的代碼量。

架構師可能會說「不會改變的地方是不存在的」。不過話說回來,有時候的確不會改。而且小型的應用也不需要架構師,架構師們都是為大型應用服務的。

在大型應用中,接口是很有幫助的。和提升的代碼靈活性、可測試性相比,多敲幾下鍵盤花費的時間就顯得微不足道了。當你在不同的接口實現類之間切換如飛的時候,你的經理一定會被你的神速驚到。此外,你也能夠寫出更能適應變化的代碼。

總而言之,記住本書提倡的「簡單」架構。如果你在寫小型應用的時候不想遵守接口原則,回退到原始模式,別覺得不好意思,那沒什么不對。不管如何,我都希望你們牢記「Code Happy」,快樂擼碼,這應該是我們的初心。如果你真的不喜歡寫接口,那就怎么舒服怎么來吧,做人嘛,開心最重要,不過還是希望你閑暇的時候可以好好評估下這件事。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM