[翻译-ASP.NET MVC]Contact Manager开发之旅迭代4 - 利用设计模式松散耦合
注:为保证可读性,文中Controller、View、Model、Route、Action等ASP.NET MVC核心单词均未翻译。
ContactManager开发之旅 迭代1 - 创建应用程序
ContactManager开发之旅 迭代2 - 修改样式,美化应用
迭代4 利用设计模式松散耦合
本次迭代
这是ContactManager的第四次迭代,本次迭代中我们将重构应用程序,通过合理的利用设计模式松散其耦合。松耦合的程序更有弹性,更易维护。当应用程序面临改动时,你只需修改某一部分的代码,而不会出现大量修改与其耦合严重的相关代码这种牵一发而动全身的情况。
在当前的ContactManager应用中,所有的数据存储及验证逻辑都分布在controller类中,这并不是个好主意。要知道这种情况下一旦你需要修改其中一部分代码,你将同时面临为其他部分增加bug的风险。比如你需要修改验证逻辑,你就必须承担数据存储或controller部分的逻辑会随之出现问题的风险。
需求的变更,个人想法的升华、始料未及的情况如此频繁,你不得不就当前的应用作出一些改变:为应用程序添加新功能、修复bug、修改应用中某个功能的实现等。就应用程序而言,它们很难处于一种静止不动的状态,他们无时无刻在被不停的改变、改善。
现在的情况是,ContactManager应用中使用了Microsoft Entity Framework处理数据通信。想象一下,你决定对数据存储层实现做出一些改变,你希望使用其它的一些方案:如ADP.NET Data Services或NHibernate。由于数据存储相关的代码并不独立于验证及controller中的代码,你将无法避免修改那些原本应该毫无干系的代码。
而另一方面,对于一个松耦合的程序来说,上面的问题就不存在了。一个经典的场景是:你可以随意切换数据存储方案而不用管验证逻辑、controller中的那些劳什子代码。
在这次迭代中,我们将利用软件设计模式的优点重构我们的Contact Manager应用程序,使其符合我们上面提到的“松耦合”的要求。尽管做完这些以后,我们的应用程序并不会表现的与以往有任何不同,但是我们从此便可轻松驾驭其未来的维护及修改过程。
使用Repository模式
我们的第一个改动便是使用叫做Repository的设计模式改善我们的应用。我们将使用这个模式将数据存储相关的代码与我们应用中的其他逻辑独立开来。
要实现Repository模式,我们需要做以下两件事
- 新建一个接口
- 新建一个类实现上面的接口。
首先,我们需要新建一个接口约定所有我们需要实现的数据存储方法。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包装成如下的样子:
总结
本次迭代中,我们并没有对Contact Manager应用添加任何的功能。本次迭代的目的是通过重构应用程序,使Contact Manager更易维护、更易修改。
首先,我们实现了Repository软件设计模式。我们将所有的数据存取相关的代码提取到独立的ContactManager repository类中。同时,我们也将验证逻辑从controller逻辑中独立出来,将其放入我们另外创建的独立的service层中。controller层与service层交互,service层则与repository层交互。
然后我们通过装饰者模式将ModelState从service层中独立出来。在我们的service层中,我们只需针对IValidationDictionary接口进行编码,而非针对ModelState类。
最后,我们使用了依赖注入这种软件设计模式。该模式使得我们在开发中可以避开具体类,而针对接口(虚类)编码。得益于依赖注入模式,我们的代码变得更具测试性。在下一次迭代中,我们将向项目中添加单元测试。
作者:紫色永恒
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利