一,建立松耦合的組件
1."分解關注點"是MVC模式裡面一個非常重要的特性。我們想要在應用程式裡面建立的組件儘可能的獨立,這樣我們就能管理比較少的依賴關係。理想情況下,每個組件都是孤立的,不知道其他組件的存在,處理應用程式的其他領域僅僅通過抽象介面,這就是所謂的松耦合,它讓我們的應用程式更加容易測試和修改。通過一個簡單的例子可以協助我們理解,假如我們想寫一個發郵件的組件,暫且就把這個組件命名為MyEmailSender,接著我們實現一個介面,這個介面定義了所有需要發送郵件的功能,也暫且將這個介面命名為IEmailSender。任何其他的應用程式的組件需要引用IEmailSender裡面的方法就行了。
比如有一個重設密碼的組件PasswordResetHelper需要在使用者重設密碼後發生郵件,展示這它們之間的關係:
通過引入IEmailSender,我們就能夠確保在PasswordResetHelper跟MyEmailSender之間沒有直接的依賴關係。比如,我們可以用其他的實現了發送郵件的Provider來替換當前的MyEmailSender而不會對PasswordResetHelper造成影響,從這裡也能夠體會到松耦合的好處吧。
當然並不是所有的組件之間存在的關係都需要用介面來解耦和。這取決於我們的應用程式的複雜程度,需要什麼樣的測試,長期維護的可能性。我們不用去對一個簡單的ASP.NET MVC程式執行解耦。
2.使用依賴注入
介面能夠協助我們解耦組件,但是這樣仍然面臨一個問題,那就是C#並沒有提供一種嵌入的方式來比較容易的建立實現介面的對象,因為我們只能建立一個具體實現了介面的組件的執行個體,比如這裡的的MyEmailSender的執行個體。像下面的這種方式:
public class PasswordResetHelper
{
public void ResetPassword()
{
IEmailSender mySender = new MyEmailSender();
//一些關於郵件的詳細
mySender.SendEmail();
}
}
我們僅僅做了松耦合的一部分工作,PasswordResetHelper類通過IEmailSender來配置和發送郵件,通過介面的實現來建立對象,這裡需要建立一個MyEmailSender的執行個體。這樣看來,我們可能讓事情更糟,因為現在的PasswordResetHelper同時依賴IEmailSender和MyEmailSender,正如所顯示的那樣:
其實我現在需要一種方式來擷取對象(這裡就是指上面代碼裡的mySender),這個對象是實現了我們給定的介面但不是直接去建立實現介面(這裡指MyEmailSender)對象本身。對於這個問題的解決方案,我們稱為依賴注入(dependency injection(DI)),也可以被認為是控制反轉(inversion of control(IoC))。
3.DI(dependency injection):
一種完成松耦合的設計模式,這是一個非常重要的概念,它是高效MVC開發的中心。
DI分為兩個部分:一是從我們的組件裡面移除任何的對具體類的依賴性。在我們的這個例子裡面,我們這樣來做,將對必要介面的實現移動到類的構造器裡面,如下所示:
public class PasswordResetHelper
{
private IEmailSender emailSender;
public PasswordResetHelper(IEmailSender emailSenderParam)
{
emailSender = emailSenderParam;
}
public void ResetPassword()
{
//郵件的詳細配置...
emailSender.SendEmail();
}
}
這樣做了以後,我們可以發現,沒有了MyEmailSender,這也就意味著我們打破了PasswordResetHelper和MyEmailSender之間的依賴性。PasswordResetHelper的構造器需要一個對象作參數,而這個對象是IEmailSender介面的的實現,它不用去知道這個對象是什麼,或者說它根本不用去關心,並且也不用負責去建立它。
4.通過上面的操作,依賴在運行時就被注入到了PasswordResetHelper裡面,也意味著那些實現了IEmailSender介面的類的執行個體將會被建立,並在PasswordResetHelper執行個體化期間傳遞給它的構造器。這樣在PasswordResetHelper和任何實現了它需要的介面的類之間沒有編譯時間的依賴。
PasswordResetHelper是通過它的構造器來實現依賴注入的,我們把這種稱為Constructor Injection(構造器注入);我也可以通過它的公用屬性來實現依賴注入,通常稱這種方式為Setter Injection(設定注入)。如果你想瞭解 Constructor Injection vs. Setter Injection,可以猛擊這裡;
因為依賴是在運行時處理,這樣我們就可以決定在程式啟動並執行時候使用哪個介面實現。就像這裡我們可以不同的Email Provider,或者是偽造一個僅僅用來測試。通過上面的方式我們目的就達到了。
二,一個具體的MVC 依賴注入的例子
還是回到我之前做的競拍,接下就是將依賴注入應用到我們的競拍程式裡面,我們的目標很簡單,建立一個controller命名為AdminController,我們使用MembersRepository來持久化,為瞭解決AdminController和MembersRepository之間的耦合,我們定義一個介面IMembersRepository.具體的代碼如下:
public interface IMembersRepository
{
void AddMember(Member member);
Member FetchByLoginName(string loginName);
void SubmitChanges();
}
public class MembersRepository : IMembersRepository
{
public void AddMember(Member member) {...}
public Member FetchByLoginName(string loginName) {... }
public void SubmitChanges() {...}
}
現在我寫個controller類,它依賴於IMembersRepository,如下所示:
public class AdminController : Controller
{
IMembersRepository membersRepository;
public AdminController(IMembersRepository repositoryParam)
{
membersRepository = repositoryParam;
}
public ActionResult ChangeLoginName(string oldLoginParam, string newLoginParam)
{
Member member = membersRepository.FetchByLoginName(oldLoginParam);
member.LoginName = newLoginParam;
membersRepository.SubmitChanges();
//用來呈現一些View
}
}
AdminController需要一個IMembersRepository介面的實現的對象作為構造器的參數。這個在運行時會被注入,也會允許AdminController對介面實現的對象的執行個體進行操作而不會發生耦合。
使用一個依賴注入容器
我已經解決了依賴的問題,因為我們會在程式運行時注入構造器依賴,
但是我仍然需要去解決另一個問題:我們怎樣去執行個體化介面實現對象的執行個體而不在我們程式的其他地方建立依賴。
解決的辦法就是DI容器,或者也稱為IoC 容器。它實際上是一個組件,充當著依賴與實現依賴之間的"經紀人"的角色,我理解為就是中介代理什麼的。具體到我們的例子裡面就是,PasswordResetHelper需求(Demands)跟MyEmailSender之間的"經紀人"。
我們通過DI容器註冊應用程式的使用的介面或抽象類別的集合,這樣就是要告訴DI容器選擇哪一個合適具體類的執行個體來滿足依賴。所以我們也應該通過容器註冊IEmailSender,並且具體到當IEmailSender的實現被需要時,哪一個MyEmailSender的執行個體被建立,無論我們什麼時候需要調用IEmailSender,比如建立一個PasswordResetHelper的執行個體,我們就去DI容器,它會給我們一個之前註冊的作為預設的(介面實作類別)的執行個體,這裡可能比較繞,我們的例子裡面指的就是MyEmailSender
可能你跟我一樣,對DI容器還非常陌生,它到底是個什麼東西。呵呵,不用擔心,我們不用去實現DI容器,有很多好的開源的實現可以直接用。其中有一個Ninject推薦使用,可能你又跟我一樣對Ninject同樣陌生,也不用擔心,你可以猛擊這裡。當然後面也會有專門的章節介紹它,所以我們大可放心。當然微軟也建立了自己的DI容器Unity,如果你想瞭解,也可以猛擊這裡。
我們可能會覺得DI的角色不重要,那你就錯了,其實好的DI會具備一些非常"聰明"的特性,如依賴鏈的解決,對象生命週期的管理,構造器參數的配置等。並不建議我們自己建DI容器,如果是自己做實驗可以,因為Ninject已經做的很好了,而且也推薦使用。
今天的筆記做到這裡,到這裡已經完成了前四章的筆記,也就是說我才學習到這個地方,非常希望路過的大牛多指導。
我也是剛學習MVC,做筆記是為了鞏固和加深理解,當然如果能給到那些跟我一樣的初學者一點點的協助,我就非常高興了。筆記裡面肯定會有我理解不對的地方,還請路過的大牛們多多協助指導。謝謝!
祝路過的朋友工作順利!
晚安!