Laravel框架的基石就是一個功能強大的 IoC 容器

Laravel00發表於2021-02-26

好記性不如爛筆頭,學習php開發也不能懶,作筆記是一種學習的好習慣!
文章來自:mp.weixin.qq.com/s/2zo7d1wb9mpweHw...
學習與交流: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(['LetsFeng']);
    $this->instance(UserRepositoryInterface::class, $repository);
    $response = $this->get('/users');

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

執行結果如下:

圖片


更進一步

讓我們考慮另一個例子來鞏固理解。當付費會員訂閱的某項服務週期快結束了,可能需要去提醒使用者該續費了。

我們會定義兩個介面,或者叫契約(這些契約使我們在更改實際實現時更加靈活),一個是支付介面,一個是通知介面

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 的例項。

從程式碼角度來說,這可能只是個微小的變動,但這種設計模式的引入,絕對會使你的整個應用架構煥然一新:因為明確指定了類的職責邊界,實現了不同層和服務之間的解耦,你的程式碼變得更加容易維護;

此外,從面向介面程式設計的角度來看,程式碼變得更加容易測試,你只需通過模擬注入依賴即可,不同類之間的測試完全可以隔離開來。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章