[譯] 4 種服務容器(service container)的使用方法幫助我們管理依賴

Epona發表於2019-08-09

本文翻譯自 4 Ways The Laravel Service Container Helps Us Managing Our Dependencies

在Laravel的世界裡,服務容器(Service Container)是一個很複雜的話題,我看到有許多人在嘗試搞清楚它到底是怎樣的一個原理,但是我們仍然不太懂。對我來說也一樣,這是因為很多的文章在解釋怎樣去“使用”服務容器。在這篇文章中,我將給大家解釋“什麼”是服務容器以及“何時”服務容器能幫助我們處理我們的依賴。

首先我們舉個栗子?,假設我們有一個匯出資料的類。它能夠匯出指定使用者的資料到CSV檔案中。

class UserStatsCsvExporter implements UserStatsExporterContract
{
    public function export(int $userId)
    {
         // Load user statistics...
         // Export file...
    }
}

在控制器中,我們會new一個類,然後呼叫裡面的export方法。

class ExportController extends Controller
{
    public function handle()
    {
        $userStatsExporter = new UserStatsCsvExporter();

        return $userStatsExporter->export(12);
    }
}

對於我們的控制器來說,這個匯出類就是一個依賴。就像上面的例子,我們能夠自己處理。那麼為什麼我們需要服務容器來管理我們的依賴呢?答案就是:控制器中的handle方法不應當有職責來建立匯出類。它的職責應當只是呼叫export方法。這樣的話我們也能服從反轉控制原則。

自動解析

這就是在我們有依賴的時候想要使用依賴注入的原因。那麼與其在handle方法中新建一個類,不如直接注入。我們可以在控制器的建構函式中進行注入,也可以在Laravel中的方法中進行注入。這叫做方法注入(method-injection)

public function handle(UserStatsCsvExporter $userStatsExporter)
{
    return $userStatsExporter->export(12);
}

通過上面的注入我們能夠直接呼叫export方法,而我們不需要告訴Laravel怎樣初始化這個類。這個方法能成功,主要的原因是在Laravel框架底層已經使用了服務容器。更確切的說,我們使用了服務容器的自動解析(auto-resolving)功能。

通過PHP的反射API,Laravel能夠找到我們的匯出類並且為我們自動建立。這是一個非常棒的功能。

但是,如果我們的匯出類自己也包含依賴呢?

class UserStatsCsvExporter implements UserStatsExporterContract
{

    /** @var Translator */
    private $translator;

    public function __construct(Translator $translator)
    {
        $this->translator = $translator;
    }

    public function export(int $userId)
    {
        // Load user statistics...
        // Export file...
    }
}

如上面的程式碼,我們在匯出類的建構函式中加入了一個Translator依賴。令人驚喜的是通過自動解析,程式碼仍然可以工作。所以,Laravel的自動解析功能十分聰明的為我們解決了相關的依賴問題。

只要我們的依賴是這種簡單的注入,而不需要傳值進去,上面的程式碼就能夠一直正常工作。

繫結到容器

Translator類中,我加入了一個新的建構函式需要我們在其初始化的時候傳入一個language字串。

class Translator
{
    /** @var string */
    private $language;

    public function __construct(string $language)
    {
        $this->language = $language;
    }

    public function translate(string $word)
    {
        // Translate word...
    }
}

現在由於Laravel不知道該傳遞什麼值給Translator類,因此自動解析方法已經無法使用了。這時我們需要告訴Laravel怎樣建立匯出例項以及需要怎樣的依賴。那麼最好的地方是在服務提供者(service provider)中進行處理。

下面我們新建一個 provider。

class UserStatsExporterProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind(UserStatsCsvExporter::class, function() {
           return new UserStatsCsvExporter(new Translator(config('app.locale')));
        });
    }
}

在每個服務提供者中,我們能夠使用$this->app來獲得服務容器。我們新寫入的language字串通過配置檔案進行載入。我們需要新建匯出例項的相關內容已經儲存到了服務容器例項中。這樣當我們需要匯出類的時候,我們不必再寫其他的程式碼來建立了。

如果你想的話,你可以使用dd(app()檢視一下現在的服務容器,在bindings屬性下面,你會發現已經包含了我們的匯出類。

bindings

繫結到介面

你已經看到了我們的CSV匯出類繼承了一個介面(interface)。這是因為我們還有一個類是用來處理匯出成XML格式的。它同樣也繼承了介面。假設我們現在需要將控制器中的CSV匯出類替換成XML匯出類。

當然,我們可以在控制器中使用XML類然後修改服務提供者中的程式碼。

public function handle(UserStatsXmlExporter $userStatsExporter)
{
    return $userStatsExporter->export(12);
}

public function register()
{
    $this->app->bind(UserStatsXmlExporter::class, function() {
       return new UserStatsXmlExporter(new Translator(config('app.locale'))
    });
}

雖然上面的修改能夠滿足我們的需求,但是有一個更好的處理辦法。由於我們已經定義了一個介面,與其使用CSV匯出類或者XML匯出類,不如我們直接使用介面。

public function handle(UserStatsExporterContract $userStatsExporter)
{
    return $userStatsExporter->export(12);
}

要讓上面程式碼工作,我們還需要改動服務提供者的程式碼。

public function register()
{
    $this->app->bind(UserStatsExporterContract::class, function() {
       return new UserStatsXmlExporter(new Translator(config('app.locale')));
    });
}

以後如果我們需要換回CSV匯出類或者其他的匯出類,我們只需要更改服務提供者的程式碼即可。

共享例項

在這篇文章裡我想最後介紹一下關於服務容器的一個功能就是共享。當我們檢查2個同樣的匯出類時,你會看到兩個不同的ID。這表示我們建立了2個例項。

public function handle(UserStatsExporterContract $userStatsExporter)
{
    dd(app(UserStatsExporterContract::class), app(UserStatsExporterContract::class));

    return $userStatsExporter->export(12);
}

share

對於大多數情況來說,這可能就是我們所需要的,但是某些情況下我們需要返回同樣的例項。要達成這樣的目的,我們僅需要使用singleton來替換bind方法即可。

public function register()
{
    $this->app->singleton(UserStatsExporterContract::class, function() {
       return new UserStatsXmlExporter(new Translator(config('app.locale')));
    });
}

share

你可以看到ID已經一致了。這樣做的原因主要有2個:

  1. 儲存狀態

當你在例項中儲存了一些資訊,其他部分的程式訪問的時候這些資訊仍然在那裡。

  1. 效能更好

有些時候建立例項並不是簡單的新建一個類就可以。你可能需要處理很多的依賴,匯入配置等等。在這種情況下,共享已經建立號的例項會比重新建立的效能要好一些。

一個很好的例子就是Laravel的資料庫服務,當你使用的時候,它需要建立一個與你資料庫的連線。那麼在整個程式執行的過程中,保持這個連線是一個很好的實現。而不必每次呼叫資料庫服務的時候再建立。

結論

這篇文章介紹了4種服務容器的使用方法。希望能夠讓你們明白“為什麼”以及“何時”使用服務容器。

There's nothing wrong with having a little fun.

相關文章