迭代 4 – 使应用程序松散耦合 (C#)

Microsoft

下载代码

在第四次迭代中,我们利用多种软件设计模式来更轻松地维护和修改 Contact Manager 应用程序。 例如,我们将应用程序重构为使用存储库模式和依赖项注入模式。

生成联系人管理 ASP.NET MVC 应用程序 (C#)

在此系列教程中,我们将从头到尾构建整个联系人管理应用程序。 通过 Contact Manager 应用程序,可以存储联系人列表的联系人信息(姓名、电话号码和电子邮件地址)。

我们通过多次迭代生成应用程序。 每次迭代都会逐步改进应用程序。 这种多次迭代方法的目标是使你能够了解每次更改的原因。

  • 迭代 #1 - 创建应用程序。 在第一次迭代中,我们以最简单的方式创建联系人管理器。 添加了对基本数据库操作的支持:创建、读取、更新和删除 (CRUD) 。

  • 迭代 #2 - 使应用程序看起来不错。 在此迭代中,我们通过修改默认 ASP.NET MVC 视图母版页和级联样式表来改进应用程序的外观。

  • 迭代 #3 - 添加表单验证。 在第三次迭代中,我们添加了基本表单验证。 我们会阻止用户在未完成所需表单字段的情况下提交表单。 我们还验证电子邮件地址和电话号码。

  • 迭代 #4 - 使应用程序松散耦合。 在第四次迭代中,我们利用多种软件设计模式来更轻松地维护和修改 Contact Manager 应用程序。 例如,我们将应用程序重构为使用存储库模式和依赖项注入模式。

  • 迭代 #5 - 创建单元测试。 在第五次迭代中,我们通过添加单元测试使应用程序更易于维护和修改。 我们将模拟数据模型类,并为控制器和验证逻辑生成单元测试。

  • 迭代 #6 - 使用测试驱动开发。 在此第六次迭代中,我们通过先编写单元测试并针对单元测试编写代码,向应用程序添加新功能。 在此迭代中,我们将添加联系人组。

  • 迭代 #7 - 添加 Ajax 功能。 第七次迭代中,我们通过添加对 Ajax 的支持来提高应用程序的响应能力和性能。

此迭代

在 Contact Manager 应用程序的第四次迭代中,我们将重构应用程序,使应用程序更松散地耦合。 当应用程序松散耦合时,可以修改应用程序的一个部分中的代码,而无需修改应用程序其他部分中的代码。 松散耦合的应用程序更能适应变化。

目前,Contact Manager 应用程序使用的所有数据访问和验证逻辑都包含在控制器类中。 这是一个坏主意。 每当需要修改应用程序的一个部分时,都存在将 bug 引入应用程序另一部分的风险。 例如,如果修改验证逻辑,则有可能在数据访问或控制器逻辑中引入新 bug。

注意

(SRP) ,类不应有多个更改原因。 混合控制器、验证和数据库逻辑严重违反单一责任原则。

可能需要修改应用程序的原因有多种。 可能需要向应用程序添加新功能,可能需要修复应用程序中的 bug,或者可能需要修改应用程序的功能的实现方式。 应用程序很少是静态的。 它们往往随着时间的推移而生长和变异。

例如,假设你决定更改实现数据访问层的方式。 现在,Contact Manager 应用程序使用 Microsoft 实体框架访问数据库。 但是,你可能决定迁移到新的或替代的数据访问技术,例如 ADO.NET Data Services 或 NHibernate。 但是,由于数据访问代码不与验证和控制器代码隔离,因此,如果不修改与数据访问不直接相关的其他代码,则无法修改应用程序中的数据访问代码。

另一方面,当应用程序松散耦合时,可以更改应用程序的一个部分,而无需接触应用程序的其他部分。 例如,无需修改验证或控制器逻辑即可切换数据访问技术。

在此迭代中,我们利用了多种软件设计模式,这些模式使我们能够将 Contact Manager 应用程序重构为更松散耦合的应用程序。 完成后,联系人管理器不会执行以前未执行的任何操作。 但是,我们将能够在将来更轻松地更改应用程序。

注意

重构是重写应用程序的过程,它不会丢失任何现有功能。

使用存储库软件设计模式

我们的第一个更改是利用名为“存储库模式”的软件设计模式。 我们将使用存储库模式将数据访问代码与应用程序的其余部分隔离开来。

实现存储库模式需要完成以下两个步骤:

  1. 创建接口
  2. 创建实现 接口的具体类

首先,我们需要创建一个接口,用于描述我们需要执行的所有数据访问方法。 IContactManagerRepository 接口包含在清单 1 中。 此接口介绍五种方法:CreateContact () 、DeleteContact () 、EditContact () 、GetContact 和 ListContacts () 。

列表 1 - Models\IContactManagerRepository.cs

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();

    }
}

