什麼是依賴注入?

譯文首發於 什麼是依賴注入,轉載請註明出處。

本文是依賴注入(Depeendency Injection)系列教程的第一篇文章,本系列教程主要講解如何使用 PHP 實現一個輕量級服務容器,教程包括:php


術語

  • Depeendency Injection 譯做 依賴注入
  • Depeendency Injection Container 譯做 依賴注入容器
  • Container 譯做 容器
  • Service Container 譯做 服務容器
  • Session 譯做 會話
  • Object-Oriented 譯做 面向對象
  • mock 譯做 模擬
  • anti-patterns 譯做 反模式
  • hardcoded 譯做 硬編碼

這篇文章不會涉及有關「容器」相關知識的講解,而是經過一些實際的案例帶你去了解「依賴注入」這種設計模式試圖解決哪些問題,以及如何幫助咱們解決這些問題的。若是您已經掌握「依賴注入」相關概念,那麼能夠跳過這篇文章。html

「依賴注入」也許是我所知的最簡單的設計模式之一,有可能您已經在項目中使用過「依賴注入」,但同時它也是最難以講透徹的模式之一。究其緣由,大概是由於市面上已有講解「依賴注入」模式的文章,大多都在使用一些毫無實際意義的示例。在此以前,我已經嘗試使用 PHP 語言來設計一些「依賴注入」的示例。因爲 PHP 是一門 Web 開發而生,咱們仍是以一些簡單的 Web 實例做爲開場較爲合適。laravel

因爲 HTTP 協議是無狀態的協議,因此 Web 應用須要一種技術可以存儲用戶信息。經過使用 Cookie 或者 PHP 內置的「會話」機制可以輕鬆實現這樣的需求:web

<?php
$_SESSION = 'fr';

上例能夠將用戶選擇的語言存儲到會話的 language 變量裏。以後,這位用戶發起的請求,均可以從 $_SESSION 數組中獲取 language 的值:數據庫

<?php
$user_language = $_SESSION['language'];

因爲「依賴注入」基於面向對象設計,因此咱們須要將上面的功能封裝到 SessionStorage 類裏:設計模式

<?php
class SessionStorage
{
    public function __construct($cookieName = 'PHP_SESS_ID')
    {
        session_name($cookieName);
        session_start();
    }

    public function set($key, $value)
    {
        $_SESSION[$key] = $value;
    }

    public function get($key)
    {
        return $_SESSION[$key];
    }
}

以及一個提供接口服務的類 User:數組

<?php
class User
{
    protected $storage;

    public function __construct()
    {
        $this->storage = new SessionStorage();
    }

    public function setLanguage($language)
    {
        $this->storage->set('language', $language);
    }

    public function getLanguage()
    {
        return $this->storage->get('language');
    }
}

這個實例很是簡單,而且 User 類調用方法也十分簡單:緩存

<?php
$user = new User();
$user->setLanguage('fr');
$user_language = $user->getLanguage();

一切都如此完美... 直到你須要擴展它。好比,你該如何修改 $this->storage 實例中的 cookie 名稱?通常有以下解決方案:性能優化

  • 直接在 User 類裏面建立 SessionStorage 實例時的 cookie 名稱硬編碼到它的構造函數:
<?php
class User
{
    public function __construct()
    {
        $this->storage = new SessionStorage('SESSION_ID');
    }
}
  • 經過在 User 類以外定義一個常量:
<?php
class User
{
    public function __construct()
    {
        $this->storage = new SessionStorage(STORAGE_SESSION_NAME);
    }
}

define('STORAGE_SESSION_NAME', 'SESSION_ID');
  • 將 User 類的構造函數重構,以接受一個會話名稱:
<?php
class User
{
  function __construct($sessionName)
  {
    $this->storage = new SessionStorage($sessionName);
  }

  // ...
}

$user = new User('SESSION_ID');
  • 或者 User 構造函數接收一個 Storage 類的選項配置:
