PHP Dependency Injection 相依性注入
這篇介紹了 Factory,並舉例如何分開Business Logic 和 Constructor物件。這篇會更深入介紹 Dependency Injection。
什麼是 Dependency Injection?
先用一個生活故事來說明比較好理解:
五歲的小朋友要喝果汁
如果你讓五歲的小朋友去開冰箱拿果汁,這小朋友可能拿出蛋糕,巧克力,甜食等等你不想讓她吃的東西。請他自己倒果汁,有可能會灑翻滿地,打破杯子。
比較好的應該是請小孩坐在桌子上,爸爸或媽媽開冰箱,將果汁倒在杯子,給小孩喝。
這場景說明了DI角色:
- Service: 倒果汁
- Client: 小孩
- Interface: 喝xx果汁
- Injector: 父母,負責開冰箱,將果汁倒入紙杯。
對應到 Dependency Injection四個角色的定義:
- the service object(s) to be used (杯子+果汁)
- the client object that is depending on the services it uses. (小孩)
- the interfaces that define how the client may use the services (喝xx果汁)
- the injector, which is responsible for constructing the services and injecting them into the client. (父母)
DI的精神就是,給予客戶(client)要的服務(service),並且客戶依照(interface)的規範使用服務。服務是透過injector負責組裝或建立好。不能由客戶(Client)建立而是搞壞服務。
先看下面沒有 DI 的程式碼
<?php
namespace DemoDI;
class Cup
{
private $name;
public function __construct($name)
{
$this->name = $name;
}
public function drink():string
{
return 'a drink of ' . $this->name;
}
}
// An example without dependency injection
class Baby
{
// Internal reference to the service used by this client
private $cup;
// Constructor
public function __construct()
{
// Specify a specific implementation in the constructor instead of using dependency injection
$this->cup = new Cup('orange juice');
}
// Method within this client that uses the services
public function get():string
{
return 'Baby get ' . $this->cup->drink();
}
}
在Baby中,我們直接建造了一杯柳橙汁,這樣就缺發了彈性,如果今天要換成水或其他果汁時怎麼辦?
三種型態的 Dependency Injection
- constructor injection: 在建構的時候給予
- setter injection: 透過設定給予
- interface injection: client必須實作setter介面,給予服務。 下面是相關的程式碼:
Construtor Injection
// An example use Constructor DI
class Baby
{
private $cup;
// Constructor
public function __construct($cup)
{
$this->cup = $cup;
}
public function get():string
{
return 'Baby get ' . $this->cup->drink();
}
}
// Injector, response for construct orange joice.
$cup = new Cup('orange joice');
// DI in constructor
$baby = new Baby($cup);
echo $baby->get() . "\n";
Setter Injection
// Use setter DI
class Baby
{
public $cup;
public function get():string
{
return 'Baby get ' . $this->cup->drink();
}
}
// Injector, response for construct orange joice.
$cup = new Cup('orange joice');
$baby = new Baby();
// property setter
$baby->cup = $cup;
echo $baby->get() . "\n";
Interface injection
// interface
interface cupSetter
{
public function setCup($cup);
}
// Use setter DI
class Baby implements cupSetter
{
private $cup;
public function get():string
{
return 'Baby get ' . $this->cup->drink();
}
public function setCup($cup)
{
$this->cup = $cup;
}
}
// Injector, response for construct orange joice.
$cup = new Cup('orange joice');
$baby = new Baby();
$baby->setCup($cup);
echo $baby->get() . "\n";
Why Dependency Injection is important?
用下面的例子說明,我們寫程式經常會建立一個Singleton的dbClient,或是static method去擷取html get。
static class HtmlClient
{
public static function get($url):string
{
$ch = curl_init($url);
// return the transfer as a string. instead of output it directly
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
//set to 1 to tell the library to include the header in the body output for requests with this handle.
curl_setopt($ch, CURLOPT_HEADER, 0);
$data = curl_exec($ch);
curl_close($ch);
return $data;
}
}
如下的DocumentHardtoTest
:你會發覺做太多的事情在constructor:
- 直接引用static method
- 發出url request.
- 取得html回來。
這樣的程式碼,如果要測試時,變得沒有機會可以override HtmlClient。
// Work inside of constructor.
class DocumentHardtoTest
{
public $html;
public function __construct($url)
{
// It's hard to override HtmlClient
$this->html = HtmlClient::get($url);
}
}
如果我們將程式碼,用Dependency Injection的方式改變如下:
class DocumentEasytoTest
{
public $html;
public function __construct($htmlClient, $url)
{
//we can test now
$this->html = $htmlClient::get($url);
}
}
我們就可以在測試的時候,給傳入$stubHtmlClient,由假的 $stubHtmlClient 模擬 get 回傳結果。
Law of Demeter - loose couple
但這上面的結果,並不是最好,因為construct裡面包含了其的服務,而這服務是Document本身不在意的。Document 只在意 html 的資料。
超商結帳
上面的 Document 裡面包含了 Html request, 就好像你去超商結帳$100元的商品,你把你的錢包拿給了店員,請店員打開錢包,從錢包裡面拿出$100元的紙鈔去結帳。
Business Logic vs Constructor Objects
所以比較好的做法,應該讓Document物件變單純:
class DocumentBetter
{
private $html;
public function __construct($html)
{
$this->html = $html;
}
}
這樣一來要生成 Document 物件變得很容易,也比較好測試。 有關產生 Document 與 HtmlClient 的物件關聯 (Object Graph)就交由 Constructor 物件:
class DocumentInjectory
{
private $client;
public function __construct($client)
{
$this->client = $client;
}
public function documentBuild($url):DocumentBetter
{
return new DocumentBetter($this->client::get($url));
}
}
$client = new HtmlClient();
$documentInjectory = new DocumentInjectory($client);
$DocumentBetter = $documentInjectory->documentBuild('https://tw.yahoo.com/');
我們依舊保持著彈性在 DocumentInjectory
,讓生成的時候,我們可以輕易地改動 $client
。利用 documentBuild 產生出 document 物件。
總結
DI的好處:
- Late binding (Client需要用服務的時候才綁定)
- Extensibility (擴充性好)
- Maintainability (容易維護,subClass工作定義清楚)
- Parallel development (平行開發)
- Testability (容易測試)
回歸到一開始的小孩子喝果汁的例子,我們應該在小孩子想要的時候,給他就可以了。不要讓小孩開門玩冰箱。
class Baby
{
public function get($cup):string
{
return 'Baby get ' . $cup->drink();
}
}