深入理解DIP、IoC、DI以及IoC容器

可米小子發表於2016-05-30

摘要

物件導向設計(OOD)有助於我們開發出高效能、易擴充套件以及易複用的程式。其中,OOD有一個重要的思想那就是依賴倒置原則(DIP),並由此引申出IoC、DI以及Ioc容器等概念。通過本文我們將一起學習這些概念,並理清他們之間微妙的關係。

目錄

  • 前言
  • 依賴倒置原則(DIP)
  • 控制反轉(IoC)
  • 依賴注入(DI)
  • IoC容器
  • 總結

前言

對於大部分小菜來說,當聽到大牛們高談DIP、IoC、DI以及IoC容器等名詞時,有沒有瞬間石化的感覺?其實,這些“高大上”的名詞,理解起來也並不是那麼的難,關鍵在於入門。只要我們入門了,然後循序漸進,假以時日,自然水到渠成。

好吧,我們先初略瞭解一下這些概念。

依賴倒置原則(DIP):一種軟體架構設計的原則(抽象概念)。

控制反轉(IoC):一種反轉流、依賴和介面的方式(DIP的具體實現方式)。

依賴注入(DI):IoC的一種實現方式,用來反轉依賴(IoC的具體實現方式)。

IoC容器:依賴注入的框架,用來對映依賴,管理物件建立和生存週期(DI框架)。

哦!也許你正為這些陌生的概念而傷透腦筋。不過沒關係,接下來我將為你一一道破這其中的玄機。

依賴倒置原則(DIP)

在講概念之前,我們先看生活中的一個例子。

圖1   ATM與銀行卡

相信大部分取過錢的朋友都深有感觸,只要有一張卡,隨便到哪一家銀行的ATM都能取錢。在這個場景中,ATM相當於高層模組,而銀行卡相當於低層模組。ATM定義了一個插口(介面),供所有的銀行卡插入使用。也就是說,ATM不依賴於具體的哪種銀行卡。它只需定義好銀行卡的規格引數(介面),所有實現了這種規格引數的銀行卡都能在ATM上使用。現實生活如此,軟體開發更是如此。依賴倒置原則,它轉換了依賴,高層模組不依賴於低層模組的實現,而低層模組依賴於高層模組定義的介面。通俗的講,就是高層模組定義介面,低層模組負責實現。

Bob Martins對DIP的定義:

高層模組不應依賴於低層模組,兩者應該依賴於抽象。

抽象不不應該依賴於實現,實現應該依賴於抽象。

如果生活中的例項不足以說明依賴倒置原則的重要性,那下面我們將通過軟體開發的場景來理解為什麼要使用依賴倒置原則。

場景一  依賴無倒置(低層模組定義介面,高層模組負責實現)

從上圖中,我們發現高層模組的類依賴於低層模組的介面。因此,低層模組需要考慮到所有的介面。如果有新的低層模組類出現時,高層模組需要修改程式碼,來實現新的低層模組的介面。這樣,就破壞了開放封閉原則。

場景二 依賴倒置(高層模組定義介面,低層模組負責實現)

在這個圖中,我們發現高層模組定義了介面,將不再直接依賴於低層模組,低層模組負責實現高層模組定義的介面。這樣,當有新的低層模組實現時,不需要修改高層模組的程式碼。

由此,我們可以總結出使用DIP的優點:

系統更柔韌:可以修改一部分程式碼而不影響其他模組。

系統更健壯:可以修改一部分程式碼而不會讓系統崩潰。

系統更高效:元件鬆耦合,且可複用,提高開發效率。

控制反轉(IoC)