接下来,我们需要创建一个实现 IContactManagerRepository 接口的具体类。 由于我们使用 Microsoft 实体框架访问数据库,因此我们将创建一个名为 EntityContactManagerRepository 的新类。 此类包含在清单 2 中。

列表 2 - Models\EntityContactManagerRepository.cs

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 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 接口。 类实现该接口描述的所有五种方法。

你可能想知道为什么我们需要费心使用接口。 为什么需要同时创建接口和实现它的类?

除了一个例外,应用程序的其余部分将与 接口交互,而不是具体类。 我们将调用由 IContactManagerRepository 接口公开的方法,而不是调用 EntityContactManagerRepository 类公开的方法。

这样,我们就可以使用新类实现 接口,而无需修改应用程序的其余部分。 例如,在将来的某个日期,我们可能需要实现实现 IContactManagerRepository 接口的 DataServicesContactManagerRepository 类。 DataServicesContactManagerRepository 类可能使用 ADO.NET Data Services 访问数据库,而不是 Microsoft 实体框架。

如果应用程序代码是针对 IContactManagerRepository 接口而不是具体的 EntityContactManagerRepository 类编程的,则可以切换具体类,而无需修改代码的其余任何内容。 例如,我们可以从 EntityContactManagerRepository 类切换到 DataServicesContactManagerRepository 类,而无需修改数据访问或验证逻辑。

根据接口 (抽象) 而不是具体类进行编程,使应用程序更具复原能力。

注意

可以通过选择菜单选项“重构”“提取接口”,从 Visual Studio 中的具体类快速创建接口。 例如,可以先创建 EntityContactManagerRepository 类,然后使用 Extract Interface 自动生成 IContactManagerRepository 接口。

使用依赖关系注入软件设计模式

现在,我们已将数据访问代码迁移到单独的存储库类,因此需要修改 Contact 控制器以使用此类。 我们将利用名为 Dependency Injection 的软件设计模式,在控制器中使用存储库类。

修改后的联系人控制器包含在清单 3 中。

清单 3 - Controllers\ContactController.cs

using System.Text.RegularExpressions;
using System.Web.Mvc;
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.");
        }

        public ActionResult Index()
        {
            return View(_repository.ListContacts());
        }

        public ActionResult Create()
        {
            return View();
        } 

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Create([Bind(Exclude = "Id")] Contact contactToCreate)
        {
            // Validation logic
            ValidateContact(contactToCreate);
            if (!ModelState.IsValid)
                return View();

            // Database logic
            try
            {
                _repository.CreateContact(contactToCreate);
                return RedirectToAction("Index");
            }
            catch
            {
                return View();
            }
        }

        public ActionResult Edit(int id)
        {
            return View(_repository.GetContact(id));
        }

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Edit(Contact contactToEdit)
        {
            // Validation logic
            ValidateContact(contactToEdit);
            if (!ModelState.IsValid)
                return View();

            // Database logic
            try
            {
                _repository.EditContact(contactToEdit);
                return RedirectToAction("Index");
            }
            catch
            {
                return View();
            }
        }

        public ActionResult Delete(int id)
        {
            return View(_repository.GetContact(id));
        }

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Delete(Contact contactToDelete)
        {
            try
            {
                _repository.DeleteContact(contactToDelete);
                return RedirectToAction("Index");
            }
            catch
            {
                return View();
            }
        }

    }
}

请注意,清单 3 中的 Contact 控制器有两个构造函数。 第一个构造函数将 IContactManagerRepository 接口的具体实例传递给第二个构造函数。 Contact 控制器类使用 构造函数依赖关系注入

使用 EntityContactManagerRepository 类的唯一位置是第一个构造函数中。 类的其余部分使用 IContactManagerRepository 接口,而不是具体的 EntityContactManagerRepository 类。

这样,将来就可以轻松地切换 IContactManagerRepository 类的实现。 如果要使用 DataServicesContactRepository 类而不是 EntityContactManagerRepository 类,只需修改第一个构造函数。

构造函数依赖关系注入还使 Contact 控制器类非常可测试。 在单元测试中,可以通过传递 IContactManagerRepository 类的模拟实现来实例化 Contact 控制器。 当我们为 Contact Manager 应用程序生成单元测试时,下一次迭代中,依赖关系注入的此功能对我们非常重要。

注意

