聊聊 Interface 在 Laravel 開發中的使用

MArtian 發表於 2022-05-12
Laravel

聊聊 Interface 在 Laravel 開發中的使用

你也許聽過面向介面程式設計,而不是物件導向程式設計的設計思想,如 程式碼到介面,而不是實現,程式到介面,使用抽象而不是具體化等。

這些所指的都是同一件事,在開發中我們的應用程式應該依賴於抽象(介面)而不是具體的(類)。

這是我第一次聽到這個說法時的反應。為什麼我要使用介面而不是類?意味著我需要建立一個介面,我還要建立一個實現該介面的類???這不是浪費時間嗎?

當然這樣設計是有意義的


在架構師的眼中「沒有什麼是不會變化的」,或者說 改變 才是 不變 的。

我們開發的業務需求隨時間和不斷擴張而變化,我們的程式碼也是如此。

所以我們的程式碼必須靈活。

程式碼到介面使我們的程式碼鬆散耦合且靈活。

請看以下程式碼示例:

class Logger {

    public function log($content) 
    {
        //輸出 Log 日誌到檔案。
        echo "Log to file";
    }
}

一個簡單的 Logger 類將日誌記錄到檔案,我們來在控制器中呼叫它。

class LogController extends Controller
{
    public function log()
    {
        $logger = new Logger();
        $logger->log('Log this');
    }
}

但是,如果我們想記錄其他位置,如資料庫、檔案、雲或其他呢?

我們在 Logger 類中再新增幾個方法:

class Logger 
{
    public function logToDb($content) 
    {
        //輸出日誌到 DB。
    }

    public function logToFile($content) 
    {
        //輸出 Log 日誌到檔案。
    }

    public function logToCloud($content) 
    {
        //輸出 Log 日誌到雲。
    } 
}

然後我們還要在 LoggerController 中新增判斷:

class LogController extends Controller
{
    public function log()
    {
        $logger = new Logger();

        $target = config('log.target');

        $content = 'Log this.';

        switch ($target) {
            case 'db':
                $logger->logToDb($content);
                break;
            case 'file':
                $logger->logToFile($content);
                break;
            default:
                $logger->logToCloud($content);
        }
    }
}

好了,我們現在可以通過配置檔案把日誌輸出到各種終端。但我們如果還要再輸出日誌到 redis 呢?我們還需要再增加一個方法,並且在控制器中再加一次判斷。

控制器程式碼很快就變得臃腫,如果還要輸出日誌到更多地方呢?Logger 類中每個方法如果還需要擴充套件呢?這對於後期維護來說並不好。

這樣做同時也不符合 SOLID 原則,我們先來拆分一下 Logger 類,將職責拆分成不同的類。

//DBLogger.php
namespace App\Logs;
class DBLogger
{
    public function log($content)
    {
        //輸出日誌到 DB。
    }
}

//FileLogger.php
namespace App\Logs;
class FileLogger
{
    public function log($content)
    {
        //輸出 Log 日誌到檔案。
    }
}

//CouldLogger.php
namespace App\Logs;
class CloudLogger
{
    public function log($content)
    {
        //輸出 Log 日誌到雲。
    }
}

再來修改 LogController:

class LogController extends Controller
{
    public function log()
    {
        $target = config('log.target');

        switch ($target) {
            case 'db':
                (new DBLogger())->log($content);
                break;
            case 'file':
                (new FileLogger())->log($content);
                break;
            default:
                (new CouldLogger())->log($content);
        }
    }
}

這看上去還行,我們拆分了 Logger,如果需要新增輸出日誌到 redis,那就繼續再加 case 吧。


但依然有一個問題就是我們的控制器「知道太多了」,它應該只去呼叫一個 log() 方法來記錄,而不應該知道使用哪個 Logger 類,也不應該去例項化任何類,這樣在將來有改動的時候,不論是要輸出到哪裡,我們都不需要再來修改 LogController 的程式碼,那應該怎麼做呢?

這種情況最適合使用介面來實現了,什麼是介面呢?

介面是定義物件可以哪些執行操作的描述。

回到我們的程式碼,控制器只需要一個帶有 log() 方法的 Logger 類,所以我們的介面也必須定義一個 log() 方法。

interface LogInterface
{
    public function log($content);
}

我一般把 Interface 介面檔案放在專案的 App\Contracts 資料夾。

介面只包含方法宣告而不包含它的實現,這就是它被稱為 抽象 的原因。

在我們實現介面時,實現介面的類必須提供介面中定義的 抽象方法 的實現細節。

再回到我們的程式碼,我們改寫成以下:

// LogController
class LogController extends Controller
{
    public function log(LogInterface $logger)
    {
        $logger->log('log to');
    }
}

//DBLogger.php
namespace App\Logs;
use App\Contracts\LogInterface;

class DBLogger implements LogInterface
{
    public function log($content)
    {
        //輸出日誌到 DB。
    }
}

//FileLogger.php
namespace App\Logs;
use App\Contracts\LogInterface;

class FileLogger implements LogInterface
{
    public function log($content)
    {
        //輸出 Log 日誌到檔案。
    }
}

//CouldLogger.php
namespace App\Logs;
use App\Contracts\LogInterface;

class CouldLogger implements LogInterface
{
    public function log($content)
    {
        //輸出 Log 日誌到雲。
    }
}

現在我們的程式碼靈活且鬆耦合,無需觸及現有程式碼,就可以隨時改變 Logger 的實現來應對需求的變化:

class RedisLogger implements Logger
{
    public function log($content)
    {
        //輸出 Log 日誌到redis。
    }
}

在使用 Laravel 框架時,我們可以利用它的服務容器來自動注入介面的實現。

我們先新建一個配置檔案 config/log.php

<?php

return [
    'default' => env('LOG_TARGET', 'file'),

    'file' => [
        'class' => App\Logs\FileLogger::class,
    ],

    'db' => [
        'class' => App\Logs\DBLogger::class,
    ],

    'redis' => [
        'class' => App\Logs\RedisLogger::class,
    ]
];

並在 app/Providers/AppServiceProvider.php 新增以下程式碼。

public function register()
{
    $default = config('log.default');
    $logger = config("log.{$default}.class");

    $this->app->bind(
        \App\Contracts\LogInterface::class, 
        $logger
    );
}

我們從配置檔案中讀取預設 Logger,並將其繫結到 LogInterface。這樣每當我們請求 Logger 介面時,容器都會解析它並返回預設的 Logger 例項。

預設 Logger 是在 env() 配置的,我們可以在不同的環境中使用不同的 Logger,例如本地環境中記錄到檔案、生產環境中記錄到資料庫。

介面允許我們建立鬆散耦合的程式碼,同時提供一定程度的抽象。它允許我們隨時更改我們的實現,而無需更改它們的上下文。所以我們應該將應用程式中的所有可能會有變化的部分使用介面來實現。

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

當然,你如果在中小型專案中,不喜歡使用介面原則那也沒什麼不對,記住「Code Happy」快樂擼碼。不過還是建議你在閒暇時間好好評估一下這件事。


enjoy :tada:

本作品採用《CC 協議》,轉載必須註明作者和本文連結
我從未見過一個早起、勤奮、謹慎,誠實的人抱怨命運。