天天看點

控制反轉IOC、依賴注入DI的詳細說明與舉例

文章目錄

    • 引入
    • IOC介紹
    • IOC的實作
      • 通過構造函數注入依賴
      • 通過 setter 設值方法注入依賴
      • 依賴注入容器
    • IOC優缺點
      • 優點
      • 缺點

閱讀時忽略語言差異,參考了很多其他部落客内容,參考博文在最後給出,侵删

引入

由于 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 方法執行存儲使用者資料。

具體實作可以分為以下幾個階段:

定義 Storage 接口

定義 Storage 接口的作用是: 使 User 與 SessionStorage 實作類進行解耦,這樣我們的 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));
    }
}
           

定義一個 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 類的執行個體化過程。即我們的子產品與依賴的具體實作類耦合到一起了。

有沒有這樣一種解決方案,讓我們的子產品僅依賴于接口類,然後在項目運作階段動态的插入具體的實作類,而非在編譯(或編碼)階段将實作類接入到使用場景中呢?

這種動态接入的能力稱為「插件」。

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

IOC介紹

面向對象設計的軟體系統中,它的底層都是由N個對象構成的,各個對象之間通過互相合作(就像下面的齒輪一樣),最終實作系統地業務邏輯。

控制反轉IOC、依賴注入DI的詳細說明與舉例

伴随着工業級應用的規模越來越龐大,對象之間的依賴關系也越來越複雜,經常會出現對象之間的多重依賴性關系,是以,架構師和設計師對于系統的分析和設計,将面臨更大的挑戰。對象之間耦合度過高的系統,必然會出現牽一發而動全身的情形。

軟體工程中的耦合是指各個子產品依賴程度,為了便于維護,自然希望耦合越低越好。

耦合關系不僅會出現在對象與對象之間,也會出現在軟體系統的各子產品之間,以及軟體系統和硬體系統之間。如何降低系統之間、子產品之間和對象之間的耦合度,是軟體工程永遠追求的目标之一。為了解決對象之間的耦合度過高的問題,軟體專家Michael Mattson 1996年提出了IOC理論,用來實作對象之間的“解耦”,目前這個理論已經被成功地應用到實踐當中。

IOC是Inversion of Control的縮寫,多數書籍翻譯成“控制反轉”。

1996年,Michael Mattson在一篇有關探讨面向對象架構的文章中,首先提出了IOC 這個概念。對于面向對象設計及程式設計的基本思想,前面我們已經講了很多了,不再贅述,簡單來說就是把複雜系統分解成互相合作的對象,這些對象類通過封裝以後,内部實作對外部是透明的,進而降低了解決問題的複雜度,而且可以靈活地被重用和擴充。

IOC理論提出的觀點大體是這樣的:借助于“第三方”實作具有依賴關系的對象之間的解耦。如下圖:

控制反轉IOC、依賴注入DI的詳細說明與舉例

大家看到了吧,由于引進了中間位置的“第三方”,也就是IOC容器,使得A、B、C、D這4個對象沒有了耦合關系,齒輪之間的傳動全部依靠“第三方”了,全部對象的控制權全部上繳給“第三方”IOC容器,是以,IOC容器成了整個系統的關鍵核心,它起到了一種類似“粘合劑”的作用,把系統中的所有對象粘合在一起發揮作用,如果沒有這個“粘合劑”,對象與對象之間會彼此失去聯系,這就是有人把IOC容器比喻成“粘合劑”的由來。

我們再來做個試驗:把上圖中間的IOC容器拿掉,然後再來看看這套系統:

控制反轉IOC、依賴注入DI的詳細說明與舉例

我們現在看到的畫面,就是我們要實作整個系統所需要完成的全部内容。這時候,A、B、C、D這4個對象之間已經沒有了耦合關系,彼此毫無聯系,這樣的話,當你在實作A的時候,根本無須再去考慮B、C和D了,對象之間的依賴關系已經降低到了最低程度。是以,如果真能實作IOC容器,對于系統開發而言,這将是一件多麼美好的事情,參與開發的每一成員隻要實作自己的類就可以了,跟别人沒有任何關系!

我們再來看看,控制反轉(IOC)到底為什麼要起這麼個名字?我們來對比一下:

軟體系統在沒有引入IOC容器之前,對象A依賴于對象B,那麼對象A在初始化或者運作到某一點的時候,自己必須主動去建立對象B或者使用已經建立的對象B。無論是建立還是使用對象B,控制權都在自己手上。

軟體系統在引入IOC容器之後,這種情形就完全改變了,由于IOC容器的加入,對象A與對象B之間失去了直接聯系,是以,當對象A運作到需要對象B的時候,IOC容器會主動建立一個對象B注入到對象A需要的地方。

