深刻淺出依賴注入

本文首發於 深刻淺出依賴注入,轉載請註明出處。

本文試圖以一種易於理解的行文講解什麼是「依賴注入」這種設計模式。php

或許您已經在項目中已經使用過「依賴注入」,只不過因爲某些緣由,導致您對它的印象不是特別深入。html

「依賴注入」多是最簡單的設計模式之一,但即使如此我發現要想真正的以一種老小咸宜的方式把它講解透徹也絕非易事。sql

本文在寫做過程當中參考了諸多優秀的與「依賴注入」相關文章,我會從如下幾個方面給你們講解「依賴注入」到底是一種怎樣的設計模式:數據庫

目錄結構

  • 什麼是「組件」和「服務」segmentfault

    • 「組件」的定義
    • 「服務」的定義
    • 「組件」與「服務」的異同
  • 什麼是控制反轉和依賴注入設計模式

    • 一個簡單的示例
    • 控制反轉
    • 依賴注入
  • 如何實現依賴注入緩存

    • 經過構造函數注入依賴
    • 經過 setter 設值方法注入依賴
  • 什麼是依賴注入容器
  • 依賴注入的優缺點cookie

    • 優勢
    • 不足
  • 如何選擇依賴注入的方式session

    • 選擇經過構造函數注入:
    • 選擇經過 setter 設值方法注入
  • 參考資料

提示:本文內容較多,會耗費較多的閱讀實現,建議抽取空閒時間進行閱讀;建議不要錯過參考資料部分的學習;另外,因爲本人技術水平所限表述不到的地方歡迎指正。異步

若是您以爲本文對您有幫助,在收藏的同時請隨手點個「贊」,謝謝!

什麼是「組件」和「服務」

在講解什麼是依賴注入以前,咱們須要對什麼是依賴這個問題進行說明。

所謂的「依賴」就是指在實現某個功能模塊時須要使用另一個(或多個)「組件」或「服務」,那麼這個所需的「組件」或「服務」將被稱爲「依賴」。

後續文中統一使用「組件」表示某個模塊的「依賴」,「依賴注入」就是指向使用者注入某個「組件」以供其使用。

「組件」的定義

「組件」:它是可能被做者沒法控制的其它應用使用,但使用者不能對其源碼進行修改的一個功能模塊。

「服務」的定義

「服務」指:使用者以同步(或異步)請求遠程接口來遠程使用的一個功能接口。

「組件」與「服務」的異同

「組件」和「服務」的 共同之處 就是它們都將被其餘應用程序或功能模塊使用。

它們的不一樣之處在於:

  • 「組件」是在本地使用(如 jar 文件、dll 或者源碼導入)
  • 「服務」是在遠程使用(如 WebService、消息系統、RPC 或者 Socket)

什麼是控制反轉和依賴注入

「控制反轉」和「依賴注入」本質上就是一個從 問題發現實現 的過程。

即在項目中咱們經過使用「依賴注入」這種技術手段實現功能模塊對其依賴組件的「控制反轉」。

咱們在開發的過程當中時長會遇到這樣一個問題:如何才能將不一樣的「組件」進行組裝才能讓它們配合默契的完成某個模塊的功能?

「依賴注入」就是爲了完成這樣的 目標:將 依賴組件 的配置和使用分離開,以下降使用者與依賴之間的耦合度。

在闡述「依賴注入」這個模式具體含義前,仍是先看一個常見的示例,或許對於理解更有幫助。

一個簡單的示例

這個示例的靈感來自 What is Dependency Injection? 這篇文章(譯文 什麼是依賴注入?)。

從事服務端研發工做的同窗,應該有這樣的體驗。

因爲 HTTP 協議是一種無狀態的協議,因此咱們就須要使用「Session(會話)」機制對有狀態的信息進行存儲。一個典型的應用場景就是存儲登陸用戶的狀態到會話中。

<?php
$user = ['uid' => 1, 'uname' => '柳公子'];
$_SESSION['user'] = $user;

上面這段代碼將登陸用戶 $user 存儲「會話」的 user 變量內。以後,同一個用戶發起請求就能夠直接從「會話」中獲取這個登陸用戶數據:

<?php
$user = $_SESSION['user'];

接着,咱們將這段面向過程的代碼,以面向對象的方法進行封裝:

<?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];
    }

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

而且須要提供一個接口服務類 user:

<?php
class User
{
    protected $storage;

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

    public function login($user)
    {
        if (!$this->storage->exists('user')) {
            $this->storage->set('user', $user);
        }

        return 'success';
    }