DIP是一種 軟體設計原則,它僅僅告訴你兩個模組之間應該如何依賴,但是它並沒有告訴如何做。IoC則是一種 軟體設計模式,它告訴你應該如何做,來解除相互依賴模組的耦合。控制反轉(IoC),它為相互依賴的元件提供抽象,將依賴(低層模組)物件的獲得交給第三方(系統)來控制,即依賴物件不在被依賴模組的類中直接通過new來獲取。在圖1的例子我們可以看到,ATM它自身並沒有插入具體的銀行卡(工行卡、農行卡等等),而是將插卡工作交給人來控制,即我們來決定將插入什麼樣的銀行卡來取錢。同樣我們也通過軟體開發過程中場景來加深理解。

軟體設計原則:原則為我們提供指南,它告訴我們什麼是對的,什麼是錯的。它不會告訴我們如何解決問題。它僅僅給出一些準則,以便我們可以設計好的軟體,避免不良的設計。一些常見的原則,比如DRY、OCP、DIP等。

軟體設計模式:模式是在軟體開發過程中總結得出的一些可重用的解決方案,它能解決一些實際的問題。一些常見的模式,比如工廠模式、單例模式等等。

做過電商網站的朋友都會面臨這樣一個問題:訂單入庫。假設系統設計初期,用的是SQL Server資料庫。通常我們會定義一個SqlServerDal類,用於資料庫的讀寫。

public class SqlServerDal
{
     public void Add()
    {
        Console.WriteLine("在資料庫中新增一條訂單!");
    }
}

然後我們定義一個Order類,負責訂單的邏輯處理。由於訂單要入庫,需要依賴於資料庫的操作。因此在Order類中,我們需要定義SqlServerDal類的變數並初始化。

public class Order
{
        private readonly SqlServerDal dal = new SqlServerDal();//新增一個私有變數儲存資料庫操作的物件

         public void Add()
       {
           dal.Add();
       }
}

最後,我們寫一個控制檯程式來檢驗成果。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DIPTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Order order = new Order();
            order.Add();

            Console.Read();
        }
    }
}

輸出結果:

OK,結果看起來挺不錯的!正當你沾沾自喜的時候,這時BOSS過來了。“小劉啊,剛客戶那邊打電話過來說資料庫要改成Access”,“對你來說,應當小CASE啦!”BOSS又補充道。帶著自豪而又糾結的情緒,我們思考著修改程式碼的思路。

由於換成了Access資料庫,SqlServerDal類肯定用不了了。因此,我們需要新定義一個AccessDal類,負責Access資料庫的操作。

public class AccessDal
{
    public void Add()
   {
       Console.WriteLine("在ACCESS資料庫中新增一條記錄!");
   }
}

然後,再看Order類中的程式碼。由於,Order類中直接引用了SqlServerDal類的物件。所以還需要修改引用,換成AccessDal物件。

public class Order
{
        private readonly AccessDal dal = new AccessDal();//新增一個私有變數儲存資料庫操作的物件

         public void Add()
       {
           dal.Add();
       }
}

輸出結果:

費了九牛二虎之力,程式終於跑起來了!試想一下,如果下次客戶要換成MySql資料庫,那我們是不是還得重新修改程式碼?

顯然,這不是一個良好的設計,元件之間高度耦合,可擴充套件性較差,它違背了DIP原則。高層模組Order類不應該依賴於低層模組SqlServerDal,AccessDal,兩者應該依賴於抽象。那麼我們是否可以通過IoC來優化程式碼呢?答案是肯定的。IoC有2種常見的實現方式:依賴注入和服務定位。其中,依賴注入使用最為廣泛。下面我們將深入理解依賴注入(DI),並學會使用。

依賴注入(DI)

控制反轉(IoC)一種重要的方式,就是將依賴物件的建立和繫結轉移到被依賴物件類的外部來實現。在上述的例項中,Order類所依賴的物件SqlServerDal的建立和繫結是在Order類內部進行的。事實證明,這種方法並不可取。既然,不能在Order類內部直接繫結依賴關係,那麼如何將SqlServerDal物件的引用傳遞給Order類使用呢?

