1.概況說明
2.貓狗大戰舉例
3.說明為什麼要針對接口編程,優點
4.說明為什麼要“依賴抽象,不要依賴具體類”
5.說明“依賴倒置”與抽象工廠模式
6.說明“將組件的配置與使用分離”
7.簡單說明依賴注入
8.講解petshop依賴注入與它的工廠模式
9.講解TheBeerHouse依賴注入形式
10.幾個.Net的依賴注入容器
11.取捨與適用
概況說明
現在在各種技術站點、書籍文章上,都能看到IoC容器、控制反轉、依賴注入的字眼,而且還會有一些專門實現這些功能的開發工具:Spring.net、 Castal Windsor、Unity等等。那麼這種技術是如何演變而來的?它的適用場景是哪裡?我們該不該學習並掌握這門技術?下面我會根據我個人的理解與搜集來的材料做出一些解釋。
貓狗大戰舉例
我現在要做一個貓狗大戰的游戲,系統內部采用了標准的OO技術,先設計了一個狗狗的抽象類。
public abstract class Dog
{
public void Run();
public void Jump();
public void Bay();
public abstract void Display();
}
假設游戲中每個狗狗跑的速度、跳的高度、叫的音量都是相同的,那麼唯一不同的就是外貌了,所以 Display()是抽象的。
public class Poodle:Dog
{
public override void Display()
{
//我是獅子狗
}
}
public class Dachshund:Dog
{
public override void Display()
{
//我是臘腸狗
}
}
//其他狗狗.
好了,現在我們想讓游戲中加入打斗的元素,狗狗會攻擊,可能你會想到只用在基類中加一個Attact()的方法,就可以這樣讓所有繼承它的狗狗就都會攻擊了。不過問題很快就來了,你會發現AIBO(日本產的電子機械寵物狗)包括家裡的絨毛玩具狗也會攻擊了,這是很不合情理的事情。所以並不是所有的狗狗都會攻擊這個行為,那麼有人肯定想到使用接口了,把Attact()這個方法提取到接口中,只讓會攻擊的狗狗實現這個接口就可以了。
public interface IAttact
{
void Attact();
}
public class Poodle:Dog,IAttact
{
public override void Display()
{
//我是獅子狗
}
public void Attact()
{
//咬一口
}
}
說明為什麼要針對接口編程,優點
這樣看起來很好,但是需求總是在變化的,現在的需求又增加了:要求每種狗狗的攻擊方式不同,有的會咬,有的會撲,有的甚至會獅子吼的功夫,當然如果狗狗升級了,還會出現更多的攻擊方式。上面這個方式的缺點就顯現出來了,代碼會在多個子類中重復,無法知道所有狗狗的全部行為,運行時不容易改變等等。
下面這樣做,我們把變化的部分提取出來,多種行為的實現用接口統一實現。
public class BiteAttact:IAttact
{
public void Attact()
{
//咬
}
}
public class SnapAttact:IAttact
{
public void Attact()
{
//撲咬
}
}
//其他實現
當我們想要增加一種行為方式時,只需繼承接口就可以,並不會改變其他行為。
public class Poodle:Dog
{
IAttact attact;
public Poodle()
{
attact=new BiteAttact();
}
//這裡我在調用的時候就可以動態的設定行為了
public void SetAttactBehavior(IAttact a)
{
attact=a;
}
public void PerformAttact()
{
attact.Attact();
}
public override void Display()
{
//我是獅子狗
}
}
這樣的設計就讓狗狗的攻擊行為有了彈性,我們可以動態的在運行時改變它的行為,也可以隨時在不影響其他類的情況下添加更改行為。而以往的做法是行為來自 Dog基類或者繼承接口並由子類自行實現,這兩種方法都依賴於“實現”,我們被邦的死死的,而無法更改行為。而接口所表示的行為實現,不會被綁死在Dog 類與子類中。這就是設計模式中的一個原則:針對接口編程,不要針對實現編程。
說明為什麼要“依賴抽象,不要依賴具體類”
但是,當我們使用“new”的時候,就已經在實例化一個具體類了。這就是一種實現,只要有具體類的出現,就會導致代碼更缺乏彈性。就好比雕塑家做好了一個“沉思者”,想把它再改造成“維納斯”,難度可想而知。
我們再回到貓狗大戰這個游戲,為了增加趣味性,我們增加了貓狗交互的功能,如果你選擇了狗狗開始游戲,那麼會隨機不同的場景,在固定的場景會遇到固定的貓。例如:在峭壁背景會遇到山貓,在象牙塔背景中會遇到波斯貓,在草原中會遇到雲貓等。
Cat cat;
if(mountain){
cat=new Catamountain();
}else if(ivory){
cat=new PersianCat();
}else if(veldt){
cat=new CloudCat();
}
但是有時真正要實例化哪些類,要在運行時有一些條件決定。當一旦有變化或擴展時,就要重新打開這段代碼進行修改,這就違反了我們“對修改關閉”的原則。這段代碼的依賴特別緊密,而且是高層依賴於低層(客戶端依賴具體類的實現)。不過慶幸的是,我們有“依賴倒轉原則”與“抽象工廠模式”來拯救我們的代碼。
說明“依賴倒置”與抽象工廠模式
依賴倒轉原則是要依賴抽象,不要依賴具體類。也就是說客戶端(高層組件)要依賴於抽象(Cat),各種貓咪(低層組件)也要依賴抽象(Cat)。雖然我們已經創造了一個抽象Cat,但我們仍然在代碼中實際地創建了具體的Cat,這個抽象並沒有什麼影響力,那麼我們如何將這些實例化對象的代碼獨立出來?工廠模式正好派上用場。工廠模式屬於創建型模式,它能將對象的創建集中在一起進行處理。
相反如果你選擇了貓咪角色,就會在不同的場景遇到特定的狗狗NPC。
現在我們要創建一個工廠的接口:
public interface Factory
{
Cat CreateCat();
Dog CreateDog();
}
public class MountainSceneFactory:Factory
{
public Cat CreateCat(){
return new Catamountain();
}
public Dog CreateDog(){
return new CragDog();
}
}
public class VeldtSceneFactory:Factory
{
public Cat CreateCat(){
return new CloudCat();
}
public Dog CreateDog(){
return new VeldtDog();
}
}
然後構建一個草原的場景:
public class VeldtScene : Scene
{
Factory factory;
private static Cat cat=null;
private static Dog dog=null;
public VeldtScene(Factory f)
{
factory=f;
}
Public void prepare()
{
if(User.Identity=="Dog")
dog=factory.CreateDog();
else(user.Identity=="Cat")
cat=factory.CreateCat();
}
}
這樣一來,場景的條件不由代碼來改變,而可以由客戶端來動態改變,來看看我們的客戶端吧!
Factory factory=new VeldtSceneFactory();
Scene scene=new VeldtScene(factory);
Scene.prepare();
這樣如果你的角色是一只狗狗的話,就能在這個草原上見到一只雲貓了。工廠模式將實例解耦出來,替換不同的工廠以取得不同的行為。
說明“將組件的配置與使用分離”
事物總是在發展,需求總是在增加。貓狗大戰要升級為網絡版,我們希望由開發人員開發游戲,而由技術支持人員做游戲的安裝配置。一般開發者會提供配置文件交給技術支持人員,由他們來動態的為游戲更改配置,例如在草原上出現了波斯貓,老虎卻出現在客廳裡。技術支持人員是無法修改源代碼的,但可以讓他們修改配置文件以來改變實例的創建。
我們的設計離不開一個基本原則--分離接口與實現。在面向對象程序裡,我們在一個地方用條件邏輯來決定具體實例化哪一個類,以後的條件分支都由多態來實現,而不是繼續重復前面的條件邏輯。當我們決定將“選擇具體實現類”的據側推遲到部署階段,則在裝配原則上是要與應用程序的其余部分分開的,這樣我就可以輕松的針對不同的部署替換不同的配置。
簡單說明依賴注入
終於可以進入本文的重點了。現在我們的目標就是要把組件的配置與使用分離開。IoC(Inversion of Control控制反轉)容器因此應運而生,martin在他的大作中將此更形象的稱謂依賴注入(Dependency Injection)。
依賴注入的基本思想是:用一個單獨的對象獲得接口的一個合適的實現,並將其實例賦給調用者的一個字段。具體的依賴注入的講解可以看Martin Fowler的文章,不再詳述。我主要以實例的形式,來更好的理解依賴注入的特點與其所帶來的好處。
講解petshop依賴注入與它的工廠模式
PetShop4.0 是一個微軟發布的開源Web應用程序,它的經典的三層架構已經成了學習架構模式的必修課程,它提供了針對SqlServer與Oracle的數據訪問層以方便不同的使用者在這兩種數據庫環境中部署應用程序,部署者只用簡單的在配置文件中修改,就可以輕松的在兩種環境中進行切換,這一切都得住於依賴注入。
抽象出來的IDAL模塊脫離了與具體數據庫的依賴,從而使整個數據訪問層利於數據庫的遷移。DALFactory模塊專門管理DAL對象的創建,以便於 BLL的訪問,SQLServerDAL和OracleDAL模塊均實現了IDAL的接口,其中包含的邏輯就是對數據庫的CRUD操作,因數據庫不同,代碼也會有所不同。當創建SQLServer的Order對象時:
PetShopFactory factory=new SQLServerFactory();
IOrder=factory.CreateOrder();
這裡new出來一個具體實現類SQLServerFactory一旦要更換Oracle,不可避免要修改代碼,但若用了依賴注入,一切問題迎刃而解。PetShop的Web.config文件中:
<appSettings>
<add key="WebDAL" value="PetShop.SQLServerDAL"/>
</appSettings>
在DALFactory中的DataAccess中,首先讀出Web.config的配置,然後運用反射技術創建對象實例:
private static readonly string path = ConfigurationManager.AppSettings["WebDAL"];
public static PetShop.IDAL.ICategory CreateCategory() {
string className = path + ".Category";
return (PetShop.IDAL.ICategory)Assembly.Load(path).CreateInstance(className);
}
BLL層調用的話,只需把創建的實力賦值給一個私有的字段:
private static readonly ICategory dal = PetShop.DALFactory.DataAccess.CreateCategory();
實例的創建只是通過反射將配置注入進程序中,若要更改為Oracle,僅需把value="PetShop.SQLServerDAL"更改為value="PetShop.OracleDAL"!
但是不要以為我們因此可以按照這個模式將所有的程序更改為這種類型的,PetShop是共享程序。部署環境因機而異,依賴注入在這裡很有必要性。若我們的應用程序的部署環境永遠不會改變的話,完全沒有必要應用依賴注入改變數據庫,相反還會增加程序復雜度和因反射帶來的20%性能損失。
講解TheBeerHouse依賴注入形式
接著再來看看另一個依賴注入的實現方式。TheBeerHouse是最先發布在微軟Startkits軟件發布區的,國內有它的講解書籍,開發者與作者是同一個人。TBH為了實現數據庫環境的移植,采用了與PetShop截然不同的依賴注入形式,首先它在配置文件中自定義配置,將各個模塊的配置參數都寫在了裡面,然後ConfigSection中對所有配置寫了默認配置並可對這些配置讀寫。如在Articles中:
[ConfigurationProperty("providerType", DefaultValue = "MB.TheBeerHouse.DAL.SqlClient.SqlArticlesProvider")]
public string ProviderType
{
get { return (string)base["providerType"]; }
set { base["providerType"] = value; }
}
它的默認提供程序即為MB.TheBeerHouse.DAL.SqlClient.SqlArticlesProvider。
這裡沒有接口的實現,而是以繼承的方式。DataAccess基類其實相當於一個簡單的SQLHelper,定義了一些如連接符屬性和Execute方法。幾大模塊的Provider繼承了這個DataAccess,寫了一些需要繼承的CRUD方法,最重要的一點是應用單件模式與依賴注入創建並返回了此 Provider的實例。
static private ArticlesProvider _instance = null;
static public ArticlesProvider Instance
{
get
{
if (_instance == null)
_instance = (ArticlesProvider)Activator.CreateInstance(
Type.GetType(Globals.Settings.Articles.ProviderType));
return _instance;
}
}
作者或許也認為這些實例的創建分散於各大Provider,不易調用,於是又寫了一個SiteProvider:
public static class SiteProvider
{
public static ArticlesProvider Articles
{
get { return ArticlesProvider.Instance; }
}
//.
}
這其實正是一個簡單工廠方法!用屬性的方式是為了更方便方法的調用。
BLL調用Articles的方法時,只需直接調用SiteProvider.Articles.GetArticleCount();
更換Oracle,寫相應的類繼承這幾大模塊的Provider,並設置配置providerType為相應的提供程序。
相對來說,PetShop依賴注入的形式更簡潔明了,而TBH采用了自定義配置而更顯得靈活多變。
幾個.Net的依賴注入容器
既然有依賴注入模式的出現,就會有針對它而發展的框架。IoC容器最先是在Java陣營中出現並發展的,.Net陣營中實現依賴注入的框架作為後起之秀,也出現了Spring.Net,Castal Windsor,Unity優秀的依賴注入框架。我們可以不去掌握這些框架的使用,但學習他們可以使我們的編碼更標准更快捷,也令我們對依賴注入的理解更透徹深入。
Spring.Net是由Java火的不能再火的的Spring演變過來的,它是一個關注於.NET企業應用開發的應用程序框架。它能夠提供寬廣范圍的功能,例如依賴注入、面向方面編程(AOP)、數據訪問抽象, 以及ASP.NET集成等。現在相當多的人用Spring.Net+NHibernate這兩個開源框架做開發,倒是也珠聯璧合相輔相成。但是Spring.Net貫徹的思想是一切皆配置,從頭到尾都是配置,往往不著道的話會被這一連串的配置文件搞花了眼。
相比來說,Castal Windsor的配置簡單的多了,對於對象之間的依賴關系,Castle IOC會自動去連接,缺點是如果有多個類(組件)實現同一個接口(服務),容器會自動選擇最先加入到容器中的組件來裝配。“Windsor是Castle的一個IOC容器。它構建於MicroKernel之上,功能非常之強大,能檢測類並了解使用這些類時需要什麼參數,檢測類型和類型之間工作依賴性,並提供服務或者發生錯誤時提供預警的機制。”
最後出場的是微軟P&P小組的Unity容器,它在Enterprise Library4.0中提供.“Unity是一個輕量級的、可擴展的依賴注入容器,容器靠使用構造器、屬性和方法的方式來實現注入,你可以單獨的使用Unity而不需要安裝Enterprise Library,Unity為Enterprise Library的對象產生提供了一種新的方式。”Unity與Windsor的方式基本相似,而且都是創建容器、注冊接口映射、獲取對象實例三個步驟。
若要實現依賴注入,這三種容易都是不錯的選擇,具體看你得習慣與專長了。有關這三個容器的使用方法,我會在以後的文章中一起放出,這裡只對它們作簡單的介紹。
取捨與適用
總體來說,依賴注入的適用場景就在於---變化,我們預先根據經驗預料或者後期重構在某個地方會產生變化,那麼就可以將依賴解耦,采用外部文件的方式動態的將對象的創建注入進程序內。若沒有變化產生也硬要采用依賴注入模式的話,就會有過度設計的問題出現了。
文章內的例子和語言可能還有很多不恰當的地方,但對依賴注入這門技術的產生與發展有了更深的認識,我的目的也就達到了。