依賴注入也是最常見的模式之一。只要物件之間存在依賴關係,即可抽離相關的程式碼改用注入的方法,把物件注入依賴該物件才能運行的物件中。

在 Day 7 的範例原始碼,我們有一支名為 RedisCache 的類別,它的程式碼是這樣的:

依賴

範例:/day-7/example-1/CacheRedis.php

class CacheRedis
{
    protected $db;

    public function __construct($setting)
    {
        $host = $setting['host'];
        $port = $setting['port'];

        $this->db = new Redis();
        $this->db->connect($host, $port);
    }

    public function get($key)
    {
        return $this->db->get($key);
    }

    public function set($key, $value)
    {
        return $this->db->set($key, $value);
    }
}

這支類別是使用 Redis 這個類別來連接 Redis 資料庫。簡單的說,CacheRedis 的依賴 (Dependency) 是 Redis。那何謂注入 (Injection) 呢?

注入

CacheRedis 改寫如下:

範例:/day-8/example-1/CacheRedis.php

class CacheRedis
{
    protected $db;

    public function __construct(Redis $redis)
    {
        $this->db = $redis;
    }

    public function get($key)
    {
        return $this->db->get($key);
    }

    public function set($key, $value)
    {
        return $this->db->set($key, $value);
    }
}

可以看到在建構子的參數:

public function __construct(Redis $redis)
{
    $this->db = $redis;
}

我們把整段實例化 Redis 的程式碼移到外面去了,改用參數的方式帶入,並使用型別提示 (type hint),規定建構子只能接受 Redis 類別實例化後的物件。

範例:/day-8/example-1/Example.php

<?php

include 'CacheRedis.php';

$host = '127.0.0.1';
$port = 6379;

// 實例化 Redis
$redis = new Redis();
$redis->connect($host, $port);

// 注入 Redis 類別產生的物件
$cache = new CacheRedis($redis);

$cache->set('day8', 'ok');

echo $cache->get('day8') . "\n";

這樣做的好處在於,更貼近封閉 - 開放原則,即是對修改封閉、對擴展開放
不需要為了要新增 Redis 要使用的參數,而去修改 CacheRedis。直接在外部實例化 Redis 後,將物件注入至 CacheRedis 使用。

依賴反轉原則

在前段的介紹範例中,有一個情況會讓 CacheRedis 無法使用。因為 Redis 這個類別是 C 語言寫的 PHP 擴充套件,假如使用 PHP 版本較舊,無法裝該套件,那就 GG 了。

另外有一個是由 PHP 寫成的 Redis 套件名叫 Predis,我們決定使用它,另一方面我們想要不管是 Redis 類別,或是 Predis 類別,往後都可以使用。那麼「依賴反轉原則 (Dependency inversion principle)」是時候派上用場了呀。

teeWrHg.png

如上圖 A。高層物件 CacheRedis 依賴於低層物件 Redis。我們要將高層物件 CacheRedis 轉而依賴抽象介面RedisInterface。並將低層物件 RedisPredis 轉而實現RedisInterface介面。

範例:/day-8/example-2/RedisInterface.php

interface RedisInterface
{
    public function get($key);

    public function set($key, $value);
}

範例:/day-8/example-2/CacheRedis.php

class CacheRedis
{
    protected $db;

    public function __construct(RedisInterface $redis)
    {
        $this->db = $redis;
    }

    // 以下忽略 ...
}

建構子改為依賴介面RedisInterface,任何實作此介面都能注入至 CacheRedis

範例:/day-8/example-2/PredisExt.php

class PredisExt implements RedisInterface
{
    protected $db;

    public function __construct($setting)
    {
        $this->db = new \Predis\Client($setting);
    }

    public function get($key)
    {
        return $this->db->get($key);
    }

    public function set($key, $value)
    {
        return $this->db->set($key, $value);
    }
}

由於 Predis 是別人寫好的套件,我們無法讓它直接實作 RedisInterface,因此 PredisExt 是一個串接 Predis 並實作介面方法的類別。

範例:/day-8/example-2/RedisExt.php

class RedisExt extends Redis implements RedisInterface
{
    public function __construct($setting)
    {
        $this->connect($setting['host'],$setting['port']);
    }
}

Redis 類別已經有實作 RedisInterface 介面的方法了,因此直接繼承即可。

範例:/day-8/example-2/Example.php

$redis = new RedisExt([
    'host' => '127.0.0.1',
    'port' => 6379
]);

$cache = new CacheRedis($redis);
$cache->set('day8', 'example 2 - redis - ok');
echo $cache->get('day8') . "\n";

注入實現界面方法的 RedisExtCacheRedis 中。

$redis = new PredisExt([
    'host' => '127.0.0.1',
    'port' => 6379
]);

$cache = new CacheRedis($redis);
$cache->set('day8', 'example 2 - predis - ok');
echo $cache->get('day8') . "\n";

注入實現界面方法的 PredisExtCacheRedis 中。

如此一來不管是 Predis 或者是 Redis 類別,往後都能不用再修改 CacheRedis的情況下,注入使用。

總結

以上濃縮為兩句話:

  • 高層物件不依賴低層物件,兩者都依賴介面。
  • 介面不依照物件的細節去定義方法,而是介面定義方法後,物件的細節針對介面來實作設計。

依賴注入及反轉原則是在物件導向程式設計中非常重要的設計模式。請務必在往後設計自己的開放原始碼作品刻意地使用它。只要用過一次,就如同打通任督二脈,再回頭看理論描述,就會了解整個概念了。從做中學是最好的學習方法。

希望本篇對讀者們有幫助喔。我們明天見。

本篇原始碼可在此瀏覽

最後修改日期: 2022-02-01

作者

留言

撰寫回覆或留言

發佈留言必須填寫的電子郵件地址不會公開。