前言
對於大部分小菜來說,當聽到大牛們高談DIP、IoC、DI以及IoC容器等名詞時,有沒有瞬間石化的感覺?其實,這些“高大上”的名詞,理解起來也並不是那么的難,關鍵在於入門。只要我們入門了,然后循序漸進,假以時日,自然水到渠成。
好吧,我們先初略了解一下這些概念。
依賴倒置原則(DIP):一種軟件架構設計的原則(抽象概念)。
控制反轉(IoC):一種反轉流、依賴和接口的方式(DIP的具體實現方式)。
依賴注入(DI):IoC的一種實現方式,用來反轉依賴(IoC的具體實現方式)。
IoC容器:依賴注入的框架,用來映射依賴,管理對象創建和生存周期(DI框架)。
哦!也許你正為這些陌生的概念而傷透腦筋。不過沒關系,接下來我將為你一一道破這其中的玄機。
依賴倒置原則(DIP)
在講概念之前,我們先看生活中的一個例子。
相信大部分取過錢的朋友都深有感觸,只要有一張卡,隨便到哪一家銀行的ATM都能取錢。在這個場景中,ATM相當於高層模塊,而銀行卡相當於低層模塊。ATM定義了一個插口(接口),供所有的銀行卡插入使用。也就是說,ATM不依賴於具體的哪種銀行卡。它只需定義好銀行卡的規格參數(接口),所有實現了這種規格參數的銀行卡都能在ATM上使用。現實生活如此,軟件開發更是如此。依賴倒置原則,它轉換了依賴,高層模塊不依賴於低層模塊的實現,而低層模塊依賴於高層模塊定義的接口。通俗的講,就是高層模塊定義接口,低層模塊負責實現(抽象不不應該依賴於實現,實現應該依賴於抽象)
如果生活中的實例不足以說明依賴倒置原則的重要性,那下面我們將通過軟件開發的場景來理解為什么要使用依賴倒置原則。
場景一 依賴無倒置(低層模塊定義接口,高層模塊負責實現)
從上圖中,我們發現高層模塊的類依賴於低層模塊的接口。因此,低層模塊需要考慮到所有的接口。如果有新的低層模塊類出現時,高層模塊需要修改代碼,來實現新的低層模塊的接口。這樣,就破壞了開放封閉原則。
場景二 依賴倒置(高層模塊定義接口,低層模塊負責實現)
在這個圖中,我們發現高層模塊定義了接口,將不再直接依賴於低層模塊,低層模塊負責實現高層模塊定義的接口。這樣,當有新的低層模塊實現時,不需要修改高層模塊的代碼。
控制反轉(IoC)
DIP是一種 軟件設計原則,它僅僅告訴你兩個模塊之間應該如何依賴,但是它並沒有告訴如何做。IoC則是一種 軟件設計模式,它告訴你應該如何做,來解除相互依賴模塊的耦合。控制反轉(IoC),它為相互依賴的組件提供抽象,將依賴(低層模塊)對象的獲得交給第三方(系統)來控制,即依賴對象不在被依賴模塊的類中直接通過new來獲取。在例子我們可以看到,ATM它自身並沒有插入具體的銀行卡(工行卡、農行卡等等),而是將插卡工作交給人來控制,即我們來決定將插入什么樣的銀行卡來取錢。同樣我們也通過軟件開發過程中場景來加深理解。
做過電商網站的朋友都會面臨這樣一個問題:訂單入庫。假設系統設計初期,用的是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(); } }
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類使用呢?
方法一 構造函數注入
構造函數函數注入,毫無疑問通過構造函數傳遞依賴。因此,構造函數的參數必然用來接收一個依賴對象。那么參數的類型是什么呢?具體依賴對象的類型?還是一個抽象類型?根據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,我們再來編寫一個控制台程序。
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數據庫中添加一條記錄!"); } }
然后在控制台程序中重新綁定依賴關系:
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(); } }
然后在控制台程序中,給屬性賦值,從而傳遞依賴:
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方法傳遞依賴:
class Program { static void Main(string[] args) { AccessDal dal = new AccessDal();//在外部創建依賴對象 Order order = new Order(); order.SetDependence(dal);//傳遞依賴 order.Add(); Console.Read(); } }
我們同樣能得到上述的輸出結果。
雖說上述三種注入形式避免了修改Order類,但仍然避免不了修改控制台輸出的方法。這樣來看,減輕了order類的負擔,但控制台輸出端就必須判斷使用的數據庫,因此:上述三種注入形式的代碼違背了設計模式的開閉原則,仍然有不小的瑕疵,如果解決這個問題呢?
IoC容器
前面所有的例子中,我們都是通過手動的方式來創建依賴對象,並將引用傳遞給被依賴模塊。比如:
SqlServerDal dal = new SqlServerDal();//在外部創建依賴對象
Order order = new Order(dal);//通過構造函數注入依賴
對於大型項目來說,相互依賴的組件比較多。如果還用手動的方式,自己來創建和注入依賴的話,顯然效率很低,而且往往還會出現不可控的場面。正因如此,IoC容器誕生了。IoC容器實際上是一個DI框架,它能簡化我們的工作量。目前,比較流行的Ioc容器有以下幾種:Ninject、Castle Windsor、Autofac、StructureMap、Unity、Spring.NET、LightInject 等