文章目錄
- 1.1 依賴關係
- 1.2 面向介面編程
- 2.2 IoC容器依賴
目錄
1. IoC使用簡介與原理
1.1 依賴關係
1.2 面向介面編程
1.3 IoC使用與實現原理
2. 模式與經驗
2.1 code configuration vs. xml configuration
2.2 IoC容器依賴
1. IoC使用簡介與原理
1.1 依賴關係
在物件導向編程中,類與類之間總是要有一些依賴關係,有些依賴強一些,有些依賴弱一些,常見的依賴關係有:
o 繼承依賴 - 子類依賴於父類
o 實現依賴 - 類依賴於介面
o 參數依賴 - 當類的一個方法有其它類型的參數時
o 其它 - 當類中的欄位為其它類型,或者代碼中有對其它類型的使用。
其中參數依賴是相對較弱的依賴關係,而繼承依賴則是較強的依賴關係。判斷兩個類是否有依賴關係有一個很簡單的辦法,比如想知道類A是否依賴於類B,只要把B刪掉然後編譯一下,如果編譯錯誤就證明A依賴於B -- 當然,刪掉B之前代碼應該是編譯正確的。
1.2 面向介面編程
看一下這段代碼:
Code
public class A
{
private B b;
public A(B b)
{
this.b = b;
}
public void Do()
{
b.DoSomething();
}
}
public class B
{
public void DoSomething()
{
//do something
}
}
其中,A對B有依賴關係,因為B是一個具體類,如果需要添加或改變B的行為則需要直接改變B的代碼,同時B和A都需要重新編譯,這就違反了開放封閉原則。我們可以修改一下代碼,讓A依賴於一個介面IFoo,而B則實現這個介面。這樣A與B就沒有依賴關係了,他們都只依賴於IFoo。這時只需要添加一個實現IFoo的類,再把類的執行個體傳給A就可以改變IFoo的實現和行為。代碼如下:
Code
public class A
{
private IFoo foo;
public A(IFoo foo)
{
this.foo = foo;
}
public void Do()
{
foo.DoSomething();
}
}
public interface IFoo
{
void DoSomething();
}
public class B : IFoo
{
public void DoSomething()
{
//do something
}
}1.3 IoC使用與實現原理
當我們需要使用A的時候,就需要執行個體化A,最簡單的方式是直接用new
A a = new A(new B());
另一種方式是使用IoC
IUnityContainer myContainer = new UnityContainer();
myContainer.RegisterType<IFoo, B>();
IFoo foo = myContainer.Resolve<IFoo>();
A a = new A(foo);
這裡調用了Unity的兩個主要方法,一個是RegisterType<IService, Service>,用來註冊一個服務和他的實現;另一個是Resolve<IService>,用來得到實現IService的類的執行個體。當我調用myContainer.Resolve<IFoo>()的時候,Unity會發現IFoo是一個介面,那麼Unity就會尋找他的實作類別,也就是B因為我們已經註冊了,接著Unity會使用類似Activator.CreateInstance這樣的方法來執行個體化B,並返回這個執行個體。
由於IoC一般都支援嵌套的依賴關係,因此我們完全不必自己來執行個體化B,可以由容器為我們自動解決。
IUnityContainer myContainer = new UnityContainer();
myContainer.RegisterType<IFoo, B>();
A a = myContainer.Resolve<A>();
在這裡,我們直接向容器請求A的執行個體。大概的步驟如下:
1. 由於A是一個具體類,因此不需要尋找他的實作類別,直接執行個體化A就可以了
2. 查看A的依賴,發現建構函式中有對IFoo的依賴,因此執行個體化A之前,需要先執行個體化IFoo
3. 由於IFoo是一個介面,因此需要尋找他的實作類別,由於我們註冊了B為IFoo的實作類別,因此會執行個體化B
4. 由於B是一個具體類,而且沒有對其它類的依賴關係,因此直接執行個體化B
5. A的所有依賴都得到了執行個體化,將B的執行個體傳給A的建構函式來執行個體化A
6. 返回A的執行個體
可以看到,IoC的執行個體化過程是一個遞迴的過程。每個類所依賴的類或介面都需要首先執行個體化,而這些類或介面又有可能依賴其它的類或介面,這樣一直遞迴下去。假設有一個類C,依賴類D,而類D又依賴於E……最後Y依賴於Z。那麼如果我向IoC容器要一個C的執行個體,IoC會先執行個體化Z,然後是Y,然後是X……E,D最後才是C。
由於IoC會自動解析介面與實作類別的關係,並執行個體化正確的類,因此程式的代碼完全不需要考慮介面的執行個體化問題,可以完全面向介面。而在IoC中註冊類時,也可以通過註冊不同的類,改變介面的行為。
例如,如果我們還有一個IFoo的實作類別C。我們想用C來代替當前的IFoo實現B,只需要修改一行代碼
myContainer.RegisterType<IFoo, B>();
替換成
myContainer.RegisterType<IFoo, C>();
這樣,A就會使用C的實現了。
除了建構函式依賴,Unity也支援其它的依賴,詳細的API和說明請參考文檔。
PS: 我認為Unity有一個比Castle好的特性,就是執行個體化一個具體類時,不需要註冊。如果用Castle的話,上面的樣本還要加上類似於RegisterType<B>()這樣的代碼。
2.模式與經驗
2.1 code configuration vs. xml configuration
目前的IoC容器一般都會支援代碼配置和Xml配置兩種配置,代碼配置就是向前面的樣本那樣,在代碼中註冊服務介面與實現,而Xml配置則是在Xml或者.config檔案中配置。由於從.NET誕生的那一天起,.config檔案就是一個推薦的配置地點,因此很多人也喜歡將配置資訊放到.config檔案中去,然而代碼配置的方式也有其優勢,在這裡我將比較一下兩種方式的優劣。
在我看來,代碼配置的方式使用起來非常簡單,所需要的程式碼數比較少,而且有自動完成,強型別,編譯時間檢查。唯一的缺點是改變更配置置時,需要重新編譯代碼,而且必須有原始碼才能修改。而xml配置的方式稍微複雜,所需的行數要多一些,沒有編譯時間檢查,需要啟動並執行時候才能直到是否有錯。優點是改配置不需要原始碼,不需要重新編譯,甚至可以在運行時修改。這兩種配置方式的表達能力,在常用情況中幾乎是等價的(代碼配置好像要多一些)。
以我個人的經驗,推薦在開發時使用代碼配置,而在快發布時再改為xml配置。因為開發時,一切都是不確定的,代碼配置的方式修改比較簡單,而且還支援重構。想象一下,如果用xml配置,你用重構工具重新命名了一個介面名,還要到xml中手動修改,這是多大的工作量啊。而在臨近發布時,代碼趨於穩定,修改的可能性很低,這時再放到xml中就可以充分利用xml的好處了。
2.2 IoC容器依賴
我看到過有些人使用IoC的方式就是把new換成了Resolve而已,幾乎每個類中需要new一個執行個體的時候,都會調一下容器的API。雖然解除了程式中類與類之間的依賴,但是這些類卻都有對IoC容器的依賴。假如我現在使用的容器是Unity,而某一天我發現Unity滿足不了我的需要,我要換一個容器時,由於代碼中遍布著對Unity的直接依賴代碼,替換容器的代價就會非常大。
這裡有兩個解決辦法:
第一就是在程式碼和容器之間加一個層,你可以自己寫一個ServiceLocator之類的類,來封裝IoC容器。這樣程式碼對IoC容器的依賴就轉變成對ServiceLocator的依賴,如果想換一個容器的話,只要修改ServiceLocator的代碼,調用另一種容器的API就行了,減少了很多工作量。由於現在多數IoC的能力和API是比較相似的,因此CodePlex上有一個項目叫CommonServiceLocator,它提供了一個公用的介面,作為這個中介層。但是我本人不推薦使用這個庫,因為本身IoC的介面一點也不複雜,自己寫一個都比學習這個要簡單,還可以根據需要調整介面。
第二種辦法就是減少對IoC容器的依賴,只在最高層代碼或者初始化代碼中調用容器。通常在一個應用程式中,都有一個入口,一般來說是Program類的Main方法,如果你用的是Winform或者WPF這樣的架構,那麼可能會有一個Initialize方法或事件,用於初始化你自己的代碼。如果你的類層次設計良好,你會有一個最高層的類,這個類依賴於同層或底層的一些類,他依賴的類又依賴於更低層的類,而這個類本身卻不被任何類所依賴。那麼當你用容器建立這個類的執行個體時,相當於所有的類都會建立出來。還記得前面的遞迴建立嗎?這種方法適用於那些不需要消極式載入的程式,因為程式啟動時就會建立所有類執行個體。