Iteração nº 4 – tornar o aplicativo fracamente acoplado (C#)
pela Microsoft
Nesta quarta iteração, aproveitamos vários padrões de design de software para facilitar a manutenção e a modificação do aplicativo Contact Manager. Por exemplo, refatoramos nosso aplicativo para usar o padrão de repositório e o padrão de injeção de dependência.
Criando um aplicativo MVC ASP.NET gerenciamento de contatos (C#)
Nesta série de tutoriais, criamos um aplicativo de Gerenciamento de Contatos inteiro do início ao fim. O aplicativo Gerenciador de Contatos permite que você armazene informações de contato - nomes, números de telefone e endereços de email - para uma lista de pessoas.
Criamos o aplicativo em várias iterações. A cada iteração, aprimoramos gradualmente o aplicativo. O objetivo dessa abordagem de iteração múltipla é permitir que você entenda o motivo de cada alteração.
Iteração nº 1 – Criar o aplicativo. Na primeira iteração, criamos o Gerenciador de Contatos da maneira mais simples possível. Adicionamos suporte para operações básicas de banco de dados: CRUD (Criar, Ler, Atualizar e Excluir).
Iteração nº 2 – Deixe o aplicativo bonito. Nesta iteração, melhoramos a aparência do aplicativo modificando o modo de exibição padrão ASP.NET MVC master página e a folha de estilos em cascata.
Iteração nº 3 – Adicionar validação de formulário. Na terceira iteração, adicionamos validação de formulário básico. Impedimos que as pessoas enviem um formulário sem concluir os campos de formulário necessários. Também validamos endereços de email e números de telefone.
Iteração nº 4 – acoplar o aplicativo livremente. Nesta quarta iteração, aproveitamos vários padrões de design de software para facilitar a manutenção e a modificação do aplicativo Contact Manager. Por exemplo, refatoramos nosso aplicativo para usar o padrão de repositório e o padrão de injeção de dependência.
Iteração nº 5 – Criar testes de unidade. Na quinta iteração, facilitamos a manutenção e a modificação do aplicativo adicionando testes de unidade. Simulamos nossas classes de modelo de dados e criamos testes de unidade para nossos controladores e lógica de validação.
Iteração nº 6 – Usar o desenvolvimento controlado por teste. Nesta sexta iteração, adicionamos uma nova funcionalidade ao nosso aplicativo escrevendo testes de unidade primeiro e escrevendo código nos testes de unidade. Nesta iteração, adicionamos grupos de contatos.
Iteração nº 7 – Adicionar funcionalidade do Ajax. Na sétima iteração, melhoramos a capacidade de resposta e o desempenho do nosso aplicativo adicionando suporte para o Ajax.
Esta Iteração
Nesta quarta iteração do aplicativo Contact Manager, refatoramos o aplicativo para tornar o aplicativo mais flexível. Quando um aplicativo é acoplado livremente, você pode modificar o código em uma parte do aplicativo sem precisar modificar o código em outras partes do aplicativo. Aplicativos flexívelmente acoplados são mais resilientes a alterações.
Atualmente, toda a lógica de acesso e validação de dados usada pelo aplicativo Gerenciador de Contatos está contida nas classes do controlador. Isso é uma má ideia. Sempre que precisar modificar uma parte do aplicativo, você corre o risco de introduzir bugs em outra parte do aplicativo. Por exemplo, se você modificar sua lógica de validação, correrá o risco de introduzir novos bugs em seu acesso a dados ou à lógica do controlador.
Observação
(SRP), uma classe nunca deve ter mais de um motivo para mudar. A combinação de controlador, validação e lógica de banco de dados é uma violação maciça do Princípio de Responsabilidade Única.
Há vários motivos pelos quais talvez seja necessário modificar seu aplicativo. Talvez seja necessário adicionar um novo recurso ao seu aplicativo, talvez seja necessário corrigir um bug em seu aplicativo ou talvez seja necessário modificar como um recurso do seu aplicativo é implementado. Os aplicativos raramente são estáticos. Eles tendem a crescer e sofrer mutações ao longo do tempo.
Imagine, por exemplo, que você decida alterar a forma como implementa sua camada de acesso a dados. No momento, o aplicativo Contact Manager usa o Microsoft Entity Framework para acessar o banco de dados. No entanto, você pode decidir migrar para uma tecnologia de acesso a dados nova ou alternativa, como ADO.NET Data Services ou NHibernate. No entanto, como o código de acesso a dados não está isolado da validação e do código do controlador, não há como modificar o código de acesso a dados em seu aplicativo sem modificar outro código que não esteja diretamente relacionado ao acesso a dados.
Quando um aplicativo está livremente acoplado, por outro lado, você pode fazer alterações em uma parte de um aplicativo sem tocar em outras partes de um aplicativo. Por exemplo, você pode alternar as tecnologias de acesso a dados sem modificar a validação ou a lógica do controlador.
Nesta iteração, aproveitamos vários padrões de design de software que nos permitem refatorar nosso aplicativo do Contact Manager em um aplicativo mais flexívelmente acoplado. Quando terminarmos, o Gerenciador de Contatos não fará nada que não tenha feito antes. No entanto, poderemos alterar o aplicativo com mais facilidade no futuro.
Observação
Refatoração é o processo de reescrever um aplicativo de forma que ele não perca nenhuma funcionalidade existente.
Usando o padrão de design de software do repositório
Nossa primeira alteração é aproveitar um padrão de design de software chamado padrão de Repositório. Usaremos o padrão repositório para isolar nosso código de acesso a dados do restante do aplicativo.
Implementar o padrão de Repositório exige que concluamos as duas etapas a seguir:
- Criar uma interface
- Criar uma classe concreta que implementa a interface
Primeiro, precisamos criar uma interface que descreva todos os métodos de acesso a dados que precisamos executar. A interface IContactManagerRepository está contida na Listagem 1. Essa interface descreve cinco métodos: CreateContact(), DeleteContact(), EditContact(), GetContact e ListContacts().
Listagem 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();
}
}
Em seguida, precisamos criar uma classe concreta que implemente a interface IContactManagerRepository. Como estamos usando o Microsoft Entity Framework para acessar o banco de dados, criaremos uma nova classe chamada EntityContactManagerRepository. Essa classe está contida na Listagem 2.
Listagem 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();
}
}
}
Observe que a classe EntityContactManagerRepository implementa a interface IContactManagerRepository. A classe implementa todos os cinco métodos descritos por essa interface.
Você pode se perguntar por que precisamos nos preocupar com uma interface. Por que precisamos criar uma interface e uma classe que a implemente?
Com uma exceção, o restante do nosso aplicativo interagirá com a interface e não com a classe concreta. Em vez de chamar os métodos expostos pela classe EntityContactManagerRepository, chamaremos os métodos expostos pela interface IContactManagerRepository.
Dessa forma, podemos implementar a interface com uma nova classe sem a necessidade de modificar o restante do aplicativo. Por exemplo, em alguma data futura, talvez queiramos implementar uma classe DataServicesContactManagerRepository que implementa a interface IContactManagerRepository. A classe DataServicesContactManagerRepository pode usar ADO.NET Data Services para acessar um banco de dados em vez do Microsoft Entity Framework.
Se o código do aplicativo for programado na interface IContactManagerRepository em vez da classe EntityContactManagerRepository concreta, poderemos alternar classes concretas sem modificar o restante do código. Por exemplo, podemos alternar da classe EntityContactManagerRepository para a classe DataServicesContactManagerRepository sem modificar nossa lógica de validação ou acesso a dados.
A programação em relação a interfaces (abstrações) em vez de classes concretas torna nosso aplicativo mais resiliente a alterações.
Observação
Você pode criar rapidamente uma interface de uma classe concreta no Visual Studio selecionando a opção de menu Refatorar, Extrair Interface. Por exemplo, você pode criar a classe EntityContactManagerRepository primeiro e, em seguida, usar Extrair Interface para gerar a interface IContactManagerRepository automaticamente.
Usando o padrão de design de software de injeção de dependência
Agora que migramos nosso código de acesso a dados para uma classe de Repositório separada, precisamos modificar nosso controlador de contato para usar essa classe. Aproveitaremos um padrão de design de software chamado Injeção de Dependência para usar a classe Repository em nosso controlador.
O controlador de contato modificado está contido na Listagem 3.
Listagem 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();
}
}
}
}
Observe que o Controlador de contato na Listagem 3 tem dois construtores. O primeiro construtor passa uma instância concreta da interface IContactManagerRepository para o segundo construtor. A classe de controlador Contact usa Injeção de Dependência do Construtor.
O único local em que a classe EntityContactManagerRepository é usada está no primeiro construtor. O restante da classe usa a interface IContactManagerRepository em vez da classe EntityContactManagerRepository concreta.
Isso facilita a troca de implementações da classe IContactManagerRepository no futuro. Se você quiser usar a classe DataServicesContactRepository em vez da classe EntityContactManagerRepository, basta modificar o primeiro construtor.
A injeção de dependência do construtor também torna a classe de controlador contact muito testável. Em seus testes de unidade, você pode instanciar o controlador de contato passando uma implementação simulada da classe IContactManagerRepository. Esse recurso de Injeção de Dependência será muito importante para nós na próxima iteração quando criarmos testes de unidade para o aplicativo Contact Manager.
Observação
Se você quiser separar completamente a classe de controlador Contact de uma implementação específica da interface IContactManagerRepository, poderá aproveitar uma estrutura que dá suporte à Injeção de Dependência, como StructureMap ou o MEF (Microsoft Entity Framework). Aproveitando uma estrutura de Injeção de Dependência, você nunca precisa se referir a uma classe concreta em seu código.
Criando uma camada de serviço
Talvez você tenha notado que nossa lógica de validação ainda está misturada com nossa lógica de controlador na classe de controlador modificada na Listagem 3. Pelo mesmo motivo pelo qual é uma boa ideia isolar nossa lógica de acesso a dados, é uma boa ideia isolar nossa lógica de validação.
Para corrigir esse problema, podemos criar uma camada de serviço separada. A camada de serviço é uma camada separada que podemos inserir entre nossas classes de controlador e repositório. A camada de serviço contém nossa lógica de negócios, incluindo toda a nossa lógica de validação.
O ContactManagerService está contido na Listagem 4. Ele contém a lógica de validação da classe Controlador de contato.
Listagem 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
}
}
Observe que o construtor do ContactManagerService requer um ValidationDictionary. A camada de serviço se comunica com a camada do controlador por meio desse ValidationDictionary. Discutimos o ValidationDictionary em detalhes na seção a seguir quando discutimos o padrão decorador.
Observe, além disso, que o ContactManagerService implementa a interface IContactManagerService. Você sempre deve se esforçar para programar em relação a interfaces em vez de classes concretas. Outras classes no aplicativo Contact Manager não interagem diretamente com a classe ContactManagerService. Em vez disso, com uma exceção, o restante do aplicativo Contact Manager é programado na interface IContactManagerService.
A interface IContactManagerService está contida na Listagem 5.
Listagem 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();
}
}
A classe de controlador Contact modificada está contida na Listagem 6. Observe que o Controlador de contato não interage mais com o repositório ContactManager. Em vez disso, o controlador de contato interage com o serviço ContactManager. Cada camada é isolada o máximo possível de outras camadas.
Listagem 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();
}
}
}
Nosso aplicativo não é mais executado de acordo com o SRP (Princípio de Responsabilidade Única). O controlador contato na Listagem 6 foi removido de todas as responsabilidades diferentes de controlar o fluxo de execução do aplicativo. Toda a lógica de validação foi removida do controlador de contato e enviada por push para a camada de serviço. Toda a lógica do banco de dados foi enviada por push para a camada do repositório.
Usando o padrão decorador
Queremos ser capazes de separar completamente nossa camada de serviço da camada do controlador. Em princípio, devemos ser capazes de compilar nossa camada de serviço em um assembly separado da camada do controlador sem a necessidade de adicionar uma referência ao nosso aplicativo MVC.
No entanto, nossa camada de serviço precisa ser capaz de passar mensagens de erro de validação de volta para a camada do controlador. Como podemos habilitar a camada de serviço para comunicar mensagens de erro de validação sem acoplar o controlador e a camada de serviço? Podemos aproveitar um padrão de design de software chamado padrão decorador.
Um controlador usa um ModelStateDictionary chamado ModelState para representar erros de validação. Portanto, você pode ser tentado a passar ModelState da camada do controlador para a camada de serviço. No entanto, o uso de ModelState na camada de serviço tornaria sua camada de serviço dependente de um recurso do ASP.NET estrutura MVC. Isso seria ruim porque, um dia, talvez você queira usar a camada de serviço com um aplicativo WPF em vez de um aplicativo MVC ASP.NET. Nesse caso, você não gostaria de referenciar a estrutura ASP.NET MVC para usar a classe ModelStateDictionary.
O padrão Decorador permite encapsular uma classe existente em uma nova classe para implementar uma interface. Nosso projeto do Contact Manager inclui a classe ModelStateWrapper contida na Listagem 7. A classe ModelStateWrapper implementa a interface na Listagem 8.
Listagem 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; }
}
}
}
Listagem 8 – Models\Validation\IValidationDictionary.cs
namespace ContactManager.Models.Validation
{
public interface IValidationDictionary
{
void AddError(string key, string errorMessage);
bool IsValid {get;}
}
}
Se você examinar de perto a Listagem 5, verá que a camada de serviço ContactManager usa exclusivamente a interface IValidationDictionary. O serviço ContactManager não depende da classe ModelStateDictionary. Quando o Controlador de contato cria o serviço ContactManager, o controlador encapsula seu ModelState da seguinte maneira:
_service = new ContactManagerService(new ModelStateWrapper(this.ModelState));
Resumo
Nesta iteração, não adicionamos nenhuma nova funcionalidade ao aplicativo Contact Manager. O objetivo dessa iteração era refatorar o aplicativo Gerenciador de Contatos para que seja mais fácil de manter e modificar.
Primeiro, implementamos o padrão de design de software do repositório. Migramos todo o código de acesso a dados para uma classe de repositório ContactManager separada.
Também isolamos nossa lógica de validação de nossa lógica de controlador. Criamos uma camada de serviço separada que contém todo o nosso código de validação. A camada do controlador interage com a camada de serviço e a camada de serviço interage com a camada do repositório.
Quando criamos a camada de serviço, aproveitamos o padrão Decorador para isolar o ModelState da camada de serviço. Em nossa camada de serviço, programamos na interface IValidationDictionary em vez de ModelState.
Por fim, aproveitamos um padrão de design de software chamado Padrão de injeção de dependência. Esse padrão nos permite programar em relação a interfaces (abstrações) em vez de classes concretas. Implementar o padrão de design injeção de dependência também torna nosso código mais testável. Na próxima iteração, adicionamos testes de unidade ao nosso projeto.