最近看了一些关于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;
}
///
<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
>
()
..
}
当然这种做法是有普遍性的,好处也是很明显。就是将来如果业务规则变化时(对应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<OrderItem> Items { get; set; }
public LazyList<Transaction> 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<IIncentive> 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<OrderItem>();
this.IncentivesUsed = new LazyList<IIncentive>();
this.ID = Guid.NewGuid();
this.DiscountAmount = 0;
this.DiscountReason = "--";
}
/// <summary>
/// Adds a product to the cart
/// </summary>
public void AddItem(Product product) {
AddItem(product, 1);
}
/// <summary>
/// Removes all items from cart
/// </summary>
public void ClearItems() {
this.Items.Clear();
}
/// <summary>
/// Adds a product to the cart
/// </summary>
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);
}
}
}
}
/// <summary>
/// Adjusts the quantity of an item in the cart
/// </summary>
public void AdjustQuantity(Product product, int newQuantity) {
OrderItem itemToAdjust = FindItem(product);
if (itemToAdjust != null) {
if (newQuantity <= 0) {
this.RemoveItem(product);
} else {
itemToAdjust.Quantity = newQuantity;
}
}
}
/// <summary>
/// Remmoves a product from the cart
/// </summary>
public void RemoveItem(Product product) {
RemoveItem(product.ID);
}
/// <summary>
/// Remmoves a product from the cart
/// </summary>
public void RemoveItem(int productID) {
var itemToRemove = FindItem(productID);
if (itemToRemove != null) {
this.Items.Remove(itemToRemove);
}
}
/// <summary>
/// Finds an item in the cart
/// </summary>
/// <param name="product"></param>
/// <returns></returns>
public OrderItem FindItem(Product product) {
OrderItem result = null;
if (product != null) {
//see if this item is in the cart already
return FindItem(product.ID);
}
return result;
}
/// <summary>
/// Finds an item in the cart
/// </summary>
/// <param name="productID">The product id to find</param>
/// <returns></returns>
public OrderItem FindItem(int productID) {
this.Items = this.Items ?? new LazyList<OrderItem>();
//see if this item is in the cart already
return (from si in this.Items
where si.Product.ID == productID
select si).SingleOrDefault();
}
..
可正当我带着兴趣去观察其它相应的域模型类时,又回到了贫血域模型。不是吗?的方法是要感觉好像
不是一个开发人员写的才会出现这种情况,因为按其架构设计上来看,这个类中被放到Serivce中实现的,
因为我不是该项目的开发人员,想不出个所以然来。
白乎了这些,其它在这两个项目中还有一些差异,当然本文开头提到的那篇文章也说出了一些“问题”。
不过还是那句话,没有最好的设计只有最适合的设计,这两个项目都有可圈可点的地方,但对自己所在公
司部门是不是“完全适合”只能结合自己团队的情况而定了。
比如说关于Commerce.MVC.Web中将controller和view放在了一起,就是个问题,比如在团队中
有如下分工:
VIEW开发人员 + Controller开发人员 + Service组件开发
那么将View目录与Controller目录放在不同的项目中应该是个不错的方式,起码在项目级别上将这
两类开发者进行了分离。当然有人会说,一般情况下VIEW 和Controller的设计者应该是一个人而不是
两个人, 但分工明确才能尽一步提升生产力,特别是MVC这个框架还很新,有些开发人员学习是从View
语法入手,有些人从Controller入手,有些人比如我是从Service入手。这就导致关注和侧重点不同,最
后导致自己的理解和优势也会不同。将View分离出来的好处在于发挥各自的优势,让前台开发人员可以
将精力放在与UI设计师交流设计实现,界面实现,js(目前是JQuery)封装调用等方面。相信随着项目
的不断扩大和开发人员的后续补充势必会造成这样的问题。
好了,今天的内容就先到这里了。
原文链接:http://www.cnblogs.com/daizhj/archive/2009/02/26/1398689.html