    public function getUser()
    {
        return $this->storage->get('user');
    }
}

以上就是登陸所需的大體功能,使用起來也很是容易:

<?php
$user = new User();
$user->login(['uid' => 1, 'uname' => '柳公子']);
$loginUser = $user->getUser();

這個功能實現很是簡單:用戶登陸 login() 方法依賴於 $this->storage 存儲對象,這個對象完成將登陸用戶的信息存儲到「會話」的處理。

那麼對於這個功能的實現,究竟還有什麼值得咱們去擔憂呢?

一切彷佛幾近完美,直到咱們的業務作大了,會發現經過「會話」機制存儲用戶的登陸信息已近沒法知足需求了,咱們須要使用「共享緩存」來存儲用戶的登陸信息。這個時候就會發現:

User 對象的 login() 方法依賴於 $this->storage 這個具體實現,即耦合到一塊兒了。這個就是咱們須要面對的 核心問題

既然咱們已經發現了問題的癥結所在,也就很容易獲得 解決方案:讓咱們的 User 對象不依賴於具體的存儲方式,但不管哪一種存儲方式,都須要提供 set 方法執行存儲用戶數據。

具體實現能夠分爲如下幾個階段:

  1. 定義 Storage 接口

定義 Storage 接口的做用是: 使 UserSessionStorage 實現類進行解耦,這樣咱們的 User 類便再也不依賴於具體的實現了。

編寫一個 Storage 接口彷佛不會太複雜:

<?php

interface Storage
{
    public function set($key, $value);

    public function get($key);

    public function exists($key);
}

而後讓 SessionStorage 類實現 Storage 接口:

<?php
class SessionStorage implements Storage
{
    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];
    }

    public function exists($key)
    {
        return isset($this->get($key));
    }
}
  1. 定義一個 Storage 接口讓 User 類僅依賴 Storage 接口

如今咱們的 User 類看起來既依賴於 Storage 接口又依賴於 SessionStorage 這個具體實現:

<?php

class User
{
    protected $storage;

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

固然這已是一個完美的登陸功能了,直到我將這個功能開放出來給別人使用。然而,若是這個應用一樣是經過「會話」機制來存儲用戶信息,現有的實現不會出現問題。

但若是使用者將「會話」機制更換到下列這些存儲方式呢?

  • 將會話存儲到 MySQL 數據庫
  • 將會話存儲到 Memcached 緩存
  • 將會話存儲到 Redis 緩存
  • 將會話存儲到 MongoDB 數據庫
  • ...
<?php
// 想象下下面的全部實現類都有實現 get,set 和 exists 方法
class MysqlStorage {}

class MemcachedStorage {}

class RedisStorage {}

class MongoDBStorage {}

...

此時咱們彷佛沒法在不修改 User 類的構造函數的的狀況下,完成替換 SessionStorage 類的實例化過程。即咱們的模塊與依賴的具體實現類耦合到一塊兒了。

有沒有這樣一種解決方案,讓咱們的模塊僅依賴於接口類,而後在項目運行階段動態的插入具體的實現類,而非在編譯(或編碼)階段將實現類接入到使用場景中呢?

這種動態接入的能力稱爲「插件」。

答案是有的:可使用「控制反轉」。

控制反轉

「控制反轉」提供了將「插件」組合進模塊的能力。

在實現「控制反轉」過程當中咱們「反轉」了哪方面的「控制」呢?其實這裏的「反轉」的意義就是 如何去定位「插件」的具體實現

採用「控制反轉」模式時,咱們經過一個組裝模塊,將「插件」的具體實現「注入」到模塊中就能夠了。

依賴注入

瞭解完「控制反轉」,咱們再來看看什麼是「依賴注入」。「依賴注入」和「控制反轉」之間是怎樣的一種關係呢?

「控制反轉」是目的:它但願咱們的模塊可以在運行時動態獲取依賴的「插件」,而後,咱們經過「依賴注入」這種手段去完成「控制反轉」的目的。

這邊我試着給出一個「依賴注入」的具體的定義:

應用程序對須要使用的依賴「插件」在編譯(編碼)階段僅依賴於接口的定義,到運行階段由一個獨立的組裝模塊(容器)完成對實現類的實例化工做,並將其「注射」到應用程序中稱之爲「依賴注入」。

如何實現依賴注入

如何實現依賴注入或者說依賴注入有哪些形式?

Inversion of Control Containers and the Dependency Injection pattern 一文中有過相關的闡述:

依賴注入的形式主要有三種,我分別將它們叫作構造注入( Constructor Injection)、設值
方法注入( Setter Injection)和接口注入( Interface Injection)

本文將結合上面的示例稍微講下:

