iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 5
1
Modern Web

RRR撞到不負責之 Laravel + Nuxt.js 踩坑全紀錄系列 第 5

Day 05. 一不小心就會扯遠的依賴注入 (DI)

今天要聊的主題是依賴注入 Dependency Injection (DI),聊這個有點為難,一來是一不小心就會解釋太遠,二來是自認為只是略懂。所以今天打算規劃如下,先說明依賴注入的基本精神,再來說明之後會怎麼用到,至此就有能力繼續之後文章的閱讀,剩餘的篇章會聊聊對於依賴注入的理解,以及 Laravel Service Provider 的運用,因為不確定能否解釋清楚,所以若有幸大神路過,還麻煩指證錯誤(給予鞭打?!) XD 。

Dependency Injection

依賴注入聽起來就不是很好吃的東西 (欸) ,倒過來說會比較好理解 注入依賴 (後續簡稱 DI)。DI 的最主要精神在於解耦,意思就是說程式之間降低彼此的耦合性,讓彈性增加。

例如,兩津是標準的錢鬼,要他做任何事都建立在錢的基礎上,因此「錢」就是兩津的依賴,而所謂的 DI 就是別人提供阿兩金錢,而不是要阿兩自己生錢出來,同時阿兩任何同等金錢概念的物品都接受,不論是日圓、美金或是小判都好,這樣阿兩對特定的貨幣的耦合性就很低。

好拉不說垃圾話,直接上 code。假設我們有一個功能是「當文章被訂閱的時候,要推送訊息給作者知道」,我們常見的寫法會是:

// ...
class PostService
{
    // ...