<?php    
class User
{
    public function __construct($storageOptions)
    {
        $this->storage = new SessionStorage($storageOptions['session_name']);
    }

    // ...
}

$user = new User(array('session_name' => 'SESSION_ID'));

全部這些方案都不太好。在 User 類裏面硬編碼並無解決實際問題,後續你依舊沒法在不修改 User 類代碼的狀況下實現更改會話名稱的目的。使用一個常量也是一個壞主意,由於 User 類如今依賴於這個常量來設置。將會話名稱做爲參數傳遞或者做爲一組選項多是最好的解決方案,可是仍然很糟糕,由於這種方式將與 User 類無關的數據與 User 類耦合在一塊兒。cookie

另外,還有個問題也沒辦法輕鬆的解決:如何修改 SessionStorage 類?好比,須要使用「模擬」對象替換它用於測試。或者,須要替換會話存儲引擎到數據庫表或者內存。目前來看,咱們沒法在不修改 User 類的狀況下輕鬆實現。

「依賴注入」就是解決這種的問題,經過將 SessionStorage 對象以構造函數的參數傳給 User 實例,替換直接在 User 類中實例化的方式便可實現以上需求:

<?php
class User
{
    public function __construct($storage)
    {
        $this->storage = $storage;
    }

    // ...
}

這就是「依賴注入」!此時,若是要使用 User 類,會稍微比以前要複雜一些:

<?php
$storage = new SessionStorage('SESSION_ID');
$user = new User($storage);

這樣配置會話存儲對象和替換會話存儲實現類均可以輕鬆完成。得益於依賴的分離設計,在不改變 User 類的狀況下,一切皆有可能。

Pico Container website 是這樣描述依賴注入的:

「依賴注入」經過以構造函數參數,設值方法或屬性字段等方式將具體組件傳遞給依賴方(譯註:使用者)。

與其餘設計模式同樣,依賴注入也有一些反模式。Pico Container website 描述了其中的一些反模式。

「依賴注入」並不侷限於經過構造函數注入這一種注入形式:

  • 以構造函數注入:
<?php
 class User
{
    public function __construct($storage)
    {
        $this->storage = $storage;
    }

    // ...
}
  • 以設值方法注入
<?php
class User
{
    public function setSessionStorage($storage)
    {
        $this->storage = $storage;
    }
}
  • 以類成員變量方式注入:
<?php
class User
{
    public $sessionStorage;
}

$user->sessionStorage = $storage;

從個人經驗來看,經過構造函數注入適用於必要的依賴,如上例;設值注入適用於可選的依賴,如項目須要一個緩存功能的實現。

現在,不少 PHP 現代框架都依賴於「依賴注入」設計模式已達到高內聚低耦合的目標:

<?php

// symfony: A constructor injection example
$dispatcher = new sfEventDispatcher();
$storage = new sfMySQLSessionStorage(array('database' => 'session', 'db_table' => 'session'));
$user = new sfUser($dispatcher, $storage, array('default_culture' => 'en'));

// Zend Framework: A setter injection example
$transport = new Zend_Mail_Transport_Smtp('smtp.gmail.com', array(
  'auth'     => 'login',
  'username' => 'foo',
  'password' => 'bar',
  'ssl'      => 'ssl',
  'port'     => 465,
));

$mailer = new Zend_Mail();
$mailer->setDefaultTransport($transport);

若是您但願深刻「依賴注入」,強烈推薦閱讀 Martin Fowler introduction 以及 Jeff Moore 的分享。此外還有我去年有關 依賴注入的分享,這篇文章有更加細膩的依賴注入的解讀(譯註:可是很遺憾我一直打不開這個鏈接 :) )。

以上,就是今天所有內容。但願您對「依賴注入」有了更加深刻的瞭解。下一篇文章將聊聊「依賴注入容器」

資料

https://www.martinfowler.com/...
http://www.mendoweb.be/blog/d...
https://laravel-china.org/art...

原文 : http://fabien.potencier.org/w...

相關文章
相關標籤/搜索