關于兩個MVC示例的思考(MVCStore和Oxite)
最近看了一些關于MVC框架的東西,加以之前就研究過一些關于 MVC架構的信息,碰巧在網上又看
到了這樣一篇文章,是關于微軟內部的開發者對Oxite項目的個人攻擊,讓我產生了寫篇文章來表達一
下自己對于這種架構模式的思考。
聲明,如果之前沒看過這兩個項目的朋友建議下載相應的源碼:
MVCStore:http://www.codeplex.com/mvcsamples
Oxite:http://www.codeplex.com/oxite
好了,開始今天的正文:)
1.Controller干了些什么
先說一下我的看法,這個所謂控制器的最大作用應該是“控制和調度”,控制即前臺視圖(view)的
顯示(顯示那個視圖), 調度即執行相應的業務邏輯 (在這兩個項目中就是那些Services,而Services
即完成對model數據模型的封裝調用,并實現相關的業務邏輯)。這里業務規則如何定義應該是在Ser-
vices里進行,與Controller無關。
就其工作性質而言還是比較簡單的,因此簡要的工作內容就應該有簡單的實現(指代碼),這里可以
看看MVCStore是如何搞的,請見下面代碼:
(摘自Commerce.MVC.Web"App"Controller"AuthenticationController.cs):
一看便知這是一個登陸驗證操作,其使用Request.Form方式從表單中獲取數據,這里暫不說其獲取的方式
優不優雅(因為與本文要聊的內容關系不大)。可以看出其實現的過程也之前采用webform方式開發出現的代碼
也差不多,只不過是將相應的login.aspx.cs中的操作放到這controller中,這種好處主要就是將原本分散但功
能上應該同屬于認證的類(Authentication類是按架構設時劃分出來的)放置在了一起,這樣在代碼分布上會
更合理一些。另外就是進行單元測試時也會很容易編寫測試代碼。當然還有好處,我想就是將那些經常變化的
代碼使用這種方式約束在了controller中,為將來的后續開發,特別是維護以及查找BUG上會有一個比較清晰的
范圍。
當然在看Oxite代碼時,這塊會有所差異,即Oxite使用了IModelBinder來實現將表單中的數據綁定到相應
的類上以完成Model中(實體)類的初始化綁定工作,如下:
其實這種做法有一定的好處,就是將這類在功能上類似的操作進行了封裝。比如大家可以想像通過web請求提交過來
一個大表單,上面有幾十個字段屬性(不要問我為什么會有這種大表單),而如何將這些字段綁定的操作與controller中
接下來的業務流程操作放在一起,會讓controller中的相應方法代碼長度增長過快,所以倒不如通過上面這個方法將相應
的實體類與Web頁面信息的綁定工作分離出來,當然 Oxite使用聲明相應UserModelBinder類的方式進行封裝的做法還
有待商榷。
說來說去,還是如Rob Conery所說的,要始終保持用Controller與View的輕快,易于測試這一原則。這一點其實正
與SOA架構中的一些思想相符合,下面結合我的理解來解釋一下:
在SOA中,有組件(component)的概念,即組件是業務邏輯的原子級功能操作,其按照高內聚低耦合的方式進行設計,
當業務流程開發運作時,其工作原理就是正確組合相應的業務組件來實現相應的應用。這樣的好處就是可以將這些組件分
布式布署,同時當業務流程發生變化時,只要調整相應的業務流程邏輯(soa中稱為bpel)即能夠快速響應業務變化。而
如何構造這種可重用的組件在SOA中也是有相應規范的,被稱為SCA.
說到這里有些遠了,那在MVC架構中又有什么類似的思想呢?其實在這里controller的一些設計要求與SOA中的bpel
有著相似的設計理念,即完成對業務組件(即MVCStore解決方案中的Commerce.Services項目下的相應文件)的流程
編排和調用。這樣即便將來需求變化,而導致了業務流程的變化(不是業務規則變化),也只是修改Controller這一層應
該可以滿足了,而正確而快速的修改的“前提”,應該就是該Controller應該設計得“盡可能的輕快”。
2.需求變化了,導致了業務規則變化怎么辦?
正如Ivar jacbson 在傳授“明智開發”模型時所說的那樣,“軟件開發中不變的是--需求的不斷變化”。這一點相信
大家是有強烈共鳴的。
這里我們先來簡單的看一下MVCStore和Oxite的處理方式,從設計思路上兩個項目基本一致,即使用接口分離方式來
應對這種變化。比如:Oxite"Services"中就有這樣的代碼:
其實現了IUserService服務接口。
而MVCStore中的Commerce.Services項目中的代碼也使用了類似接口定義,比如:
定義并實現這些服務接口之后,就可以通過IOC這類方式來實現最終的注入,以決定在程序運行時使用那些具體
實現類了,比如Oxite中的Oxite/ContainerFactory.cs是這樣進行注冊的(使用了Unity框架):
當然這種做法是有普遍性的,好處也是很明顯。就是將來如果業務規則變化時(對應service接口實現類
也要發生變化),這時不需要真正修改已有的代碼,只需再開發一個相應的實現類即可滿足需求,這種擴展
方式也是與設計模式中的思想相符合的。
說到這里,把話題再深入一下,就是微軟模式與實踐小組的Service Layer Guidelines中對象這塊還會
有一個Application Facade(在其Business層中),如下圖:

其完成的是對這些service組件的“應用層面級”封裝,說的再白一些,其可以包括對業務工作流,業務
實體,業務組件的三者的封裝。以便于對外實現(暴露)統一的服務訪問接口。就這部分而言,MVCStore
做的比Oxiete要好,其在工作流中對各類已定義的服務組件的邏輯調用寫的很有味道,比如Commerce.-
Services項目下的 AcceptPayPalWorkflow.cs 和 ShipOrderWorkflow.cs。
當然就目前工作流的作用遠不止這些,必定其也可以采用WCF服務的方式把自己暴露給外界。就這一
點,其自身也可以轉化為一個服務組件,到這里就出現了一個有趣的現象,即:
已將一些服務組件囊括的工作流自己也成了一個服務組件而被其它服務組件所調用。不是嗎?
在SOA架構中,這種情況是很普遍的,因為組件是一些基本的業務規則邏輯,其應允許被其它組件訪問
甚至包含以使業務規則更加清晰,說白了就是可復用性。
對開發者而言只有這樣才可能提升開發速度(重用已有組件的好處不僅僅是少寫代碼,還包括測試和布
署等方面的成本也會降低),這一點想一想那些開源的框架就會理解了。而對于企業管理者而言就是保護“
已有投資”
3.兩個項目中的困惑
的確,看了這兩個MVC之后,還是有些讓我感覺不是太清晰的地方,比如MVCStore中,Commerce.Data
項目下的Model/Order.cs類,我剛開始一看,還真被震住了,很有充血模型的味,下面是部分代碼:
Code
可正當我帶著興趣去觀察其它相應的域模型類時,又回到了貧血域模型。不是嗎?的方法是要感覺好像
不是一個開發人員寫的才會出現這種情況,因為按其架構設計上來看,這個類中被放到Serivce中實現的,
因為我不是該項目的開發人員,想不出個所以然來。
白乎了這些,其它在這兩個項目中還有一些差異,當然本文開頭提到的那篇文章也說出了一些“問題”。
不過還是那句話,沒有最好的設計只有最適合的設計,這兩個項目都有可圈可點的地方,但對自己所在公
司部門是不是“完全適合”只能結合自己團隊的情況而定了。
比如說關于Commerce.MVC.Web中將controller和view放在了一起,就是個問題,比如在團隊中
有如下分工:
VIEW開發人員 + Controller開發人員 + Service組件開發
那么將View目錄與Controller目錄放在不同的項目中應該是個不錯的方式,起碼在項目級別上將這
兩類開發者進行了分離。當然有人會說,一般情況下VIEW 和Controller的設計者應該是一個人而不是
兩個人, 但分工明確才能盡一步提升生產力,特別是MVC這個框架還很新,有些開發人員學習是從View
語法入手,有些人從Controller入手,有些人比如我是從Service入手。這就導致關注和側重點不同,最
后導致自己的理解和優勢也會不同。將View分離出來的好處在于發揮各自的優勢,讓前臺開發人員可以
將精力放在與UI設計師交流設計實現,界面實現,js(目前是JQuery)封裝調用等方面。相信隨著項目
的不斷擴大和開發人員的后續補充勢必會造成這樣的問題。
好了,今天的內容就先到這里了。
原文鏈接:http://www.rzrgm.cn/daizhj/archive/2009/02/26/1398689.html
作者: daizhj, 代震軍
Tags: soa,mvc,sca,bpel
網址: http://daizhj.cnblogs.com
到了這樣一篇文章,是關于微軟內部的開發者對Oxite項目的個人攻擊,讓我產生了寫篇文章來表達一
下自己對于這種架構模式的思考。
聲明,如果之前沒看過這兩個項目的朋友建議下載相應的源碼:
MVCStore:http://www.codeplex.com/mvcsamples
Oxite:http://www.codeplex.com/oxite
好了,開始今天的正文:)
1.Controller干了些什么
先說一下我的看法,這個所謂控制器的最大作用應該是“控制和調度”,控制即前臺視圖(view)的
顯示(顯示那個視圖), 調度即執行相應的業務邏輯 (在這兩個項目中就是那些Services,而Services
即完成對model數據模型的封裝調用,并實現相關的業務邏輯)。這里業務規則如何定義應該是在Ser-
vices里進行,與Controller無關。
就其工作性質而言還是比較簡單的,因此簡要的工作內容就應該有簡單的實現(指代碼),這里可以
看看MVCStore是如何搞的,請見下面代碼:
(摘自Commerce.MVC.Web"App"Controller"AuthenticationController.cs):
public class AuthenticationController : Controller
{

.
public ActionResult Login()
{
string oldUserName = this.GetUserName();
string login = Request.Form["login"];
string password = Request.Form["password"];
if (!String.IsNullOrEmpty(login) && !String.IsNullOrEmpty(password))
{
var svc = new AspNetAuthenticationService();
bool isValid = svc.IsValidLogin(login, password);
//log them in
if (isValid)
{
SetPersonalizationCookie(login, login);
//migrate the current order
_orderService.MigrateCurrentOrder(oldUserName, login);
return AuthAndRedirect(login);
}
}
return View();
}


}
{

.public ActionResult Login()
{
string oldUserName = this.GetUserName();
string login = Request.Form["login"];
string password = Request.Form["password"];
if (!String.IsNullOrEmpty(login) && !String.IsNullOrEmpty(password))
{
var svc = new AspNetAuthenticationService();
bool isValid = svc.IsValidLogin(login, password);
//log them in
if (isValid)
{
SetPersonalizationCookie(login, login);
//migrate the current order
_orderService.MigrateCurrentOrder(oldUserName, login);
return AuthAndRedirect(login);
}
}
return View();
}


}
一看便知這是一個登陸驗證操作,其使用Request.Form方式從表單中獲取數據,這里暫不說其獲取的方式
優不優雅(因為與本文要聊的內容關系不大)。可以看出其實現的過程也之前采用webform方式開發出現的代碼
也差不多,只不過是將相應的login.aspx.cs中的操作放到這controller中,這種好處主要就是將原本分散但功
能上應該同屬于認證的類(Authentication類是按架構設時劃分出來的)放置在了一起,這樣在代碼分布上會
更合理一些。另外就是進行單元測試時也會很容易編寫測試代碼。當然還有好處,我想就是將那些經常變化的
代碼使用這種方式約束在了controller中,為將來的后續開發,特別是維護以及查找BUG上會有一個比較清晰的
范圍。
當然在看Oxite代碼時,這塊會有所差異,即Oxite使用了IModelBinder來實現將表單中的數據綁定到相應
的類上以完成Model中(實體)類的初始化綁定工作,如下:
public class UserModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
NameValueCollection form = controllerContext.HttpContext.Request.Form;
User user = null;
Guid siteID = Guid.Empty;
if (!string.IsNullOrEmpty(form["siteID"]))
{
form["siteID"].GuidTryParse(out siteID);
}
if (siteID == Guid.Empty)
{
user = new User
{
Name = form["userName"],
Email = form["userEmail"],
DisplayName = form["userDisplayName"],
Password = form["userPassword"]
};
Guid userID;
if (!string.IsNullOrEmpty(form["userID"]) && form["userID"].GuidTryParse(out userID))
{
user.ID = userID;
}
}
return user;
}
}
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
NameValueCollection form = controllerContext.HttpContext.Request.Form;
User user = null;
Guid siteID = Guid.Empty;
if (!string.IsNullOrEmpty(form["siteID"]))
{
form["siteID"].GuidTryParse(out siteID);
}
if (siteID == Guid.Empty)
{
user = new User
{
Name = form["userName"],
Email = form["userEmail"],
DisplayName = form["userDisplayName"],
Password = form["userPassword"]
};
Guid userID;
if (!string.IsNullOrEmpty(form["userID"]) && form["userID"].GuidTryParse(out userID))
{
user.ID = userID;
}
}
return user;
}
}
其實這種做法有一定的好處,就是將這類在功能上類似的操作進行了封裝。比如大家可以想像通過web請求提交過來
一個大表單,上面有幾十個字段屬性(不要問我為什么會有這種大表單),而如何將這些字段綁定的操作與controller中
接下來的業務流程操作放在一起,會讓controller中的相應方法代碼長度增長過快,所以倒不如通過上面這個方法將相應
的實體類與Web頁面信息的綁定工作分離出來,當然 Oxite使用聲明相應UserModelBinder類的方式進行封裝的做法還
有待商榷。
說來說去,還是如Rob Conery所說的,要始終保持用Controller與View的輕快,易于測試這一原則。這一點其實正
與SOA架構中的一些思想相符合,下面結合我的理解來解釋一下:
在SOA中,有組件(component)的概念,即組件是業務邏輯的原子級功能操作,其按照高內聚低耦合的方式進行設計,
當業務流程開發運作時,其工作原理就是正確組合相應的業務組件來實現相應的應用。這樣的好處就是可以將這些組件分
布式布署,同時當業務流程發生變化時,只要調整相應的業務流程邏輯(soa中稱為bpel)即能夠快速響應業務變化。而
如何構造這種可重用的組件在SOA中也是有相應規范的,被稱為SCA.
說到這里有些遠了,那在MVC架構中又有什么類似的思想呢?其實在這里controller的一些設計要求與SOA中的bpel
有著相似的設計理念,即完成對業務組件(即MVCStore解決方案中的Commerce.Services項目下的相應文件)的流程
編排和調用。這樣即便將來需求變化,而導致了業務流程的變化(不是業務規則變化),也只是修改Controller這一層應
該可以滿足了,而正確而快速的修改的“前提”,應該就是該Controller應該設計得“盡可能的輕快”。
2.需求變化了,導致了業務規則變化怎么辦?
正如Ivar jacbson 在傳授“明智開發”模型時所說的那樣,“軟件開發中不變的是--需求的不斷變化”。這一點相信
大家是有強烈共鳴的。
這里我們先來簡單的看一下MVCStore和Oxite的處理方式,從設計思路上兩個項目基本一致,即使用接口分離方式來
應對這種變化。比如:Oxite"Services"中就有這樣的代碼:
public class UserService : IUserService
{
private readonly IUserRepository repository;
private readonly IValidationService validator;
public UserService(IUserRepository repository, IValidationService validator)
{
this.repository = repository;
this.validator = validator;
}
#region IUserService Members
public User GetUser(string name)
{
return repository.GetUser(name);
}
public User GetUser(string name, string password)
{
User user = string.Compare(name, "Anonymous", true) != 0 ? repository.GetUser(name) : null;
if (user != null && user.Password == saltAndHash(password, user.PasswordSalt))
return user;
return null;
}
public void AddUser(User user, out ValidationStateDictionary validationState, out User newUser)
{
validationState = new ValidationStateDictionary();
validationState.Add(typeof(User), validator.Validate(user));
if (!validationState.IsValid)
{
newUser = null;
return;
}
.
}
{
private readonly IUserRepository repository;
private readonly IValidationService validator;
public UserService(IUserRepository repository, IValidationService validator)
{
this.repository = repository;
this.validator = validator;
}
#region IUserService Members
public User GetUser(string name)
{
return repository.GetUser(name);
}
public User GetUser(string name, string password)
{
User user = string.Compare(name, "Anonymous", true) != 0 ? repository.GetUser(name) : null;
if (user != null && user.Password == saltAndHash(password, user.PasswordSalt))
return user;
return null;
}
public void AddUser(User user, out ValidationStateDictionary validationState, out User newUser)
{
validationState = new ValidationStateDictionary();
validationState.Add(typeof(User), validator.Validate(user));
if (!validationState.IsValid)
{
newUser = null;
return;
}
.}
其實現了IUserService服務接口。
而MVCStore中的Commerce.Services項目中的代碼也使用了類似接口定義,比如:
[Serializable]
public class OrderService : Commerce.Services.IOrderService {
IOrderRepository _orderRepository;
ICatalogRepository _catalogRepository;
IShippingRepository _shippingRepository;
IShippingService _shippingService;
public OrderService() { }
public OrderService(IOrderRepository rep, ICatalogRepository catalog,
IShippingRepository shippingRepository, IShippingService shippingService)
{
_orderRepository = rep;
_catalogRepository = catalog;
_shippingRepository = shippingRepository;
_shippingService = shippingService;
}
/// <summary>
/// Gets all orders in the system
/// </summary>
/// <returns></returns>
public IList<Order> GetOrders() {
return _orderRepository.GetOrders().ToList();
}
.
}
public class OrderService : Commerce.Services.IOrderService {
IOrderRepository _orderRepository;
ICatalogRepository _catalogRepository;
IShippingRepository _shippingRepository;
IShippingService _shippingService;
public OrderService() { }
public OrderService(IOrderRepository rep, ICatalogRepository catalog,
IShippingRepository shippingRepository, IShippingService shippingService)
{
_orderRepository = rep;
_catalogRepository = catalog;
_shippingRepository = shippingRepository;
_shippingService = shippingService;
}
/// <summary>
/// Gets all orders in the system
/// </summary>
/// <returns></returns>
public IList<Order> GetOrders() {
return _orderRepository.GetOrders().ToList();
}
.}
定義并實現這些服務接口之后,就可以通過IOC這類方式來實現最終的注入,以決定在程序運行時使用那些具體
實現類了,比如Oxite中的Oxite/ContainerFactory.cs是這樣進行注冊的(使用了Unity框架):
public IUnityContainer GetOxiteContainer()
{
IUnityContainer parentContainer = new UnityContainer();
parentContainer
.RegisterInstance(new AppSettingsHelper(ConfigurationManager.AppSettings))
.RegisterInstance(RouteTable.Routes)
.RegisterInstance(HostingEnvironment.VirtualPathProvider)
.RegisterInstance("RegisterRoutesHandler", typeof(MvcRouteHandler));
foreach (ConnectionStringSettings connectionString in ConfigurationManager.ConnectionStrings)
{
parentContainer.RegisterInstance(connectionString.Name, connectionString.ConnectionString);
}
parentContainer
.RegisterType<ISiteService, SiteService>()
.RegisterType<IPluginService, PluginService>()
.RegisterType<IUserService, UserService>()
.RegisterType<ITagService, TagService>()
.RegisterType<IPostService, PostService>()
.RegisterType<ITrackbackOutboundService, TrackbackOutboundService>()
..
}
{
IUnityContainer parentContainer = new UnityContainer();
parentContainer
.RegisterInstance(new AppSettingsHelper(ConfigurationManager.AppSettings))
.RegisterInstance(RouteTable.Routes)
.RegisterInstance(HostingEnvironment.VirtualPathProvider)
.RegisterInstance("RegisterRoutesHandler", typeof(MvcRouteHandler));
foreach (ConnectionStringSettings connectionString in ConfigurationManager.ConnectionStrings)
{
parentContainer.RegisterInstance(connectionString.Name, connectionString.ConnectionString);
}
parentContainer
.RegisterType<ISiteService, SiteService>()
.RegisterType<IPluginService, PluginService>()
.RegisterType<IUserService, UserService>()
.RegisterType<ITagService, TagService>()
.RegisterType<IPostService, PostService>()
.RegisterType<ITrackbackOutboundService, TrackbackOutboundService>()
..}
當然這種做法是有普遍性的,好處也是很明顯。就是將來如果業務規則變化時(對應service接口實現類
也要發生變化),這時不需要真正修改已有的代碼,只需再開發一個相應的實現類即可滿足需求,這種擴展
方式也是與設計模式中的思想相符合的。
說到這里,把話題再深入一下,就是微軟模式與實踐小組的Service Layer Guidelines中對象這塊還會
有一個Application Facade(在其Business層中),如下圖:

其完成的是對這些service組件的“應用層面級”封裝,說的再白一些,其可以包括對業務工作流,業務
實體,業務組件的三者的封裝。以便于對外實現(暴露)統一的服務訪問接口。就這部分而言,MVCStore
做的比Oxiete要好,其在工作流中對各類已定義的服務組件的邏輯調用寫的很有味道,比如Commerce.-
Services項目下的 AcceptPayPalWorkflow.cs 和 ShipOrderWorkflow.cs。
當然就目前工作流的作用遠不止這些,必定其也可以采用WCF服務的方式把自己暴露給外界。就這一
點,其自身也可以轉化為一個服務組件,到這里就出現了一個有趣的現象,即:
已將一些服務組件囊括的工作流自己也成了一個服務組件而被其它服務組件所調用。不是嗎?
在SOA架構中,這種情況是很普遍的,因為組件是一些基本的業務規則邏輯,其應允許被其它組件訪問
甚至包含以使業務規則更加清晰,說白了就是可復用性。
對開發者而言只有這樣才可能提升開發速度(重用已有組件的好處不僅僅是少寫代碼,還包括測試和布
署等方面的成本也會降低),這一點想一想那些開源的框架就會理解了。而對于企業管理者而言就是保護“
已有投資”
3.兩個項目中的困惑
的確,看了這兩個MVC之后,還是有些讓我感覺不是太清晰的地方,比如MVCStore中,Commerce.Data
項目下的Model/Order.cs類,我剛開始一看,還真被震住了,很有充血模型的味,下面是部分代碼:
可正當我帶著興趣去觀察其它相應的域模型類時,又回到了貧血域模型。不是嗎?的方法是要感覺好像
不是一個開發人員寫的才會出現這種情況,因為按其架構設計上來看,這個類中被放到Serivce中實現的,
因為我不是該項目的開發人員,想不出個所以然來。
白乎了這些,其它在這兩個項目中還有一些差異,當然本文開頭提到的那篇文章也說出了一些“問題”。
不過還是那句話,沒有最好的設計只有最適合的設計,這兩個項目都有可圈可點的地方,但對自己所在公
司部門是不是“完全適合”只能結合自己團隊的情況而定了。
比如說關于Commerce.MVC.Web中將controller和view放在了一起,就是個問題,比如在團隊中
有如下分工:
VIEW開發人員 + Controller開發人員 + Service組件開發
那么將View目錄與Controller目錄放在不同的項目中應該是個不錯的方式,起碼在項目級別上將這
兩類開發者進行了分離。當然有人會說,一般情況下VIEW 和Controller的設計者應該是一個人而不是
兩個人, 但分工明確才能盡一步提升生產力,特別是MVC這個框架還很新,有些開發人員學習是從View
語法入手,有些人從Controller入手,有些人比如我是從Service入手。這就導致關注和側重點不同,最
后導致自己的理解和優勢也會不同。將View分離出來的好處在于發揮各自的優勢,讓前臺開發人員可以
將精力放在與UI設計師交流設計實現,界面實現,js(目前是JQuery)封裝調用等方面。相信隨著項目
的不斷擴大和開發人員的后續補充勢必會造成這樣的問題。
好了,今天的內容就先到這里了。
原文鏈接:http://www.rzrgm.cn/daizhj/archive/2009/02/26/1398689.html
作者: daizhj, 代震軍
Tags: soa,mvc,sca,bpel
網址: http://daizhj.cnblogs.com

浙公網安備 33010602011771號