依賴注入(DI),它提供一種機制,將需要依賴(低層模組)物件的引用傳遞給被依賴(高層模組)物件。通過DI,我們可以在Order類的外部將SqlServerDal物件的引用傳遞給Order類物件。那麼具體是如何實現呢?

方法一 建構函式注入

建構函式函式注入,毫無疑問通過建構函式傳遞依賴。因此,建構函式的引數必然用來接收一個依賴物件。那麼引數的型別是什麼呢?具體依賴物件的型別?還是一個抽象型別?根據DIP原則,我們知道高層模組不應該依賴於低層模組,兩者應該依賴於抽象。那麼建構函式的引數應該是一個抽象型別。我們再回到上面那個問題,如何將SqlServerDal物件的引用傳遞給Order類使用呢?

首選,我們需要定義SqlServerDal的抽象型別IDataAccess,並在IDataAccess介面中宣告一個Add方法。

public interface IDataAccess
{
        void Add();
}

然後在SqlServerDal類中,實現IDataAccess介面。

 public class SqlServerDal:IDataAccess
 {
        public void Add()
        {
            Console.WriteLine("在資料庫中新增一條訂單!");
        }
 }

接下來,我們還需要修改Order類。

  public class Order
  {
        private IDataAccess _ida;//定義一個私有變數儲存抽象

        //建構函式注入
        public Order(IDataAccess ida)
        {
            _ida = ida;//傳遞依賴
      }

        public void Add()
        {
            _ida.Add();
        }
}

OK,我們再來編寫一個控制檯程式。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DIPTest
{
    class Program
    {
        static void Main(string[] args)
        {
            SqlServerDal dal = new SqlServerDal();//在外部建立依賴物件
            Order order = new Order(dal);//通過建構函式注入依賴

            order.Add();

            Console.Read();
        }
    }
}

輸出結果:

從上面我們可以看出,我們將依賴物件SqlServerDal物件的建立和繫結轉移到Order類外部來實現,這樣就解除了SqlServerDal和Order類的耦合關係。當我們資料庫換成Access資料庫時,只需定義一個AccessDal類,然後外部重新繫結依賴,不需要修改Order類內部程式碼,則可實現Access資料庫的操作。

定義AccessDal類:

public class AccessDal:IDataAccess
{
        public void Add()
        {
            Console.WriteLine("在ACCESS資料庫中新增一條記錄!");
        }
}

然後在控制檯程式中重新繫結依賴關係:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DIPTest
{
    class Program
    {
        static void Main(string[] args)
        {
             AccessDal dal = new AccessDal();//在外部建立依賴物件
               Order order = new Order(dal);//通過建構函式注入依賴

               order.Add();

            Console.Read();
        }
    }
}

輸出結果:

顯然,我們不需要修改Order類的程式碼,就完成了Access資料庫的移植,這無疑體現了IoC的精妙。

方法二 屬性注入

顧名思義,屬性注入是通過屬性來傳遞依賴。因此,我們首先需要在依賴類Order中定義一個屬性:

 public class Order
 {
       private IDataAccess _ida;//定義一個私有變數儲存抽象

         //屬性,接受依賴
         public IDataAccess Ida
        {
            set { _ida = value; }
            get { return _ida; }
        }

        public void Add()
        {
            _ida.Add();
        }
 }

然後在控制檯程式中,給屬性賦值,從而傳遞依賴:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DIPTest
{
    class Program
    {
        static void Main(string[] args)
        {
            AccessDal dal = new AccessDal();//在外部建立依賴物件
            Order order = new Order();
            order.Ida = dal;//給屬性賦值

            order.Add();

            Console.Read();
        }
    }
}

我們可以得到上述同樣的結果。

方法三 介面注入

相比建構函式注入和屬性注入,介面注入顯得有些複雜,使用也不常見。具體思路是先定義一個介面,包含一個設定依賴的方法。然後依賴類,繼承並實現這個介面。