如果要将 Contact 控制器类与 IContactManagerRepository 接口的特定实现完全分离,则可以利用支持依赖关系注入的框架,例如 StructureMap 或 Microsoft Entity Framework (MEF) 。 利用依赖关系注入框架,无需在代码中引用具体类。

创建服务层

你可能已经注意到,我们的验证逻辑仍然与列表 3 中修改的控制器类中的控制器逻辑混为一体。 出于同样的原因,最好隔离数据访问逻辑,最好隔离验证逻辑。

若要解决此问题,可以创建单独的 服务层。 服务层是可以在控制器和存储库类之间插入的单独层。 服务层包含业务逻辑,包括所有验证逻辑。

ContactManagerService 包含在清单 4 中。 它包含来自 Contact 控制器类的验证逻辑。

列表 4 - Models\ContactManagerService.cs

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。 服务层通过此 ValidationDictionary 与控制器层通信。 当我们讨论修饰器模式时,我们将在下一节中详细讨论 ValidationDictionary。

此外,请注意,ContactManagerService 实现了 IContactManagerService 接口。 应始终努力针对接口而不是具体类进行编程。 Contact Manager 应用程序中的其他类不直接与 ContactManagerService 类交互。 相反,除了一个例外,Contact Manager 应用程序的其余部分是针对 IContactManagerService 接口进行编程的。

IContactManagerService 接口包含在清单 5 中。

列表 5 - Models\IContactManagerService.cs

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 ListContacts();
    }
}

修改后的 Contact 控制器类包含在清单 6 中。 请注意,Contact 控制器不再与 ContactManager 存储库交互。 相反,Contact 控制器与 ContactManager 服务交互。 每个层都尽可能与其他层隔离。

列表 6 - Controllers\ContactController.cs

using System.Web.Mvc;
using ContactManager.Models;

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) 。 除了控制应用程序执行流之外,清单 6 中的 Contact 控制器已取消所有责任。 所有验证逻辑都已从 Contact 控制器中删除并推送到服务层。 所有数据库逻辑都已推送到存储库层。

使用修饰器模式

我们希望能够将服务层与控制器层完全分离。 原则上,我们应该能够在与控制器层不同的程序集中编译服务层,而无需添加对 MVC 应用程序的引用。

但是,服务层需要能够将验证错误消息传递回控制器层。 如何在不耦合控制器和服务层的情况下使服务层能够传达验证错误消息? 我们可以利用名为 “修饰器模式”的软件设计模式。

控制器使用名为 ModelState 的 ModelStateDictionary 来表示验证错误。 因此,你可能会想将 ModelState 从控制器层传递到服务层。 但是,在服务层中使用 ModelState 会使服务层依赖于 ASP.NET MVC 框架的功能。 这很糟糕,因为总有一天,你可能想要将服务层与 WPF 应用程序一起使用,而不是 ASP.NET MVC 应用程序。 在这种情况下,你不希望引用 ASP.NET MVC 框架来使用 ModelStateDictionary 类。

通过修饰器模式,可以将现有类包装在新类中,以实现接口。 我们的 Contact Manager 项目包括列表 7 中包含的 ModelStateWrapper 类。 ModelStateWrapper 类实现清单 8 中的 接口。

列表 7 - Models\Validation\ModelStateWrapper.cs

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; }
        }
    }
}

列表 8 - Models\Validation\IValidationDictionary.cs

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

如果仔细查看清单 5,你将看到 ContactManager 服务层以独占方式使用 IValidationDictionary 接口。 ContactManager 服务不依赖于 ModelStateDictionary 类。 当 Contact 控制器创建 ContactManager 服务时,控制器包装其 ModelState,如下所示:

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

总结

在此迭代中,我们没有向 Contact Manager 应用程序添加任何新功能。 此迭代的目标是重构 Contact Manager 应用程序,使其更易于维护和修改。

首先,我们实现了存储库软件设计模式。 我们已将所有数据访问代码迁移到单独的 ContactManager 存储库类。

我们还将验证逻辑与控制器逻辑隔离开来。 我们创建了一个单独的服务层,其中包含所有验证代码。 控制器层与服务层交互,服务层与存储库层交互。

创建服务层时,我们利用修饰器模式将 ModelState 与服务层隔离开来。 在服务层中,我们针对 IValidationDictionary 接口而不是 ModelState 编程。

最后,我们利用了名为依赖关系注入模式的软件设计模式。 此模式使我们能够针对接口 (抽象) 而不是具体类进行编程。 实现依赖关系注入设计模式也使我们的代码更易于测试。 在下一次迭代中,我们将单元测试添加到项目中。