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();
    }
}

Reference

results matching ""

    No results matching ""