new程式碼味道——狎暱(xia ni)關係:過分親近
這個主題是我比較想重點聊聊的,因為我個人的理解是依賴注入思想最終想解決的問題就是消除物件之間的耦合,再通俗一點講就是消除new程式碼味道,解決的指導思想是將元件的配置和使用分離。
什麼是程式碼味道?
- 如果某段程式碼可能存在問題,就可以說有程式碼味道。這裡使用“可能”是因為少量的程式碼味道並不一定就是問題。
- 程式碼味道還可能表明有技術債務存在,而技術債務的修復是有代價的。揹負技術債務越久,債務修復就會越難。
- 程式碼味道有許多分類。
思考一下為什麼除了一些特殊情況外,凡是出現new關鍵字的地方都是程式碼味道?
一個示例,展示如何通過例項化物件來破壞程式碼的自適應能力
public class AccoutController
{
private readonly SecurityService securityService;
public AccountController()
{
this.securityService = new SecurityService();
}
[HttpPost]
public void ChangePassword(long userId,string newPassword)
{
var userRepository = new UserRepository();
var user = userRepository.GetById(userId);
this.securityService.ChangePassword(user,newPassword);
}
}
複製程式碼
這段程式碼就比較接近業務程式碼了,程式碼中有下列一些問題,這些問題是由於兩個顯式呼叫new關鍵字的構造物件例項引起的。
- AccoutController類永遠依賴SecurityService類以及UserRepository類的具體實現。
- AccoutController類隱式依賴SecurityService類以及UserRepository類的所有依賴。
- AccoutController類很難測試,因為無法使用偽實現(模擬物件或存根)來模擬和替代SecurityService類和UserRepository類。
- SecurityService類的ChangePassword方法需要客戶端預選載入好User類的例項物件(變相的依賴)。
詳細剖析一下這幾個問題。 1.無法增強實現——違反了OCP開閉原則 當我們想改變SecurityService類的實現時,只有兩種選擇,要麼改動AccountController來直接引用新的實現,要麼給現有的SecurityService類新增新功能。我們會發現這兩種選擇都不好。第一種選擇違反了對修改關閉,對擴充套件開放的開閉原則;第二種可能會違反SRP單一職責原則。這樣的程式碼無法增強實現,無異於一錘子買賣。 2.依賴關係鏈——違反了DIP控制反轉原則 AccoutController類依賴SecurityService類,SecurityService類也會有自己的依賴關係。上面的示例程式碼SecurityService類可能看起來沒有什麼依賴,但是實際上可能會是這樣:
public SecurityService()
{
this.Session = SessionFactory.GetSession();
}
複製程式碼
SecurityService類實際上依賴SessionFactory獲取Session物件,這就意味著AccoutController類也隱式依賴SessionFactory。違反了DIP控制反轉原則:更高層次的模組不能依賴低層模組,兩者都應該依賴抽象介面或者抽象類。而示例程式碼中到處都是對低層模組的依賴。
3.缺乏可測試性——違反了程式碼的可測試性 程式碼的可測試性也非常重要,它需要程式碼以一定的格式構建。如果不這樣做,測試將變得極其困難。我們寫過單元測試一定知道,單元測試第一步便是要對待測試物件進行依賴隔離,只有這樣我們的測試才是穩定的(排除了依賴物件的不穩定性)、可重複的。我們使用的隔離框架moq(其實是所有隔離框架)都是通過使用模擬實現來替代待測試物件的依賴物件工作的。示例程式碼中依賴的物件在程式碼編譯階段就已經被確定了,無法在程式碼執行階段動態的替換依賴物件,所以也就不具備可測試性了。
物件構造的替代方法
怎樣做才可以同時改進AccountController和SecurityService這兩個類,或者其他任何不合適的物件構造呼叫呢?如何才能正確設計和實現這兩個類以避免上節所講述的任何問題呢?下面有一些互補的方式可供選擇。 1.針對介面程式設計 我們首先需要做的改動是將SecurityService類的實現隱藏在一個介面後。這樣AccountController類只會依賴SecurityService類的介面而不是它的具體實現。第一個程式碼重構就是為SecurityService類提取一個介面。 為SecurityService類提取一個介面:
public interface ISecurityService
{
void ChangePassword(long userId,string newPassword);
}
public class SecurityService:ISecurityService
{
public void ChangePassword(long userId,string newPassword)
{
//...
}
}
複製程式碼
下一步就是改動客戶端程式碼類呼叫ISecurityService介面,而不是SecurityService類。 AccountController類現在依賴ISecurityService介面:
public class AccountController
{
private readonly ISecurityService securityService;
public AccountController ()
{
this.securityService = new SecurityService();
}
public void ChangePassword(long userId,string newPassword)
{
securityService.ChangePassword(userId,newPassword);
}
}
複製程式碼
重構仍然沒有結束,因為依然直接呼叫了SecurityService類的建構函式,所以重構後的AccountController類依然依賴SecurityService類的具體實現。要將這兩個具體類完全解耦,還需要作進一步的重構。引入依賴注入(DI)。 2.使用依賴注入 這個主題比較大,無法用很短的篇幅講完。並且後面我們會詳細的探討依賴注入,所以現在我只會從使用依賴注入的類的角度來講解一些基本的要點。 繼續我們的重構,重構後的建構函式程式碼部分已經加粗顯示,重構動作的改動非常小,但是管理依賴的能力卻大不相同。AccountController類不再要求構造SecurityService類的例項,而是要求它的客戶端程式碼提供一個ISecurityService介面的實現。 使用依賴注入從AccountController類中移除對SecurityService類的依賴:
public class AccountController
{
private readonly ISecurityService securityService;
public AccountController (ISecurityService securityService)
{
if(securityService == null)
{
throw new ArgumentNullException("securityService");
}
this.securityService = securityService;
}
public void ChangePassword(long userId,string newPassword)
{
this.securityService.ChangePassword(userId,newPassword);
}
}
複製程式碼
本節我們主要討論了new程式碼味道及其缺點,也通過重構程式碼的方式引出了new程式碼味道兩種互補的方式--針對介面編碼和使用依賴注入。之所以說是互補的方式,是因為針對介面編碼只能讓程式碼部分解耦,還是沒有解決直接呼叫被依賴類的建構函式的問題;而使用依賴注入雖然解決了這個問題,但是使用依賴注入是依賴於針對介面程式設計的。可以說只有我們針對介面編碼,才有可能使用依賴注入解決掉new程式碼味道。
忘記是誰說的了,瞭解學習一件事物之前要先了解學習它的發展歷史。學習任何知識,很重要的一點是學習其中的思維方式,看待問題,解決問題的思維方式。所以我希望能通過一個很簡單的小遊戲力求形象的描述依賴注入的演變歷程,以及是什麼推進了依賴注入的演變歷程。希望大家看完之後都能有所收穫,也希望大家看完之後對於依賴注入有自己的理解。讓我們開始吧!
鴨貓大戰
好了,讓我們從最簡單的開始,希望我們能從簡單到複雜,慢慢理解從面向介面程式設計到依賴注入的思想:
複製程式碼
我現在要設計一個鴨貓大戰的遊戲,採用標準的OO技術,首先設計一個鴨子的抽象類。
public abstract class Duck
{
public void Eat(){};
public void Run(){};
public abstract void Display();
}
複製程式碼
假設在遊戲中鴨子的吃東西、跑等行為都是相同的,唯一不同的是鴨子的外觀,所以Display方法設定為抽象的,具體的實現在子類中實現。
public class BeijingDuck:Duck
{
public override void Display()
{
//北京鴨
};
}
public class ShandongDuck:Duck
{
public override void Display()
{
//山東鴨
};
}
//其他鴨...
複製程式碼
好了,現在鴨鴨大戰第一版已經上線了。現在產品想讓遊戲中的鴨子可以叫,最簡單的一種實現方式就是在抽象基類中增加一個Shout()方法,這樣所有的繼承鴨子型別都可以叫了。不過我們很快就會發現問題來了,這樣做的 話所有的所有的鴨子都會叫了,這顯然是不符合邏輯的。那麼有人肯定會想到使用介面了,將Shout()方法提取到 介面中,然後讓會叫的鴨子型別實現介面就可以了。
public interface IShout
{
void Shout();
}
public class BeijingDuck:Duck,IShout
{
public override void Display()
{
//北京鴨
};
public void Shout()
{
//呱呱
}
}
public class ShandongDuck:Duck,IShout
{
public override void Display()
{
//山東鴨
};
public void Shout()
{
//呱呱
}
}
複製程式碼
上面的實現看起來很好,但是、但是、但是需求總是在變化的。
現在產品要求鴨子不僅要會叫,而且每種鴨子型別叫聲還要求不一樣,並且不同的鴨子型別叫聲還可能會一樣。那麼上面的這種實現當時的缺點就顯示出來了,程式碼會在多個子類中重複,並且執行時不能修改(繼承體系的缺點,程式碼在編譯時就已經確定,無法動態改變)等。
理解為什麼要“面向介面程式設計,而不要面向實現程式設計”
接下來我們可以把變化的地方提取出來,多種行為的實現用統一的介面實現。當我們想增加一種行為時,只需要繼承介面就可以了,對其它行為沒有任何影響。
public interface IShout
{
void Shout();
}
public class GuaGuaShout:IShout
{
public void Shout()
{
//呱呱
}
}
public class GaGaShout:IShout
{
public void Shout()
{
//嘎嘎
}
}
//其他叫聲行為類
複製程式碼
現在某一種具體的鴨子型別實現就變成了:
public class BeijingDuck:Duck
{
IShout shout;
public BeijingDuck(IShout shout)
{
this.shout = shout;
}
public void Shout()
{
shout.Shout();
}
//可以在執行時動態改變行為
public void SetShout(IShout shout)
{
this.shout = shout;
}
public override void Display()
{
//北京鴨
};
}
複製程式碼
這樣的設計的優點就在於可以在執行時動態的改變行為,而且在不影響其他類的情況下增加更改行為。所以這樣的設計是充滿彈性的。而對比前面的設計我們就會發現,之前的設計依賴於繼承抽象類和實現介面,這兩種設計都依賴於“實現”,物件的行為在編譯完成的那一刻就已經被決定了,無法改變。(組合優於繼承)
理解為什麼要“依賴抽象,而不要依賴具體類”
現在我們要開始鴨貓遊戲,首先我們建立一個鴨子物件才能開始遊戲,就像下面這樣。
//建立一隻會呱呱叫的北京鴨
new BeijingDuck(new GuaGuaShout());
//建立一隻會嘎嘎叫的北京鴨
new BeijingDuck(new GaGaShout());
//建立一隻會嘎嘎叫的北京鴨
new ShandongDuck(new GuaGuaShout());
複製程式碼
問題又出現了,程式碼中充斥著的大量的"new"程式碼。當我們使用“new”的時候,就是在例項化具體類。當出現實體類的時候,程式碼就會更缺乏“彈性”。越是缺乏彈性越是難於改造。在後面我們還會繼續討論“new程式碼味道”。
簡單工廠
讓我們繼續回到遊戲。為了增加遊戲的互動性,你可以選擇鴨或貓中的任一角色開始遊戲。如果我們選擇了鴨子角色開始遊戲,那麼我們應該在固定的場景會遇到固定的貓。在波斯會遇到波斯貓,在中國遇到狸花貓,在歐洲遇到挪威森林貓。用簡單工廠不難實現。
public interface ICat
{
void Scratch();
}
//波斯貓
public class PersianCat:ICat
{
public void Scratch()
{
//來自波斯貓的撓
}
}
//狸花貓
public class FoxFlowerCat:ICat
{
public void Scratch()
{
//來自狸花貓的撓
}
}
//挪威森林貓
public class NorwegianForestCat:ICat
{
public void Scratch()
{
//來自挪威森林貓的撓
}
}
複製程式碼
生產貓的工廠:
public class CatFactory
{
public ICat GetCat(string catType)
{
if(catType.IsNullOrEmpty())
{
return null;
}
if(catType == "PersianCat")
{
return new PersianCat();
}else if(catType == "FoxFlowerCat")
{
return new FoxFlowerCat();
}else if(catType == "NorwegianForestCat")
{
return new NorwegianForestCat();
}
return null;
}
}
複製程式碼
使用工廠建立貓物件:
public class FactoryPatternDemo
{
public static void main(String[] args)
{
CatFactory factory = new CatFactory();
//建立波斯貓物件
ICat persianCat = factory.GetCat("PersianCat");
//建立狸花貓物件
ICat foxFlowerCat = factory.GetCat("FoxFlowerCat");
//建立挪威森林貓物件
ICat norwegianForestCat = factory.GetCat("NorwegianForestCat");
}
}
複製程式碼
簡單工廠設計模式屬於建立型模式,它提供了一種建立物件的最佳方式。這是設計模式裡對於工廠模式的說明。 工廠模式確實在一定程度上解決了建立物件的難題,專案中不會再到處充斥了“new程式碼味道”。但是有一個問題沒有解決,要例項化哪一個物件,是在執行時由一些條件決定。當一旦有變化或擴充套件時,就要開啟這段程式碼(工廠實現程式碼)進行修改,這違反了“對修改關閉”的原則。還有就是這段程式碼依賴特別緊密,並且是高層依賴底層(客戶端依賴具體類(工廠類)的實現),因為判斷建立哪種物件是在工廠類中實現的。幸運的是,我們還有“依賴倒置原則”和“抽象工廠模式”來拯救我們。
抽象工廠和依賴倒置原則
客戶端(高層元件)依賴於抽象Cat,各種貓咪(底層元件)也依賴於抽象Cat,雖然我們已經建立了一個抽象Cat,但是仍然在程式碼中建立了具體的Cat,這個抽象其實並沒有什麼影響力。使用抽象工廠模式可以將這些例項化物件的程式碼隔離出來。這符合軟體設計中的對於可以預見變化的部分,要使用介面進行隔離。讓我們繼續回到遊戲中,之前我們提到過,在固定的場景會遇到固定的遊戲角色,所以我們需要為不同遊戲場景建立場景物件。
首先我們要建立一個工廠介面:
public interface IFactory
{
ICat CreateCat();
Duck CreateDuck();
}
複製程式碼
然後建立建立場景的工廠類:
public class BeijingSceneFactory:IFactory
{
public ICat CreateCat()
{
return new FoxFlowerCat();
}
public Duck CreateDuck()
{
return new BeijingDuck();
}
}
public class ShanDongSceneFactory:IFactory
{
public ICat CreateCat()
{
return new FoxFlowerCat();
}
public Duck CreateDuck()
{
return new ShanDongDuck();
}
}
複製程式碼
構建場景類(虛擬碼):
public class Scene
{
private ICat cat;
private Duck duck;
IFactory factory;
public Scene(IFactory factory)
{
this.factory = factory;
}
public void Create(string role)
{
if(role == "duck")
{
duck = factory.CreateDuck();
}
if(role == "cat")
{
cat = factory.CreateCat();
}
}
}
複製程式碼
這樣一來,遊戲場景改變不由程式碼來改變,而是由客戶端動態的決定。相當於變相的減少了高層對底層的依賴。現在其實我們已經能大體理解依賴倒置的原則:依賴抽象,而不依賴具體類。 客戶端程式碼實現:
public class FactoryPatternDemo
{
public static void main(String[] args)
{
IFactory factory = new BeijingSceneFactory();
Scene scene = new Scene(factory);
scene.Create("duck");
}
}
複製程式碼
現在的程式碼設計已經比較靠近“注入”的概念了(窮人的依賴注入),仔細看 Scene scene = new Scene(factory);
, 建立場景的工廠物件是抽象的介面型別,而且是通過建構函式動態傳入的,通過這樣的改造就為我們使用依賴注入框架提供了可能性。當然在抽象工廠和依賴注入之間,還有一個問題值得我們去思考。這個問題就是“如何將元件的配置和使用分離”,答案也已經很明瞭了——依賴注入。
理解將元件的配置和使用分離
如果覺得元件這個比較抽象的話,我們可以把“元件”理解為“物件”(底層元件),那麼相應的“元件的配置”就理解成為“物件的初始化”。現在“將元件的配置和使用分離”這句話就很好理解了 ,就是將物件的建立和使用分離。這樣做的優點很明顯,將物件的建立推遲到了部署階段(這句話可能不太好理解),就是說物件的建立全部依賴於我們統一的配置,我們可以修改配置動態的把我們不想使用的物件替換成我們想使用的物件,而不用修改任何使用物件的程式碼。原則上我們需要把物件的裝配(配置)和業務程式碼(使用)分離開來。
依賴注入
依賴注入(DI)是一個很簡單的概念,實現起來也很簡單。儘管如此,這種簡單性卻掩蓋了該模式的重要性。當某些事情很簡單也很重要時,人們就會將它過度複雜化,依賴注入也一樣。要理解依賴注入,我們首先要這個詞拆開來解讀——依賴和注入。
什麼是依賴?
要用文字解釋這個概念可能不太好理解(文不如表,表不如圖),我們可以使用有向圖對依賴建模。一個依賴關係包含了兩個實體,它們之間的聯絡方向是從依賴者到被依賴者。
使用有向圖對依賴建模: A依賴B:
B依賴A:
網際網路提供很多服務,服務依賴網際網路:
包(包括程式集和名稱空間)既是客戶也是服務:
客戶端類依賴服務類:
有些服務會隱藏在介面後面:
有向圖中有一種特殊的迴圈叫做自迴圈:
方法層的遞迴就是一個很好的自迴圈的例子。
軟體系統中的依賴
我們都知道,在採用物件導向設計的軟體系統中,萬物皆物件。所有的物件通過彼此的合作,完成整個系統的工作。就好比下面的齒輪系統,每個齒輪轉動帶動整個齒輪系統的運轉。但是這樣的設計就意味著強依賴,強耦合。如果某個齒輪出問題不轉動了,整個齒輪系統就會癱瘓掉,這顯然是我們所不能接受的。
圖1.軟體系統中耦合的物件:
什麼是控制反轉(IOC)?
耦合關係不僅會出現在物件與物件之間,也會出現在軟體系統的各模組之間,以及軟體系統和硬體系統之間。如何降低系統之間、模組之間和物件之間的耦合度,是軟體工程永遠追求的目標之一。為了解決物件之間的耦合度過高的問題,軟體專家Michael Mattson提出了IOC理論,用來實現物件之間的“解耦”。目前這個理論已經被成熟的應用到專案當中,衍生出了各式各樣的IOC框架產品。 IOC理論提出的觀點大致是這樣的:藉助於“第三方”實現具有依賴關係的物件之間的解耦。如下圖: 圖2.IOC解耦過程:
由於引進了中間位置的“第三方”,也就是IOC容器,使得A、B、C、D這4個物件沒有了耦合關係,齒輪之間的傳動全部依靠“第三方”了,全部物件的控制權全部上繳給“第三方”IOC容器,所以,IOC容器成了整個系統的關鍵核心,它起到了一種類似“粘合劑”的作用,把系統中的所有物件粘合在一起發揮作用,如果沒有這個“粘合劑”,物件與物件之間會彼此失去聯絡,這就是有人把IOC容器比喻成“粘合劑”的由來。 那麼如果我們把IOC容器拿掉,系統會是什麼樣子呢? 圖3.拿掉IOC容器的系統:
拿掉IOC容器的系統,A、B、C、D這4個物件之間已經沒有了耦合關係,彼此毫無聯絡,這樣的話,當你在實現A的時候,根本無須再去考慮B、C和D了,物件之間的依賴關係已經降低到了最低程度。
軟體系統在沒有引入IOC容器之前,如圖1所示,物件A依賴於物件B,那麼物件A在初始化或者執行到某一點的時候,自己必須主動去建立物件B或者使用已經建立的物件B。無論是建立還是使用物件B,控制權都在自己手上。軟體系統在引入IOC容器之後,這種情形就完全改變了,如圖3所示,由於IOC容器的加入,物件A與物件B之間失去了直接聯絡,所以,當物件A執行到需要物件B的時候,IOC容器會主動建立一個物件B注入到物件A需要的地方。通過前後的對比,我們不難看出來:物件A獲得依賴物件B的過程,由主動行為變為了被動行為,控制權顛倒過來了,這就是“控制反轉”這個名稱的由來。
什麼是依賴注入?
2004年,Martin Fowler探討了同一個問題,既然IOC是控制反轉,那麼到底是“哪些方面的控制被反轉了呢?”,經過詳細地分析和論證後,他得出了答案:“獲得依賴物件的過程被反轉了”。控制被反轉之後,獲得依賴物件的過程由自身管理變為了由IOC容器主動注入。於是,他給“控制反轉”取了一個更合適的名字叫做“依賴注入(Dependency Injection)”。他的這個答案,實際上給出了實現IOC的方法:注入。所謂依賴注入,就是由IOC容器在執行期間,動態地將某種依賴關係注入到物件之中。 所以現在我們知道,控制反轉(IOC)和依賴注入(DI)是從不同角度對同一件事物的描述。就是通過引入IOC容器,利用注入依賴關係的方式,實現物件之間的解耦。
使用控制反轉(IOC)容器
我們在開發時經常會遇到這種情況,開發中的類委託某些抽象完成動作,而這些被委託的抽象又被其他的類實現,這些類又委託其他的一些抽象完成某種動作。最終,在依賴鏈終結的地方,都是一些小且直接的類,它們已經不需要任何依賴了。我們已經知道如何通過手動構造類例項並把它們傳遞給建構函式的方式來實現依賴注入的效果(窮人的依賴注入)。儘管這種方式可以任意替換依賴的實現,但是構造的例項物件圖依舊是靜態的,也就是說編譯時就已經確定了。控制反轉允許我們將構建物件圖的動作推遲到執行時。 控制反轉容器組成的系統能夠將應用程式使用的介面和它的實現類關聯起來,並能在獲取例項的的同時解析所有相關的依賴。 示例程式碼中沒有手動構造實現的例項,而是通過使用Unity控制反轉容器來建立類和介面的對映關係:
public partial class App:Application
{
private IUnityContainer container;
private void OnApplicationStartUp()
{
container = new UnityContainer();
container.RegisterType<ISettings,ApplicationSettings>();
container.RegisterType<ITaskService,TaskService>();
var taskService = container.Resolve<ITaskService>();
}
}
複製程式碼
1.程式碼的第一步就是初始化得到一個UnityContainer例項。 2.在建立好Unity容器後,我們需要告訴該容器應用程式生命週期內每個介面對應的具體實現類是什麼。Unity遇到任何介面時,都會知道去解析哪個實現。如果我們沒有為某個介面指定對應的實現類,Unity會提醒我們該介面無法例項化。 3.在完成介面和對應實現類的關係註冊後,我們需要獲得一個TaskService類的例項。Unity容器的Resolve方法會檢查TaskService類的建構函式,然後嘗試去例項化建構函式要注入的依賴項。如此反覆,直到完全例項化整個依賴鏈上的所有依賴項的例項後,Resolve方法會成功例項化TaskService類的例項。
控制反轉(IOC)容器的工作模式——註冊、解析、釋放模式
所有的控制反轉容器都符合一個只有三個的方法的簡單介面,Unity也不例外。 儘管每個控制反轉容器實現不完全相同,但是都符合下面這個通用的介面:
public interface IContainer:IDisposable
{
void Register<TInterface,TImplementation>()
where TImplementation:TInterface;
TImplementation Resolve<TInterface>();
void Release();
}
複製程式碼
- Register:應用程式首先會呼叫此方法。而且該方法會被多次呼叫以註冊不同的介面及其實現之間的對映關係。這裡的Where子句用來強制TImplementation型別必須實現它所繼承的TInterface介面。
- Resolve:應用程式執行時會呼叫此方法獲取物件例項。
- Release:應用程式生命週期中,當某些類的的例項不再需要時,就可以呼叫此方法釋放它們佔用的資源。這有可能發生在應用程式結束時,也有可能發生在應用程式執行的某個恰當時機。 我們都知道在我們使用的Unity容器註冊時可以配置是否開啟單例模式。通常情況下,資源只對單次請求有效,每次請求後都會呼叫Release方法。但是當我們配置開啟單例模式時,只有在應用程式關閉時才會呼叫Release方法。
命令式與宣告式註冊
到此為止,我們都是使用的命令式註冊:命令式的從容器物件上呼叫方法。 命令式註冊優點:
比較簡潔,易讀。
編譯時檢查問題的代價非常小(比如防止程式碼輸入錯誤等)。 命令式註冊缺點:
註冊的過程在編譯時已經確定了,如果想要替換實現,必須修改原始碼,然後重新編譯。
如果通過XML配置進行宣告式註冊,就不需要重新編譯。 應用程式配置檔案:
<configuration>
<configSections name="unity"
type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection">
</configSections>
<unity>
<container>
<register type="ISettings" mapTo="ApplicationSettings"/>
<register type="ITaskService" mapTo="TaskService"/>
</container>
</unity>
</configuration>
複製程式碼
應用程式入口:
public partial class App:Application
{
private IUnityContainer container;
private void OnApplicationStartUp()
{
var section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
container = new UnityContainer().LoadConfiguration(section);
var taskService = container.Resolve<ITaskService>();
}
}
複製程式碼
宣告式註冊優點:
- 將介面和相應的實現的對映動作推遲到配置時。 宣告式註冊缺點:
- 太繁瑣,配置檔案會巨大。
- 註冊時的錯誤會跳過編譯,直到執行時才能被發現和捕獲。
三種依賴注入方式及其優缺點
首先大家思考一下為什麼在專案中會要求大家在控制器層使用屬性注入,在業務邏輯層使用建構函式注入?
1.建構函式注入
public class TaskService
{
private ITaskOneRepository taskOneRepository;
private ITaskTwoRepository taskTwoRepository;
public TaskService(
ITaskOneRepository taskOneRepository,
ITaskTwoRepository taskTwoRepository)
{
this.taskOneRepository = taskOneRepository;
this.taskTwoRepository = taskTwoRepository;
}
}
複製程式碼
優點:
- 在構造方法中體現出對其他類的依賴,一眼就能看出這個類需要其他那些類才能工作。
- 脫離了IOC框架,這個類仍然可以工作(窮人的依賴注入)。
- 一旦物件初始化成功了,這個物件的狀態肯定是正確的。
缺點:
- 建構函式會有很多引數。
- 有些類是需要預設建構函式的,比如MVC框架的Controller類,一旦使用建構函式注入,就無法使用預設建構函式。
2.屬性注入
public class TaskService
{
private ITaskRepository taskRepository;
private ISettings settings;
public TaskService(
ITaskRepository taskRepository,
ISettings settings)
{
this.taskRepository = taskRepository;
this.settings = settings;
}
public void OnLoad()
{
taskRepository.settings = settings;
}
}
複製程式碼
優點:
- 在物件的整個生命週期內,可以隨時動態的改變依賴。
- 非常靈活。
缺點:
- 物件在建立後,被設定依賴物件之前這段時間狀態是不對的(從建構函式注入的依賴例項在類的整個生命週期內都可以使用,而從屬性注入的依賴例項還能從類生命週期的某個中間點開始起作用)。
- 不直觀,無法清晰地表示哪些屬性是必須的。
3.方法注入
public class TaskRepository
{
private ISettings settings;
public void PrePare(ISettings settings)
{
this.settings = settings;
}
}
複製程式碼
優點:
- 比較靈活。
缺點:
- 新加入依賴時會破壞原有的方法簽名,如果這個方法已經被其他很多模組用到就很麻煩。
- 與構造方法注入一樣,會有很多引數。
相信大家現在一定理解了專案中某一層指定某一種注入方式的原因:利用其優點,規避其缺點。
組合根和解析根
1.組合根 應用程式中只應該有一個地方直到依賴注入的細節,這個地方就是組合根。在使用窮人的依賴注入時就是我們手動構造類的地方,在使用控制反轉容器時就是我們註冊介面和實現類間對映關係的地方。組合根提供了一個查詢依賴注入配置的公認位置,它能幫你避免把對容器的依賴擴散到應用程式的其他地方。 2.解析根 和組合根密切相關的一個概念是解析根。它是要解析的目標物件圖中根節點的物件型別。 這樣講很抽象,舉個例子: MVC應用程式的解析根就是控制器。來自瀏覽器的請求都會被路由到被稱為動作(action)的控制器方法上。每當請求來臨時,MVC框架會將URL對映為某個控制器名稱,然後找到對應名稱的類例項化它,最後在該例項上觸發動作。更確切的講,例項化控制器的過程就是解析控制器的過程。這意味著,我們能輕易的按照註冊、解析和釋放的模式,最小化對Resolve方法的呼叫,理想狀況下,就只應該在一個地方呼叫該方法。
組合根和解析根又是前面所講的“將元件的配置和使用分離”一種體現。
依賴注入的技術點
IOC中最基本的技術就是“反射(Reflection)”程式設計。有關反射的相關概念大家應該都很清楚,通俗的講就是程式碼執行階段,根據給出的資訊動態的生成物件。
總結
做一下總結,我們從new程式碼味道出發,引出了消除new程式碼味道(程式碼解耦)的兩種方式——針對介面編碼和使用依賴注入。然後我們通過開發一個小遊戲,瞭解了面向介面程式設計到依賴注入的歷程。最後深入了介紹了大Boss——控制反轉(依賴注入),主要介紹了什麼是依賴,控制反轉(依賴注入)的概念,使用控制反轉(IOC)容器,工作模式,命令式與宣告式註冊,三種依賴注入方式及其優缺點,組合根和解析根,依賴注入的技術點。 本次分享力求從原理和思想層面剖析依賴注入。因為我水平有限,可能有些點講的有些片面或不夠深入,所以給出我準備這次分享的參考資料。有興趣深入研究的同學,可以自行去看一下這些資料: 1.C#敏捷開發實踐 第2章 依賴和分層 第9章 依賴注入原則
2.HeadFirst設計模式 鴨貓大戰改編自第一章 設計模式入門
-----END-----
喜歡本文的朋友們,歡迎掃一掃下圖關注公眾號擼碼那些事,收看更多精彩內容