首先定義一個介面:

 public interface IDependent
 {
            void SetDependence(IDataAccess ida);//設定依賴項
 }

依賴類實現這個介面:

   public class Order : IDependent
    {
        private IDataAccess _ida;//定義一個私有變數儲存抽象

        //實現介面
        public void SetDependence(IDataAccess ida)
        {
            _ida = ida;
        }

        public void Add()
        {
            _ida.Add();
        }

    }

控制檯程式通過SetDependence方法傳遞依賴:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DIPTest
{
    class Program
    {
        static void Main(string[] args)
        {
            AccessDal dal = new AccessDal();//在外部建立依賴物件
          Order order = new Order();

            order.SetDependence(dal);//傳遞依賴

            order.Add();

            Console.Read();
        }
    }
}

我們同樣能得到上述的輸出結果。

IoC容器

前面所有的例子中,我們都是通過手動的方式來建立依賴物件,並將引用傳遞給被依賴模組。比如:

SqlServerDal dal = new SqlServerDal();//在外部建立依賴物件 
Order order = new Order(dal);//通過建構函式注入依賴

對於大型專案來說,相互依賴的元件比較多。如果還用手動的方式,自己來建立和注入依賴的話,顯然效率很低,而且往往還會出現不可控的場面。正因如此,IoC容器誕生了。IoC容器實際上是一個DI框架,它能簡化我們的工作量。它包含以下幾個功能:

  • 動態建立、注入依賴物件。
  • 管理物件生命週期。
  • 對映依賴關係。

目前,比較流行的Ioc容器有以下幾種:

1. Ninject:  http://www.ninject.org/

2. Castle Windsor:  http://www.castleproject.org/container/index.html

3. Autofac:  http://code.google.com/p/autofac/

4. StructureMap: http://docs.structuremap.net/

5. Unity:  http://unity.codeplex.com/

注:根據園友 徐少俠 的提醒,MEF不應該是IoC容器。我又查閱了一些資料,覺得MEF作為IoC容器是有點勉強,它的主要作用還是用於應用程式擴充套件,避免生成脆弱的硬依賴項。

6. MEF:  http://msdn.microsoft.com/zh-cn/library/dd460648.aspx

另外,園友 aixuexi 提出Spring.NET也是比較流行的IoC容器。

7. Spring.NET: http://www.springframework.net/

園友 wdwwtzy 也推薦了一個不錯的IoC容器:

8. LightInject:  http://www.lightinject.net/ (推薦使用Chrome瀏覽器訪問)

以Ninject為例,我們同樣來實現 [方法一 建構函式注入] 的功能。

首先在專案新增Ninject程式集,同時使用using指令引入。

using Ninject;

然後,Ioc容器註冊繫結依賴:

StandardKernel kernel = new StandardKernel();

kernel.Bind<IDataAccess>().To<SqlServerDal>();//註冊依賴

接下來,我們獲取需要的Order物件(注入了依賴物件):

Order order = kernel.Get<Order>();

下面,我們寫一個完整的控制檯程式

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Ninject;

namespace DIPTest
{
    class Program
    {
        static void Main(string[] args)
        {
           StandardKernel kernel = new StandardKernel();//建立Ioc容器
           kernel.Bind<IDataAccess>().To<SqlServerDal>();//註冊依賴

             Order order = kernel.Get<Order>();//獲取目標物件

             order.Add();
           Console.Read();
        }
    }
}

輸出結果:

使用IoC容器,我們同樣實現了該功能。

總結

在本文中,我試圖以最通俗的方式講解,希望能幫助大家理解這些概念。下面我們一起來總結一下:DIP是軟體設計的一種思想,IoC則是基於DIP衍生出的一種軟體設計模式。DI是IoC的具體實現方式之一,使用最為廣泛。IoC容器是DI構造函注入的框架,它管理著依賴項的生命週期以及對映關係。

相關文章