    public function subscribed()
    {
        $email = new EmailSender();
        $email->setTarget($author);
        $email->send();
        // ...

所謂的 DI 就是將要用到的類別實例,改為由外部帶參數進來:

    public function subscribed(EmailSender $email)
    {
        $email->setTarget($author);
        $email->send();

從上面的舉例就比較能呼應 注入依賴 一詞,其中 「依賴」 代表function 內所要使用的一些資料或是實例;而 「注入」 代表由外部提供。

至此即為 DI 基本的寫法,但為因應更多複雜的使用情況以及降低對依賴的耦合,因此可以再進一步改寫:

interface MessageSender
{
    public function setTarget($author);

    public function send();
}

class EmailSender implements MessageSender
{
    public function setTarget($author)
    {
        $this->target = $author['email'];
    }

    public function send()
    {
        // ...
    }
}

class LineSender implements MessageSender
{
    public function setTarget($author)
    {
        $this->target = $author['id'];
    }

    public function send()
    {
        // ...
    }
}

當我們有各種不同的發送訊息工具,文章訂閱功能就可以修改成下面的形式,對於 subscribed 而言,他只要知道要設定傳訊息對象以及傳送訊息兩件事即可,至於是使用哪種工具傳送就不是他要關注的事項了。

    public function subscribed(MessageSender $sender)
    {
        $sender->setTarget($author);
        $sender->send();

簡單的 DI 重點整理:

  1. 所謂的 DI 就是將原來程式裡面要產生出來的實例改為由外部帶參數進來。
  2. 若依類本身可能有各種類型可以抽換,製作 interface 作為參數型別可以大大降低耦合性。

使用時機與運作機制

在 Laravel 當中具有自動依賴的部分,而且是屬於巢狀依賴,意思是依賴本身若有其他依賴,依賴本身也會自動注入。 例如,由於 EmailSender 在建構時期有依賴需要注入,因此當程式執行 subscribed 的時候,會自動注入 EmailSender 同時也會為了 EmailSender 再注入 SMTPConnection:

class EmailSender implements MessageSender
{
    private $smtp;
    public function __construct(SMTPConnection $smtp)
    {
        $this->smtp = $smtp;
        // ...
    public function subscribed(EmailSender $email)
    {
        $email->setTarget($author);
        $email->send();

Laravel 之所以能夠實現自動注入,是因為具有 Service ContainerService Provider以及PHP的Reflection機制,由於這部分再講下去會有點遠,因此如果單純運用跟閱讀上這裡先不需要更深入的探討,只要知道一點: 所有 Controller (存放所有 api 網址對應方法的類別,之後會提到) 的 __constructfunctions 都會實現自動注入。


Laravel 的 DI 重點整理:

  1. DI 是一種將原本在程式中需要建構出來的資料,改由外部程式提供參數進入使用。
  2. 若以 Interface 作為注入可以為程式提高彈性,當然注入具體類別也是沒問題的。
  3. Laravel 具有自動且巢狀的注入。
  4. Laravel 自動注入的起始點為 Controller 的 __construct 和 functions (說法不是太精確但可以先這樣理解)。

Service Provider

還記得上面的 MessageSender 嗎? 因為他是 interface,所以如果沒有告訴 Laravel 實際上到底要注入甚麼具體類別會出錯。因此 Service Provider 就是提供我們註冊各種日後需要注入依賴的地方。

  1. 首先在 cmd 輸入下列指令
php artisan make:provider MessageServiceProvider
  1. 產生的 ServiceProvider 檔案如下,register() 是我們主要告訴 Laravel 到底要提供甚麼樣具體類別的地方
namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class MessageServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        // 在這裡進行註冊
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        // 當註冊完服務之後會呼叫、執行的邏輯
    }
}

  1. 在 register() 當中,我們會使用 Laravel Service Container bind 的方法完成註冊的邏輯
    public function register()
    {
        // 告訴 Laravel 當每次呼叫 MessageSender 自動注入時,
        // 都會以執行後面 function (閉包),取得一個全新且正確具體類別實例
        $this->app->bind(MessageSender::class, function($app) {
            // 各種必要的判斷邏輯
            $usingLineSender = request()->has('line');
            if ($usingLineSender) {
                // 回傳正確的具體類別
                return new LineSender();
            }
            // 回傳正確的具體類別
            return new EmailSender();
        });
    }
  1. 開啟 configs/app.phpproviders 中加入下列程式
    'providers' => [
        // ...
        /*
         * Application Service Providers...
         */
         // ...
         App\Providers\MessageServiceProvider::class,

根據不同需求,我們可以利用 Laravel Service Container 綁定各種不同的服務,例如應該許多專案都會有細碎 helper functions 我們就可以集中起來,同時因為 function 大多相互獨立或是不會因為使用情境而有狀態改變的需求,因此我們就可以設計為,當每次呼叫 Helper 類別進行注入,我們都只使用第一次也是唯一的實例,例如下面的範例:

class Helper
{
    // 各種獨立沒有關聯性的 function 

    public function isValidatedRequest($request, &$response) {
        // ...
    }

    public function twTimestamp(string $date, string $format)
    {
        //...
    }
    public function register()
    {
        // 每次呼叫 Helper 進行注入的時候,
        // 先檢查系統是否已經存在 Helper 的實例,
        // 如果已經存在就回傳該實例,反之,建立、回傳一個新的實例
        $this->app->singleton(Helper::class, function($app) {
            return new Helper();
        });
    }

Static Class Vs Singleton

這裡已經開始走鐘了,由於在上面提到 singleton 跟 Helper 這個範例,一定會有人說何必這麼麻煩,寫成 static class 就好了。確實從程式執行的角度來看並沒有甚麼差別,而且製作 singleton 還比較麻煩,這裡沒有要戰的意思,只是在拜讀 Stack Overflow 和網路上各位大神的文章,為 singleton 整理出下列優點:

  1. static class 只能使用 static members
  2. singleton 可以繼承其他 class 以及實作 interface,更加符合 OOP 特性
  3. singleton 因為實作 interface 所以有利於進行各種抽換,尤其是在寫測試的時候

由於小弟個人初次在撰寫 Laravel 的時候,實在不知道為甚麼各種地方都會將參數自動帶入 __constructfunctions,而且還沒看到是怎麼給的,因此利用輕鬆愉快的小周末,和各位大大聊一下 DI,即便是只有要快速開發 api,至少也可以比較不會這麼困惑 (還是只有我太笨 XD),而後半段的 service provider、service container 以及 Singleton pattern 如果有說錯或是不清楚的地方還希望各位大大指證或交換意見! 明天我們就要來寫 Repository 啦~

參考


上一篇
Day 04. DB 三劍客 Migration, Model 和 Resource
下一篇
Day 06. Controller 減重計畫 (Repository 篇)
系列文
RRR撞到不負責之 Laravel + Nuxt.js 踩坑全紀錄31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
blue860601
iT邦新手 5 級 ‧ 2022-02-11 22:20:01

最近才剛回鍋laravel(?),有點忘記概念是甚麼,版主的教學非常前顯易懂XD

我要留言

立即登入留言