最近看了一些关于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):
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();
}
}
一看便知这是一个登陆验证操作,其使用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;
}
}
其实这种做法有一定的好处,就是将这类在功能上类似的操作进行了封装。比如大家可以想像通过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;
}
.
}
其实现了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;
}
///
/// Gets all orders in the system
///
///
public IList 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()
.RegisterType()
.RegisterType()
.RegisterType()
.RegisterType()
.RegisterType()
..
}
当然这种做法是有普遍性的,好处也是很明显。就是将来如果业务规则变化时(对应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
[Serializable]
public class Order {
public Guid ID { get; set; }
public string OrderNumber { get; set; }
public string UserName { get; set; }
public DateTime DateCreated { get; set; }
public LazyList Items { get; set; }
public LazyList Transactions { get; set; }
public string UserLanguageCode { get; set; }
public OrderStatus Status { get; set; }
public Address ShippingAddress { get; set; }
public Address BillingAddress { get; set; }
public ShippingMethod ShippingMethod { get; set; }
public decimal TaxAmount { get; set; }
public PaymentMethod PaymentMethod { get; set; }
public DateTime? DateShipped { get; set; }
public DateTime? EstimatedDelivery { get; set; }
public string TrackingNumber { get; set; }
public LazyList IncentivesUsed { get; set; }
public Order():this("","") {
}
public Order(string userName)
: this("",userName) {
}
public Order(string orderNumber, string userName) {
this.OrderNumber = orderNumber;
this.UserName = userName;
this.Status = OrderStatus.NotCheckoutOut;
this.Items=new LazyList();
this.IncentivesUsed = new LazyList();
this.ID = Guid.NewGuid();
this.DiscountAmount = 0;
this.DiscountReason = "--";
}
///
/// Adds a product to the cart
///
public void AddItem(Product product) {
AddItem(product, 1);
}
///
/// Removes all items from cart
///
public void ClearItems() {
this.Items.Clear();
}
///
/// Adds a product to the cart
///
public void AddItem(Product product, int quantity) {
//see if this item is in the cart already
OrderItem item = FindItem(product);
if (quantity != 0) {
if (item != null) {
//if the passed in amount is 0, do nothing
//as we're assuming "add 0 of this item" means
//do nothing
if (quantity != 0)
AdjustQuantity(product, item.Quantity + quantity);
} else {
if (quantity > 0) {
item = new OrderItem(this.ID,product, quantity);
//add to list
this.Items.Add(item);
}
}
}
}
///
/// Adjusts the quantity of an item in the cart
///
public void AdjustQuantity(Product product, int newQuantity) {
OrderItem itemToAdjust = FindItem(product);
if (itemToAdjust != null) {
if (newQuantity |