  1. 經過構造函數注入依賴
  2. 經過 setter 設值方法注入依賴

這兩種注入方式。

經過構造函數注入依賴

經過前面的文章咱們知道 User 類的構造函數既依賴於 Storage 接口,又依賴於 SessionStorage 這個具體的實現。

如今咱們經過重寫 User 類的構造函數,使其僅依賴於 Storage 接口:

<?php

class User
{
    protected $storage;

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

咱們知道 User 類中的 login 和 getUser 方法內依賴的是 $this->storage 實例,也就無需修改這部分的代碼了。

以後咱們就能夠經過「依賴注入」完成將 SessionStorage 實例注入到 User 類中,實現高內聚低耦合的目標:

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

經過 setter 設值方法注入依賴

設值注入也很簡單:

<?php

class User
{
    protected $storage;

    public function setStorage(Storage $storage)
    {
        $this->storage = $storage;
    }
}

使用也幾乎和構造方法注入同樣:

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

什麼是依賴注入容器

上面實現依賴注入的過程僅僅能夠當作一個演示,真實的項目中確定沒有這樣使用的。那麼咱們在項目中該如何去實現依賴注入呢?

嗯,這是個好問題,因此如今咱們須要瞭解另一個與「依賴注入」相關的內容「依賴注入容器」。

依賴注入容器咱們在給「依賴注入」下定義的時候有提到 由一個獨立的組裝模塊(容器)完成對實現類的實例化工做,那麼這個組裝模塊就是「依賴注入容器」。

「依賴注入容器」是一個知道如何去實例化和配置依賴組件的對象。

儘管,咱們已經可以將 User 類與實現分離,可是還須要進一步,才能稱之爲完美。

定義一個簡單的服務容器:

<?php
class Container
{
    public function getStorage()
    {
        return new SessionStorage();
    }

    public function getUser()
    {
        $user = new User($this->getStorage());
        return $user;
    }
}

使用也很簡單:

<?php
$container = new Container();
$user = $container->getUser();

咱們看到,若是咱們須要使用 User 對象僅須要經過 Container 容器的 getUser 方法便可獲取這個實例,而無需關心它是如何被建立建立出來的。

這樣,咱們就瞭解了「依賴注入」幾乎所有的細節了,可是現實老是會比理想更加骨感。由於,咱們現有的依賴注入容器還至關的脆弱,由於它一樣依賴於 SessionStorage,一旦咱們須要替換這個實現,仍是不得不去修改裏面的源代碼,而沒法實如今運行時配置。

作了這麼多工做,仍是這樣的結果,真是晴天霹靂啊!

爲何不考慮將實現類相關數據寫入到配置文件中,在容器中實例化是從配置文件中讀取呢?

有關使用依賴注入容器的更加詳細的使用能夠閱讀我翻譯的 依賴注入 系列文章,文章還部分篇章沒有翻譯,因此你也能夠直接閱讀 原文

依賴注入的優缺點

優勢

  • 提供系統解耦的能力
  • 能夠明確的瞭解到組件之間的依賴關係
  • 簡化測試工做

前兩個比較好理解,稍微說下依賴注入是如何簡化測試的。

若是咱們在實現 User 類時,尚未實現具體的 SessionStorage 類,而僅定義了 Storage 接口。

那麼在測試時,能夠編寫一個 NopStorage 先用於測試,以後等實現了 SessionStorage 在進行替換便可。

不足

組件與注入器之間不會有依賴關係,所以組件沒法從注入器那裏得到更多的服務,只能得到配置信息中所提供的那些。

如何選擇依賴注入的方式

如何選擇依賴注入方式在 Inversion of Control Containers and the Dependency Injection pattern 一文中有給出相關論述。

選擇經過構造函數注入:

  • 可以在構造階段就建立完整、合法的對象;
  • 帶有參數的構造子能夠明確地告訴你如何建立一個合法的對象;
  • 能夠隱藏任何不可變的字段。

選擇經過 setter 設值方法注入

  • 若是依賴的「插件」太多時,選擇設值注入更優

說完了什麼是「控制反轉」和「依賴注入」,相信你們已經對這兩個概念有了相對比較清晰的瞭解。我想說的是任何事物的瞭解程度都不是一蹴而就的,因此即使有號稱能一句話講明白什麼是「依賴注入」的文章,其實仍是須要咱們有了相對深刻的瞭解後才能感悟其中的真意,所謂「讀書百遍,其義自見」就是這個道理。

參考資料

相關文章
相關標籤/搜索