通過前後的對比,我們不難看出來:對象A獲得依賴對象B的過程,由主動行為變為了被動行為,控制權颠倒過來了,這就是“控制反轉”這個名稱的由來。

IOC的實作

兩種實作方式:依賴查找(DL)、依賴注入(DI)

控制反轉IOC、依賴注入DI的詳細說明與舉例

DL 已經被抛棄,因為他需要使用者自己去是使用 API 進行查找資源群組裝對象,即有侵入性。

我們着重看看DI:

2004年,Martin Fowler探讨了同一個問題,既然IOC是控制反轉,那麼到底是“哪些方面的控制被反轉了呢?”,經過詳細地分析和論證後,他得出了答案:“獲得依賴對象的過程被反轉了”。控制被反轉之後,獲得依賴對象的過程由自身管理變為了由IOC容器主動注入。于是,他給“控制反轉”取了一個更合适的名字叫做“依賴注入(Dependency Injection)”。他的這個答案,實際上給出了實作IOC的方法:注入。所謂依賴注入,就是由IOC容器在運作期間,動态地将某種依賴關系注入到對象之中。

是以,依賴注入(DI)和控制反轉(IOC)是從不同的角度的描述的同一件事情,就是指通過引入IOC容器,利用依賴關系注入的方式,實作對象之間的解耦。

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

通過構造函數注入依賴

通過前面的文章我們知道 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);
           

接口注入就是實作相關接口,通過接口定義調用其中的inject方法完成注入過程。

依賴注入容器

上面實作依賴注入的過程僅僅可以當做一個示範,真實的項目中肯定沒有這樣使用的。那麼我們在項目中該如何去實作依賴注入呢?

嗯,這是個好問題,是以現在我們需要了解另外一個與「依賴注入」相關的内容「依賴注入容器」。

依賴注入容器我們在給「依賴注入」下定義的時候有提到 由一個獨立的組裝子產品(容器)完成對實作類的執行個體化工作,那麼這個組裝子產品就是「依賴注入容器」。

「依賴注入容器」是一個知道如何去執行個體化和配置依賴元件的對象。

盡管,我們已經能夠将 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 方法即可擷取這個執行個體,而無需關心它是如何被建立建立出來的。

IOC優缺點

優點

1、靈活性

對于廣泛使用的接口,更改其實作類變得更簡單(例如,用生産執行個體替換模拟web服務)

更改給定類的檢索政策更簡單(例如,将服務從類路徑移動到JNDI樹)

添加攔截器很容易,而且在一個地方就可以完成(例如,将緩存攔截器添加到基于JDBC的DAO中)

2、可讀性

該項目有一個統一一緻的元件模型,代碼更簡潔,而且沒有依賴項查找代碼(例如調用JNDI InitialContext)

3、可測試性

當依賴項通過構造函數或setter公開時,可以很容易地替換

更容易的測試可以帶來更多的測試,更多的測試會帶來更好的代碼品質、更低的耦合、更高的内聚性

缺點

第一、軟體系統中由于引入了第三方IOC容器,生成對象的步驟變得有些複雜,本來是兩者之間的事情,又憑空多出一道手續,是以,我們在剛開始使用IOC架構的時候,會感覺系統變得不太直覺。是以,引入了一個全新的架構,就會增加團隊成員學習和認識的教育訓練成本,并且在以後的運作維護中,還得讓新加入者具備同樣的知識體系。

第二、由于IOC容器生成對象是通過反射方式,在運作效率上有一定的損耗。如果你要追求運作效率的話,就必須對此進行權衡。

第三、具體到IOC架構産品(比如:Spring)來講,需要進行大量的配制工作,比較繁瑣,對于一些小的項目而言,客觀上也可能加大一些工作成本。

第四、IOC架構産品本身的成熟度需要進行評估,如果引入一個不成熟的IOC架構産品,那麼會影響到整個項目,是以這也是一個隐性的風險。

我們大體可以得出這樣的結論:一些工作量不大的項目或者産品,不太适合使用IOC架構産品。另外,如果團隊成員的知識能力欠缺,對于IOC架構産品缺乏深入的了解,也不要貿然引入。最後,特别強調運作效率的項目或者産品,也不太适合引入IOC架構産品,像WEB2.0網站就是這種情況。

參考:

https://www.cnblogs.com/DebugLZQ/archive/2013/06/05/3107957.html

https://www.jianshu.com/p/17b66e6390fd

https://segmentfault.com/a/1190000014803412

https://segmentfault.com/a/1190000014719665

繼續閱讀