依賴注入也是最常見的模式之一。只要物件之間存在依賴關係,即可抽離相關的程式碼改用注入的方法,把物件注入依賴該物件才能運行的物件中。
在 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
依賴 Redis
。那何謂注入呢?
將 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
類別,往後都可以使用。那麼「依賴反轉原則」是時候派上用場了呀。
如上圖 A。高層物件 CacheRedis
依賴於低層物件 Redis
。我們要將高層物件 CacheRedis
轉而依賴抽象介面RedisInterface
。並將低層物件 Redis
及 Predis
轉而實現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";
注入實現界面方法的 RedisExt
到 CacheRedis
中。
$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";
注入實現界面方法的 PredisExt
到 CacheRedis
中。
如此一來不管是 Predis
或者是 Redis
類別,往後都能不用再修改 CacheRedis
的情況下,注入使用。
以上濃縮為兩句話:
依賴注入及反轉原則是在物件導向程式設計中非常重要的設計模式。請務必在往後設計自己的開放原始碼作品刻意地使用它。只要用過一次,就如同打通任督二脈,再回頭看理論描述,就會了解整個概念了。從做中學是最好的學習方法。
希望本篇對讀者們有幫助喔。我們明天見。
本篇原始碼可在此瀏覽。
本文同步更新於 TerryL 部落格 Day 8 - PHP 設計模式:依賴注入 (Dependency Injection),歡迎前往討論。
原始檔好像少Redis.php這個類別 !!
沒這個檔,RedisExt這個類別在extends會出錯 !
抱歉,我知道原因了,Redis是指原生的Redis