面向對象設計(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的優點:
系統更柔韌:可以修改一部分代碼而不影響其他子產品。
系統更健壯:可以修改一部分代碼而不會讓系統崩潰。
系統更高效:元件松耦合,且可複用,提高開發效率。
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),并學會使用。
控制反轉(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();
}
}
}
我們同樣能得到上述的輸出結果。
前面所有的例子中,我們都是通過手動的方式來建立依賴對象,并将引用傳遞給被依賴子產品。比如:
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構造函注入的架構,它管理着依賴項的生命周期以及映射關系。
我叫劉皓,很高興您能閱讀完我的這篇文章。
我花了大量時間和精力來完成這篇文章,如果文章對您有幫助,請不要忘了點推薦哦!
如果您能點選右邊的打賞按鈕,打賞一杯咖啡錢,我将獲得更多的動力和能量寫出下一篇好文章。
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連結,否則保留追究法律責任的權利。
我的部落格即将搬運同步至騰訊雲+社群,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan