[ASP.Net]翻译-ASP.NET MVC Contact Manager开发之旅迭代4 -

本翻译系列为asp.net mvc官方实 例教程。在这个系列中,Stephen Walther将演示如何通过ASP.NET MVC framework结合单元测试、TDD、Ajax、软件设计原则及设计模式创建一个完整的Contact Manager应用。本系列共七个章节,也是七次迭代过程。本人将陆续对其进行翻译并发布出来,希望能对学习ASP.NET MVC 的各位有所帮助。由于本人也是个MVC菜鸟,且E文水平亦是平平,文中如有疏漏敬请见谅。
注:为保证可读性,文中Controller、View、Model、Route、Action等ASP.NET MVC核心单词均未翻译。

 

ContactManager开发之旅-索引页

ContactManager开发之旅 迭代1 – 创建应用程序

ContactManager开发之旅 迭代2 – 修改样式,美化应用

ContactManager开发之旅 迭代3 – 验证表单

迭代4 利用设计模式松散耦合

本次迭代

这是ContactManager的第四次迭代,本次迭代中我们将重构应用程序,通过合理的利用设计模式松散其耦合。松耦合的程序更有弹性,更易维 护。当应用程序面临改动时,你只需修改某一部分的代码,而不会出现大量修改与其耦合严重的相关代码这种牵一发而动全身的情况。

在当前的ContactManager应用中,所有的数据存储及验证逻辑都分布在controller类中,这并不是个好主意。要知道这种情况下一 旦你需要修改其中一部分代码,你将同时面临为其他部分增加bug的风险。比如你需要修改验证逻辑,你就必须承担数据存储或controller部分的逻辑 会随之出现问题的风险。

(SRP-单一职责原则), 就一个类而言,应该只专注于做一件事和仅有一个引起它变化的原因。将controller、验证及数据存储逻辑耦合在一起严重的违反了SRP。

需求的变更,个人想法的升华、始料未及的情况如此频繁,你不得不就当前的应用作出一些改变:为应用程序添加新功能、修复bug、修改应用中某个功能的实现等。就应用程序而言,它们很难处于一种静止不动的状态,他们无时无刻在被不停的改变、改善。

现在的情况是,ContactManager应用中使用了Microsoft Entity Framework处理数据通信。想象一下,你决定对数据存储层实现做出一些改变,你希望使用其它的一些方案:如ADP.NET Data Services或NHibernate。由于数据存储相关的代码并不独立于验证及controller中的代码,你将无法避免修改那些原本应该毫无干系 的代码。

而另一方面,对于一个松耦合的程序来说,上面的问题就不存在了。一个经典的场景是:你可以随意切换数据存储方案而不用管验证逻辑、controller中的那些劳什子代码。

在这次迭代中,我们将利用软件设计模式的优点重构我们的Contact Manager应用程序,使其符合我们上面提到的“松耦合”的要求。尽管做完这些以后,我们的应用程序并不会表现的与以往有任何不同,但是我们从此便可轻松驾驭其未来的维护及修改过程。

重构就是指在不改变程序外在行为的前提下,对代码做出修改,改进程序内部结构的过程。

 

使用Repository模式

我们的第一个改动便是使用叫做Repository的设计模式改善我们的应用。我们将使用这个模式将数据存储相关的代码与我们应用中的其他逻辑独立开来。

要实现Repository模式,我们需要做以下两件事

  1. 新建一个接口
  2. 新建一个类实现上面的接口。

首先,我们需要新建一个接口约定所有我们需要实现的数据存储方法。IContactManagerRepository接口代码如下。这个接口约定 了五个方法:CreateContact()、DeleteContact()、EditContact()、GetContact()及 ListContacts()方法:

using System;
using System.Collections.Generic;
namespace ContactManager.Models
{
public interface IContactRepository
{
Contact CreateContact(Contact contactToCreate);
void DeleteContact(Contact contactToDelete);
Contact EditContact(Contact contactToUpdate);
Contact GetContact(int id);
IEnumerable<Contact> ListContacts();
}
}

接着,我们需要新建一个具体的类来实现IContactManagerRepositoyr接口。由于我们这里使用Microsoft Entity Framework操作数据库,所以我们为这个类命名为“EntityContactManagerRepository”,这个类的代码如下:

using System.Collections.Generic;
using System.Linq;
namespace ContactManager.Models
{
public class EntityContactManagerRepository : ContactManager.Models.IContactManagerRepository
{
private ContactManagerDBEntities _entities = new ContactManagerDBEntities();
public Contact GetContact(int id)
{
return (from c in _entities.ContactSet
where c.Id == id
select c).FirstOrDefault();
}
public IEnumerable<Contact> ListContacts()
{
return _entities.ContactSet.ToList();
}
public Contact CreateContact(Contact contactToCreate)
{
_entities.AddToContactSet(contactToCreate);
_entities.SaveChanges();
return contactToCreate;
}
public Contact EditContact(Contact contactToEdit)
{
var originalContact = GetContact(contactToEdit.Id);
_entities.ApplyPropertyChanges(originalContact.EntityKey.EntitySetName, contactToEdit);
_entities.SaveChanges();
return contactToEdit;
}
public void DeleteContact(Contact contactToDelete)
{
var originalContact = GetContact(contactToDelete.Id);
_entities.DeleteObject(originalContact);
_entities.SaveChanges();
}
}
}

注意,EntityContactManagerRepository类实现了IContactManagerRepository接口约定的5个方法。

为什么我们一定要要建立个接口再建立一个类来实现它呢?

应用程序中的其他部分将与接口而不是具体的类进行交互。也就是说,它们将调用接口声明方法而不是具体的类中的方法。

所以,我们可以以一个新的类实现某个接口但不用修改应用程序中其他的部分。例如,将来我们可能需要建立一个 DataServicesContactManagerRepository类实现IContactManagerRepository接口。 DataServicesContactManagerRepository类使用ADO.NET Data Services,我们用它代替Microsoft Entity Framework.与数据库通信进行数据存储。

如果我们的应用程序代码是基于IContactManagerRepository接口而不是 EntityContactManagerRepository这个具体的类,那么我们可以只改变不同的类名而非代码中的其他部分。例如我们可以将 EntityContactManagerRepository修改成DataServicesContactManagerRepository而不用 去碰数据存储和验证逻辑相关的代码。

面向接口(虚类)编程使我们的应用程序更有弹性,更易修改。

通过在VS中选择“重构”菜单->“提取接口”,你可以根据一个具体的类方便快速的创建出一个与之对应的接口。例如你可以先建立一个 EntityContactManagerRepository类,然后使用如上文所述的方法自动生成 IContactManagerRepository接口。

使用依赖注入

现在,我们已经将数据访问相关的代码独立到了Repository类中。而后,我们需要修改Contact controller以适应这些改变。这里我们将使用依赖注入的方式。

修改后的Contact controller代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Ajax;
using ContactManager.Models;
namespace ContactManager.Controllers
{
public class ContactController : Controller
{
private IContactManagerRepository _repository;
public ContactController()
: this(new EntityContactManagerRepository()) { }
public ContactController(IContactManagerRepository repository)
{
_repository = repository;
}
protected void ValidateContact(Contact contactToValidate)
{
if (contactToValidate.FirstName.Trim().Length == 0)
ModelState.AddModelError("FirstName", "First name is required.");
if (contactToValidate.LastName.Trim().Length == 0)
ModelState.AddModelError("LastName", "Last name is required.");
if (contactToValidate.Phone.Length > 0 && !Regex.IsMatch(contactToValidate.Phone, @"((\(\d{3}\) ?)|(\d{3}-))?\d{3}-\d{4}"))
ModelState.AddModelError("Phone", "Invalid phone number.");
if (contactToValidate.Email.Length > 0 && !Regex.IsMatch(contactToValidate.Email, @"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$"))
ModelState.AddModelError("Email", "Invalid email address.");
}
//
// GET: /Home/
public ActionResult Index()
{
return View(_repository.ListContacts());
}
//
// GET: /Home/Details/5
public ActionResult Details(int id)
{
return View();
}
//
// GET: /Home/Create
public ActionResult Create()
{
return View();
}
//
// POST: /Home/Create
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create([Bind(Exclude = "Id")] Contact contactToCreate)
{
//Validation logic
ValidateContact(contactToCreate);
if (!ModelState.IsValid)
{
return View();
}
else
{
try
{
_repository.CreateContact(contactToCreate);
return RedirectToAction("Index");
}
catch
{
return View();
}
}
}
//
// GET: /Home/Edit/5
public ActionResult Edit(int id)
{
return View(_repository.GetContact(id));
}
//
// POST: /Home/Edit/5
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(Contact contactToEdit)
{
ValidateContact(contactToEdit);
if (!ModelState.IsValid)
return View();
try
{
_repository.EditContact(contactToEdit);
return RedirectToAction("Index");
}
catch
{
return View();
}
}
//
// GET: /Home/Delete/5
public ActionResult Delete(int id)
{
return View(_repository.GetContact(id));
}
//
// POST: /Home/Delete/5
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Delete(Contact contactToDelete)
{
try
{
_repository.DeleteContact(contactToDelete);
return RedirectToAction("Index");
}
catch
{
return View();
}
}
}
}

注意上面代码中,Contact controller包含两个构造函数。第一个构造函数向第二个构造函数传递一个基于IContactManagerRepository接口的实例。这就是“构造子注入”。

EntityContactManagerRepository类仅仅在第一个构造函数中被使用。其他的地方一律使用IContactManagerRepository接口代替确切的EntityContactManagerRepository类。

在这种情况下,如果以后我们想改变IContactManagerRepository的实现也就很方便了。比如你想使用 DataServicesContactRepository类代替EntityContactManagerRepository类,则只需要修改第一 个构造函数即可。

不仅如此,构造子注入更使Contact controller的可测试性变得更强。在你的单元测试用,你可以通过传递一个mock的IContactManagerRepository的实现进 而实例化Contact controller。依赖注入所带来的特性将在我们对Contact Manager的下一次迭代—进行单元测试—时显得非常重要。

如果你希望将Contact controller类与具体的IContactManagerRepository接口的实现彻底解耦,则可以使用一些支持依赖注入的框架,如 StructureMap或Microsoft Entity Framework (MEF)。有了这些依赖注入框架的帮忙,你就不必在代码中面向具体的类了。

建立service层

你应该注意到了,我们的验证逻辑仍与上面代码中修改过的controller逻辑混合在一起。像我们独立数据存储逻辑一样,将验证逻辑独立出来同样是个好注意。

So,我们应当建立service层。在这里,它作为独立的一层以衔接controller和repository类。service层应当包括所有的业务逻辑,我们的验证逻辑当然也不例外。

ContactManagerService的代码如下,我们将验证逻辑转移到了这里:

using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Web.Mvc;
using ContactManager.Models.Validation;
namespace ContactManager.Models
{
public class ContactManagerService : IContactManagerService
{
private IValidationDictionary _validationDictionary;
private IContactManagerRepository _repository;
public ContactManagerService(IValidationDictionary validationDictionary)
: this(validationDictionary, new EntityContactManagerRepository())
{ }
public ContactManagerService(IValidationDictionary validationDictionary, IContactManagerRepository repository)
{
_validationDictionary = validationDictionary;
_repository = repository;
}
public bool ValidateContact(Contact contactToValidate)
{
if (contactToValidate.FirstName.Trim().Length == 0)
_validationDictionary.AddError("FirstName", "First name is required.");
if (contactToValidate.LastName.Trim().Length == 0)
_validationDictionary.AddError("LastName", "Last name is required.");
if (contactToValidate.Phone.Length > 0 && !Regex.IsMatch(contactToValidate.Phone, @"((\(\d{3}\) ?)|(\d{3}-))?\d{3}-\d{4}"))
_validationDictionary.AddError("Phone", "Invalid phone number.");
if (contactToValidate.Email.Length > 0 && !Regex.IsMatch(contactToValidate.Email, @"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$"))
_validationDictionary.AddError("Email", "Invalid email address.");
return _validationDictionary.IsValid;
}
#region IContactManagerService Members
public bool CreateContact(Contact contactToCreate)
{
// Validation logic
if (!ValidateContact(contactToCreate))
return false;
// Database logic
try
{
_repository.CreateContact(contactToCreate);
}
catch
{
return false;
}
return true;
}
public bool EditContact(Contact contactToEdit)
{
// Validation logic
if (!ValidateContact(contactToEdit))
return false;
// Database logic
try
{
_repository.EditContact(contactToEdit);
}
catch
{
return false;
}
return true;
}
public bool DeleteContact(Contact contactToDelete)
{
try
{
_repository.DeleteContact(contactToDelete);
}
catch
{
return false;
}
return true;
}
public Contact GetContact(int id)
{
return _repository.GetContact(id);
}
public IEnumerable<Contact> ListContacts()
{
return _repository.ListContacts();
}
#endregion
}
}

需要注意的是,ContactManagerService的构造函数中需要一个ValidationDictionary参数。service层 通过这个ValidationDictionary与controller层进行交互。我们将在接下来讨论装饰者模式时来说明它。

更值得注意的是,ContactManagerService实现了IContactManagerService接口。你需要时刻努力进行面向接 口变成。Contact Manager应用中的其他类都不与具体的ContactManagerService类直接交互。它们皆需面向 IContactManagerService接口。

IContactManagerService接口的代码如下:

 

using System.Collections.Generic; namespace ContactManager.Models { public interface IContactManagerService { bool CreateContact(Contact contactToCreate); bool DeleteContact(Contact contactToDelete); bool EditContact(Contact contactToEdit); Contact GetContact(int id); IEnumerable<Contact> ListContacts(); } }

修改后的Contact controller类代码如下,这里Contact controller类已不再与ContactManager service交互,每一层都尽可能的与其他层独立开来。

using System.Web.Mvc;
using ContactManager.Models;
using ContactManager.Models.Validation;
namespace ContactManager.Controllers
{
public class ContactController : Controller
{
private IContactManagerService _service;
public ContactController()
{
_service = new ContactManagerService(new ModelStateWrapper(this.ModelState));
}
public ContactController(IContactManagerService service)
{
_service = service;
}
public ActionResult Index()
{
return View(_service.ListContacts());
}
public ActionResult Create()
{
return View();
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create([Bind(Exclude = "Id")] Contact contactToCreate)
{
if (_service.CreateContact(contactToCreate))
return RedirectToAction("Index");
return View();
}
public ActionResult Edit(int id)
{
return View(_service.GetContact(id));
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(Contact contactToEdit)
{
if (_service.EditContact(contactToEdit))
return RedirectToAction("Index");
return View();
}
public ActionResult Delete(int id)
{
return View(_service.GetContact(id));
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Delete(Contact contactToDelete)
{
if (_service.DeleteContact(contactToDelete))
return RedirectToAction("Index");
return View();
}
}
}

我们的应用程序已经不再违反SRP原则了。上面所示代码的Contact controller中,所有的验证逻辑都被转移到service层中,所有的数据库存储逻辑都被转移到repository层中。

使用装饰者模式

我们欲将service层与controller层完全解耦,原则上讲也就是我们应当可以在独立的程序集中编译service层而无需添加对MVC应用程序的引用。

然而我们的service层需要将验证错误信息回传给controller层,那么我们如何才能在service层和controller不耦合的前提下完成这项任务呢?答案是:装饰者模式。

Contrlooer使用名为ModelState的ModelStateDictionary表现验证错误信息。因此我们可能会想将 ModelState从controller层传递到sercice层。然而在service层中使用ModelState会使你的服务层依赖于 ASP.NET MVC framework提供的某些特性。这可能会很糟,假设某天你想在一个WPF应用程序中使用这个service层,你就不得不添加对ASP.NET MVC framework的引用才能使用ModelStateDictionary类。

装饰者模式通过将已有的类包装在新的类中从而实现某接口。我们的Contact Manager项目中包含的ModelStateWrapper类的代码如下:

using System.Web.Mvc;
namespace ContactManager.Models.Validation
{
public class ModelStateWrapper : IValidationDictionary
{
private ModelStateDictionary _modelState;
public ModelStateWrapper(ModelStateDictionary modelState)
{
_modelState = modelState;
}
public void AddError(string key, string errorMessage)
{
_modelState.AddModelError(key, errorMessage);
}
public bool IsValid
{
get { return _modelState.IsValid; }
}
}
}

其接口代码如下:

namespace ContactManager.Models.Validation
{
public interface IValidationDictionary
{
void AddError(string key, string errorMessage);
bool IsValid { get; }
}
}

仔细观察IContactManagerService接口中的代码,你会发现ContactManager service层中仅使用了IValidationDictionary接口。ContactManager service不依赖ModelStateDictionary类。当Contact controller创建ContactManager service时,controller将其ModelState包装成如下的样子:

_service = new ContactManagerService(new ModelStateWrapper(this.ModelState));

总结

本次迭代中,我们并没有对Contact Manager应用添加任何的功能。本次迭代的目的是通过重构应用程序,使Contact Manager更易维护、更易修改。

首先,我们实现了Repository软件设计模式。我们将所有的数据存取相关的代码提取到独立的ContactManager repository类中。同时,我们也将验证逻辑从controller逻辑中独立出来,将其放入我们另外创建的独立的service层中。 controller层与service层交互,service层则与repository层交互。

然后我们通过装饰者模式将ModelState从service层中独立出来。在我们的service层中,我们只需针对IValidationDictionary接口进行编码,而非针对ModelState类。

最后,我们使用了依赖注入这种软件设计模式。该模式使得我们在开发中可以避开具体类,而针对接口(虚类)编码。得益于依赖注入模式,我们的代码变得更具测试性。在下一次迭代中,我们将向项目中添加单元测试。

作者:紫色永恒
出处:http://024hi.cnblogs.com/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利
Tag标签: asp.net mvc

posted on 2009-04-12 19:27 紫色永恒 阅读(1032) 评论(18)  编辑 收藏 网摘 所属分类: Asp.Net MVC

Feedback

#1楼  2009-04-12 19:55 ·风信子·      

又见博主的大作,支持   回复  引用  查看    

#2楼  2009-04-12 20:50 EntLib      

不错,很专业!

我们也在翻译NerdDinner 范例程序的创建过程。

一步一步学习ASP.NET MVC 1.0创建NerdDinner 范例程序 (Part 1-10) – 强烈推荐!
一步一步学习ASP.NET MVC 1.0创建NerdDinner 范例程序 (Part 11-20) – 强烈推荐! 
欢迎交流

  回复  引用  查看    

#3楼  2009-04-12 21:07 Jeffrey Zhao      

这是Repository吗?这是Decorator吗?不过是一路打通的简单逻辑,分为了业务逻辑数据访问层,再根据接口进行访问——哪来Repository?哪来Decorator?   回复  引用  查看    

#4楼 [楼主] 2009-04-12 21:22 紫色永恒      

@Jeffrey Zhao
其实我也有此问,但翻译就是翻译   回复  引用  查看    

#5楼  2009-04-12 23:18 Q.Lee.lulu      

@Jeffrey Zhao
这不是Repository么?那么对于真正的Repository还差些什么呢?   回复  引用  查看    

#6楼 [楼主] 2009-04-13 07:57 紫色永恒      

@Q.Lee.lulu
我主要觉得这里面的DI实在有点勉强   回复  引用  查看    

#7楼  2009-04-13 09:32 假正经哥哥      

Repository?扫下盲。。。   回复  引用  查看    

#8楼  2009-04-13 19:56 Jeffrey Zhao      

Repository应该是一个collection like的interface,提供查询/修改操作,而不是包含一系列CRUD方法的接口。
例如Repository的查询:
var r = new Repository()
var q = new SomeQueryObject();
return r.List(q);
可以看出,linq是个不错的内置的query object。
修改呢?get/modify/save
添加呢?create/add/save
而不是一个功能一个接口,否则就是个普通的数据访问层而已,什么Repository……   回复  引用  查看    

#9楼  2009-04-13 20:51 EntLib      

@Jeffrey Zhao
在Scott Gu的ASP.NET MVC 1.0 – NerdDinner 范例程序,是这样定义IDinnerRepository接口的:
public interface IDinnerRepository {
IQueryable<Dinner> FindAllDinners();
IQueryable<Dinner> FindByLocation(float latitude, float longitude);
IQueryable<Dinner> FindUpcomingDinners();
Dinner GetDinner(int id);
void Add(Dinner dinner);
void Delete(Dinner dinner);
void Save();
}
和你的想法似乎不一致。
  回复  引用  查看    

#10楼  2009-04-13 21:53 Jeffrey Zhao      

@EntLib
我说的是PoEAA那本书上的样子(也就是Query Object),ScottGu也只是多加了一些额外的查询接口,和我的说法大体一致阿,都是针对集合的操作,比如Add/Delete/Save。
不像现在的文章,CreateXxx,DeleteXxx,EditXxx,都不是针对集合的操作,只是一个数据访问层对象而已。试想如果要更多接口的会怎么样呢?
UpdateContactsForCompany(int companyId, …)
这又算什么Repository呢?   回复  引用  查看    

#11楼  2009-04-13 22:36 EntLib      

@Jeffrey Zhao
根据我的理解,我认为Scott Gu定义的IDinnerRepository 接口,和本文中定义了IContactRepository 接口基本思路是一样的。
不知道其他朋友有什么想法?
  回复  引用  查看    

#12楼  2009-04-13 22:44 Jeffrey Zhao      

@EntLib
嗯,我总结一下我的观点:
Repository有其特定的概念和操作方式,这也是为什么Scott和我认为Repository都需要一个Save方法的缘故。 Repository是一个集合,NHibernate,Linq to SQL都适合实现Repository,它是把Domain和关系进行映射(Mapping),而Repository是一个抽象的“容器”。
这片文章里就是个普通的数据访问层,和我们随便写一个程序,其中的数据访问层就会是这样的,说白了,这更像是最普通的Trasaction Scripts,缺少Mapping的概念。
所以说,看起来有接口类似(部分查询功能),但是操作理念是大相径庭的,这也就是我说这篇文章在胡搞的原因。
// Decorator更不用说了,没有组合,没有扩展,一点Decorator的特点都没有。   回复  引用  查看    

#13楼  2009-04-13 23:15 EntLib      

@Jeffrey Zhao,
首先,很高兴和你讨论这个问题。谢谢!
关于引入Repository Class,Scott Gu 在文章中是这样解释的:

创建DinnerRepository
对于一个小应用程序而言,有时让Conrollers类直接使用LINQ to SQL DataContext类,并将LINQ 查询语句写在Controllers中。但随着应用程序越来越大,这一方法的维护和测试将变得麻烦,并且导致重复的LINQ查询在多个地方出现。
 

让维护和测试更方便的方法是使用Repository 模式。Repository类帮助封装数据查询和存储逻辑,从应用程序中抽象隔离具体的数据存储实现。除了是应用程序代码更加简洁外,使用 Repository模式使将来更改数据库存储实现更加方便,并且有助于在没有真实数据库的情况下,进行应用程序进行单元测试。
原文链接:一步一步学习ASP.NET MVC 1.0创建NerdDinner 范例程序,Part 5

赞(0) 打赏
分享到: 更多 (0)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