這一次在此講述MVC模式,讓大家對MVC有一個更加深刻的影響,為大家的深入學習做好堅定的基礎!如果對MVC模概念還是混淆的新同學,這話一定要好好學習了!
理解MVC模式
MVC模式意味着MVC應用程序將被分成至少三個部件:
- Models(模型):用於封裝與應用程序的業務邏輯相關的數據以及對數據的處理方法。“模型”有對數據直接訪問的權力,例如對數據庫的訪問。“模型”不依賴“視圖”和“控制器”,也就是說,模型不關心它會被如何顯示或是如何被操作。但是模型中數據的變化一般會通過一種刷新機制被公布。為了實現這種機制,那些用於監視此模型的視圖必須事先在此模型上注冊,從而,視圖可以了解在數據模型上發生的改變。
- Views(視圖):負責呈現數據給客戶端,也就是客戶端所看到的頁面。
- Controllers(控制器):控制器起到不同層面間的組織作用,用於控制應用程序的流程。它處理事件並作出響應。“事件”包括用戶的行為和數據模型上的改變。
由此可以看出MVC的每個模塊都是高內聚低偶合,注重點分離,操縱的邏輯數據模型中只包含在模型中,模型邏輯數據僅顯示在視圖中,代碼,處理用戶請求和輸入只包含在控制器。但是每個模塊之間的銜接缺十分微妙,恰到好處。
理解域模型(Domain Model)
我們創建模型識別的真實世界的實體、操作和規則中存在的行業或活動,我們的應用程序必須支持,稱為領域(Domain).對應我們來說,域模型(Domain Model)是一組c#類型(類,結構,等等),被稱為域類型(Domain Type).通過定義在領域類型里面的方法表示對領域的各種操作,並且領域的規則也表示在了這些方法里面,當我們創建了一個領域類型(Domain Type)的實例時,也就是創建了一個領域對象(Domain Object),領域模型通常是持久化的,當然持久化有很多方式,通常情況下利用關系型數據庫。
Asp.Net MVC的實現
在MVC模式中,控制器是c#類、通常來源於System.Web.Mvc.Controller類,每一個共有的方法我們稱為Action Method,這些Action(方法)通過Asp.Net運行時(Runtime)(路由系統)跟可配置的URL相關聯。當一個請求發送服務端時Controller(控制器)里的Action(方法)會被相應的執行一些操作域模型(Domain Model),然后選擇View(視圖)呈現到客戶端。如下圖1所示:
圖1
運用領域驅動開發(Applying Domain-Drivern Development)
域模型是MVC應用程序的一個中心部分,其他的一切包括視圖(Views)和控制器(Controllers)都只是一種手段來與之交互Domain Model(領域模型).Asp.Net MVC並不決定技術用於領域模型(Domain)。我們可以自由選擇任何技術跟.NET Framework相關的交互,當然這樣的技術那就相當多了。但是,Asp.Net MVC的確能為我們的基礎設施和約定來幫助域模型(Domain)中的類(Classes)與控制器(Controllers)和視圖(Views)連接,當然也包含MVC框架本身。這里有三個關鍵的特性:
- 模型綁定(Model bingding):自動使用HTML表單提交的數據來組織領域對象(Domain Objects)基礎的約定。
- 模型元數據(Model metadata):描述了自己寫的Model Classes對.Net框架所要表達的意思.Asp.Net MVC能夠自動的識別Model Classes顯示到Views里面。
- 驗證(Validation):在模型綁定時執行,應用被定義為模型元數據的那些規則。
建立一個簡單的域模型(Modeling an Example Domain)
下面的草圖模型為拍賣程序的類圖,如下圖2.
圖2
上面的模型包含了一個Members的集合,每一個Member會有一個Bids集合,每一個Bid對應一個Item,每一個Item可以有多個來自不同Members的Bids實現我們自己的域模型(Domain Model)並作為一個獨立的組件其中一個關鍵的地方是我們選擇的語言和術語,這個不是我們的編程語言,而是域建模的通用的語言。
- 首先,開發人員傾向於使用編程語言,比如類名,數據庫等等名詞來表達。而業務專家們是不懂這些的,他們也不需要懂。業務專家知曉一些技術方面的知識是一件非常危險的事情,因為他們會經常根據自己對技術的理解來不斷篩選他們的需求,這也就意味着需求會頻繁的更改,進而導致開發人員也不知道業務專家的真實需求到底是什么。創建通用語言的方法能夠幫助我們避免在一個應用程序里面過度的泛化需求,程序員傾向於建立每一個可能業務實際模型,而不是具體到某一個業務需求。
- 在通用語言和域模型(Domain Model)之間的這種連接不應該是非常膚淺的而是向DDD(Domain-Driven Design)專家所建議的那樣:對通用語言的任何變化都會導致Model的變化。假如我們讓建模跟業務領域不同步,我們就需要建立一種從Model到Domain映射的中間語言,從長遠來看,這種做法會導致災難。為此,我們將創建一個會兩種語言的特殊人類,他們隨后就開始篩選需求,這卻是建立在他們對兩種語言都不完全理解的基礎之上,當然這樣的后果可以想象。
聚合和簡化(Aggregates and Simplification)
圖2給我提供了一個良好的建立域模型(Domain Model)的開始,但是上面圖示的模型並沒有提供用C#和SQL Server實現Model的任何有用的幫助,接着就出現許多問題:
- 如果我們裝載一個Member進入內存,也是不是應該裝載Member的Bids以及相關的Items進入內存呢?
- 如果我們這樣做了,我們是否需要將這個Item其他的bids也加載進內存,並且也將做這些Bids的Members一同加載進內存呢?
- 當我們刪除一個對象時,我是否應該刪除相關的對象呢?如果是,又有哪些呢?
- 如果我們選擇用文檔存儲代替關系型數據庫來持久化,那哪一個對象的集合應該呈現到同一個文檔呢?
- .......
所以上面的問題,我們不知道如何解答,我們的域模型也不知道如何解答。
DDD(domain-driven development)的方式回應這些問題是將Domain Objects分配到組里面,這種方式稱為聚合(aggregates)。如下圖3.
圖3.
一個聚合的實體組將若干域對象聯合(Together)到了一起,有一個根實體被用來標識整個聚合,它在驗證和持久化操作里面充當了"Boss"的角色。在數據變化時,我把聚合當作一個單元來統籌處理,所以我們需要創建呈現在領域模型上下文里面有意義的關系的聚合,並且創建跟實現業務過程一致的邏輯操作。也就是說,我們需要通過分組對象來創建聚合,而這些對象是可以作為一個組被改變的。
DDD(domain-driven development)有一個重要的規則是,在一個聚合實例范圍外的對象,只能通過對根實體(Root entity)的引用來持久化,而不是引用在聚合里面的對象。這條規則強化了將聚合里面的對象作為一個單元來對待的概念。在本例子里面,Members和Items都是聚合的根,而Bids只能在作為它們聚合根實體的Item的上下文里面被訪問。Bids可以引用Members(根實體),但是Members不能引用Bids(不是根實體)。
着呀好處之一是,它簡化了聚合組對象之間的關系的域模型。通常這樣能夠幫助我們對需要建模的領域的本質的理解。本質上講,創建聚合約束領域模型和對象之間的關系使得這種關系更加接近於現實領域里面存在的關系。具體C#代碼如下:
public class Member { public string LoginName { get; set; } public int ReputationPoints { get; set; } } public class Item { public int ItemID { get; private set; } public string Title { get; set; } public string Description { get; set; } public DateTime AuctionEndDate { get; set; } public IList<Bid> Bids { get; set; } } public class Bid { public Member Member { get; set; } public DateTime DatePlaced { get; set; } public decimal BidAmount { get; set; } }
從上面的代碼可以看出,我們很容易就捕捉到了Bids和Members之間的單向關系的本質,當然我們也可以建立一些其他的約束。應用聚合能夠幫助我們建立更加有用,更加精確的領域模型,也能夠讓我們用C#熟練的實現。
一般來說,聚合為一個領域模型增加了結構化和精確化。這也使得應用驗證變更方便(根實體變成負責驗證狀態中的所有對象總體狀態),顯而易見的單位的持久性。而且,因為聚集在本質上是我們的域的原子單元模型,它們也能夠適用事務管理的單元和級聯從數據庫刪除的單元。
另一方面,聚合常常是人為的加上限制。聚合(Aggregates)的概念能夠很自然的從文檔型數據庫得到,但它不是Sqlserver本身的概念,也不是存在大部分ORM工具里的概念.
定義存儲庫(Defining Repositories)
在某種程度上,我們需要為我們的域模型添加持久性。這通常是通過一個關系或者對象或文檔數據庫。持久化不是我們領域模型的一部分,它是一個獨立的關注點,這也就意味着我們不能將持久化的代碼跟定義領域模型的代碼混合到一起。通常的方法去執行的分離域模型和持久化系統之間定義存儲庫。這些都是對象表示的基礎數據庫。而不是直接處理數據庫,域模型調用方法 定義的存儲庫,這反過來也會使調用到數據庫來存儲和檢索數據的模型.這允許我們分離模型與實現的持久性。這樣約定就是為每一個聚合(Aggregates)定義單獨的數據模型。在我們的競拍程序里面,我們可以創建2個Repositories,它們分別是針對Members的Repository和針對Items的Repository。注意這里,我們並不需要創建針對Bids的Repository,因為Bids會作為Items聚會持久化的一部分).代碼如下:
public class MembersRepository { public void AddMember(Member member) { } public Member FetchByLoginName(string loginName) { } public void SubmitChanges() { } } public class ItemsRepository { public void AddItem(Item item) { } public Item FetchByID(int itemID) { } public IList<Item> ListItems(int pageSize, int pageIndex) { } public void SubmitChanges() { } }
需要注意:Repositories僅僅針對加載和保存數據.它們不包含任何其他的邏輯。
建造松耦合組件(Building Loosely Coupled Components)
"分離關注點"是MVC模式里面十分非常重要的特性。我們希望組件作為獨立的應用程序中,盡可能為我們可以管理帶來較少相互依賴關系。 在我們的理想情況下,每個組件都沒有任何其他組件直接聯系,處理應用程序的其他應用程序領域中只有通過抽象的接口。這就是所謂的松耦合,它使測試和修改我們的應用程序更加容易。比如一個簡單的例子:如果我們在編寫一個組件,這個組件稱為MyEmailSenderto發送電子郵件,我們將實現一個接口,接口定義了所有的郵件發送的功能,我們將這個接口叫IEmailSender,任何其他的應用程序的組件需要引用IEmailSender里面的方法就行了。如下圖4(用接口來分離組件)
圖4.
通過上圖可以看出,我們引入IEmailSender,可以保證PasswordReset和MyEmailSender沒有直接的依賴關系。任何實現發郵件的功能都能來代替MyEmailSenderto而不會對PasswordResetHelper造成影響。
注意:不是每一段關系需要解耦使用接口。這個決定我們應用程序是多么復雜,需要什么樣的測試,並且需要長期的維護。例如,我們可不去解耦一個小而簡單的Asp.Net MVC應用程序。
使用依賴注入(Using Dependency Injection)
什么是依賴注入:簡單描述一下;依賴注入其實叫"控制反轉"(Inversion of Control,英文縮寫為IoC),是一個重要的面向對象編程的法則來削減計算機程序的耦合問題。 控制反轉還有一個名字叫做依賴注入(Dependency Injection)。簡稱DI。應用控制反轉,對象在被創建的時候,由一個調控系統內所有對象的外界實體,將其所依賴的對象的引用,傳遞給它。也可以說,依賴被注入到對象中。所以,控制反轉是,關於一個對象如何獲取他所依賴的對象的引用,這個責任的反轉。它也是一種設計模式!
接口能夠幫助我們解耦組件,但是這樣仍然面臨一個問題,那就是C#並沒有提供一種嵌入的方式來比較容易的創建實現接口的對象,我們只能創建一個實現接口的具體實例,比如下面的實現者:
public class PasswordResetHelper { public void ResetPassword() { IEmailSender mySender = new MyEmailSender(); //...Email Action... mySender.SendEmail(); } }
我們只是做了松耦合的一部分工作,PasswordResetHelper類通過IEmailSender來配置和發送郵件,通過接口的實現來創建對象,這里需要創建一個MyEmailSender的實例。但是我們卻讓事情更糟,因為現在的PasswordResetHelper同時依賴IEmailSender和MyEmailSender,如下圖5.
圖5.
其實現在我現在需要一種方式來獲取對象(指上面代碼里的mySender),這個對象是實現了我們指定的接口但不是去創建實現接口(這里指MyEmailSender)對象本身。對於這樣問題的解決方案,我們稱為依賴注入(dependency injection(DI)),也可以被認為是控制反轉。
DI(dependency injection)是完成或者說是松散耦合的一種模式。DI有分為兩個部分:一是刪除我們組件里任何對類有依賴的。那么上面我們的例子似乎可以成為下面的樣子:
public class PasswordResetHelper { private IEmailSender emailSender; public PasswordResetHelper(IEmailSender emailSenderParam) { emailSender = emailSenderParam; } public void ResetPassword() { //IEmailSender mySender = new MyEmailSender(); //...Email Action... emailSender.SendEmail(); } }
我們可以發現,沒有了MyEmailSender,這就意味着我們打破了PasswordResetHelper和MyEmailSender之間的依賴性。PasswordResetHelper的構造器需要一個對象作參數,而這個對象是IEmailSender接口的的實現,它不用去知道這個對象是什么,或者說它根本不用去關心,並且也不用負責去創建它。
依賴在運行時就被注入到了PasswordResetHelper里面,也意味着那些實現了IEmailSender接口的類的實例將會被創建,並在PasswordResetHelper實例化期間傳遞給它的構造器。這樣在PasswordResetHelper和任何實現了它需要的接口的類之間沒有編譯時的依賴。
用Asp.Net MVC實戰一個簡單依賴注入的例子
回到我之前做的競拍,接下就是將依賴注入應用到我們的競拍程序里面,我們的目標很簡單,創建一個controller命名為AdminController,我們使用MembersRepository來持久化,為了解決AdminController和MembersRepository之間的耦合,我們定義一個接口IMembersRepository.具體的代碼如下:
public interface IMembersRepository { public interface IMembersRepository { void AddMember(Member member); Member FetchByLoginName(string loginName); void SubmitChanges(); } public class MembersRepository : IMembersRepository { public void AddMember(Member member) { //member.LoginName = "Huitai"; //member.ReputationPoints = 100; } public Member FetchByLoginName(string loginName) { //自己寫實現code //Member m = new Member(); //....... //return m; return null; } public void SubmitChanges() { } } void AddMember(Member member); Member FetchByLoginName(string loginName); void SubmitChanges(); } public class MembersRepository : IMembersRepository { public void AddMember(Member member) { //member.LoginName = "Huitai"; //member.ReputationPoints = 100; } public Member FetchByLoginName(string loginName) { //自己寫實現code //Member m = new Member(); //....... //return m; return null; } public void SubmitChanges() { } }
是MVC那就寫個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(); // ... now render some view return this.View(); } }
AdminController類的要求IMembersRepositoryinterface接口的實現作為一個構造函數的參數。這將在運行時進行,允許AdminControllerto操作一個類的一個實例,該類實現接口沒有是耦合,實現。
關於MVC就先介紹這些吧!可能不完善,其他的知識后續繼續補充,大家共同學習。要是那里有描述錯誤還是寫錯的地方,還請各位高手多多指點,批評,指導。也願所有的新接觸的同學學習什么的時候多帶些問號?這樣大家才能學到更好技術,共同進步!