[翻译-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类。

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

















本文转自紫色永恒51CTO博客,原文链接: http://www.cnblogs.com/024hi/archive/2009/04/12/ASP_NET_MVC_SAMPLE_CONTACT_MANAGER_4.html,如需转载请自行联系原作者


相关文章
|
4月前
|
设计模式 算法 搜索推荐
后端开发中的设计模式应用与实践
在软件开发的广袤天地中,后端技术如同构筑高楼大厦的钢筋水泥,支撑起整个应用程序的骨架。本文旨在通过深入浅出的方式,探讨后端开发领域内不可或缺的设计模式,这些模式犹如精雕细琢的工具箱,能够助力开发者打造出既健壮又灵活的系统架构。从单例模式到工厂模式,从观察者模式到策略模式,每一种设计模式都蕴含着深刻的哲理与实践价值,它们不仅仅是代码的组织方式,更是解决复杂问题的智慧结晶。
|
5月前
|
设计模式 算法 搜索推荐
后端开发中的设计模式应用
在软件开发的浩瀚海洋中,设计模式犹如一座座灯塔,为后端开发者指引方向。本文将深入探讨后端开发中常见的设计模式,并通过实例展示如何在实际项目中巧妙应用这些模式,以提升代码的可维护性、扩展性和复用性。通过阅读本文,您将能够更加自信地应对复杂后端系统的设计与实现挑战。
94 3
|
5月前
|
设计模式 安全 数据库连接
后端开发中的设计模式应用
在软件开发的浩瀚海洋中,设计模式如同灯塔,为后端开发者指引方向。它们不仅仅是代码的模板,更是解决复杂问题的智慧结晶。本文将深入探讨几种常见的设计模式,包括单例模式、工厂模式和观察者模式,并揭示它们在实际应用中如何提升代码的可维护性、扩展性和重用性。通过实例分析,我们将一窥这些模式如何在后端开发中大放异彩,助力构建高效、灵活的软件系统。
|
2月前
|
监控 前端开发 API
一款基于 .NET MVC 框架开发、功能全面的MES系统
一款基于 .NET MVC 框架开发、功能全面的MES系统
|
5月前
|
设计模式 算法 搜索推荐
后端开发中的设计模式应用
在软件开发的浩瀚海洋中,设计模式犹如灯塔一般指引着方向。它们不是一成不变的规则,而是前人智慧的结晶。本文将深入探讨几种在后端开发中常用的设计模式,如单例、工厂、观察者和策略模式,并阐述如何在实际项目中灵活运用这些模式来提升代码质量、可维护性和扩展性。通过对比传统开发方式与应用设计模式后的差异,我们将揭示设计模式在解决复杂问题和优化系统架构中的独特价值。
|
5月前
|
设计模式 JavaScript Java
后端开发中的设计模式应用
本文将深入探讨后端开发中常见的设计模式,包括单例模式、工厂模式和观察者模式。每种模式不仅会介绍其定义和结构,还会结合实际案例展示如何在后端开发中应用这些模式来优化代码的可维护性与扩展性。通过对比传统方法与设计模式的应用,读者可以更清晰地理解设计模式的优势,并学会在项目中灵活运用这些模式解决实际问题。
|
4月前
|
设计模式 算法 数据库连接
后端开发中的设计模式应用与实践
在软件开发的广袤天地中,设计模式如同夜空中最亮的星辰,引领着开发者们穿越复杂系统的迷雾。本文旨在通过深入浅出的方式,不仅探讨设计模式的理论精髓,揭示它们在后端架构中的重要性,还将以生动的实践案例,展示如何在实际项目中巧妙运用这些模式。我们邀请您一同踏上这场编程之旅,探索如何借助设计模式的力量,让后端系统更加健壮、灵活且易于维护,共同揭开后端技术神秘面纱的一角。
|
5月前
|
开发框架 前端开发 JavaScript
ASP.NET MVC 教程
ASP.NET 是一个使用 HTML、CSS、JavaScript 和服务器脚本创建网页和网站的开发框架。
69 7
|
5月前
|
存储 开发框架 前端开发
ASP.NET MVC 迅速集成 SignalR
ASP.NET MVC 迅速集成 SignalR
113 0
|
6月前
|
开发框架 前端开发 .NET
ASP.NET MVC WebApi 接口返回 JOSN 日期格式化 date format
ASP.NET MVC WebApi 接口返回 JOSN 日期格式化 date format
84 0

热门文章

最新文章