Iteração nº 4 – tornar o aplicativo fracamente acoplado (VB)
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 VB (aplicativo MVC) ASP.NET gerenciamento de contatos
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.vb
Public Interface IContactManagerRepository
Function CreateContact(ByVal contactToCreate As Contact) As Contact
Sub DeleteContact(ByVal contactToDelete As Contact)
Function EditContact(ByVal contactToUpdate As Contact) As Contact
Function GetContact(ByVal id As Integer) As Contact
Function ListContacts() As IEnumerable(Of Contact)
End Interface
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.vb
Public Class EntityContactManagerRepository
Implements IContactManagerRepository
Private _entities As New ContactManagerDBEntities()
Public Function GetContact(ByVal id As Integer) As Contact Implements IContactManagerRepository.GetContact
Return (From c In _entities.ContactSet _
Where c.Id = id _
Select c).FirstOrDefault()
End Function
Public Function ListContacts() As IEnumerable(Of Contact) Implements IContactManagerRepository.ListContacts
Return _entities.ContactSet.ToList()
End Function
Public Function CreateContact(ByVal contactToCreate As Contact) As Contact Implements IContactManagerRepository.CreateContact
_entities.AddToContactSet(contactToCreate)
_entities.SaveChanges()
Return contactToCreate
End Function
Public Function EditContact(ByVal contactToEdit As Contact) As Contact Implements IContactManagerRepository.EditContact
Dim originalContact = GetContact(contactToEdit.Id)
_entities.ApplyPropertyChanges(originalContact.EntityKey.EntitySetName, contactToEdit)
_entities.SaveChanges()
Return contactToEdit
End Function
Public Sub DeleteContact(ByVal contactToDelete As Contact) Implements IContactManagerRepository.DeleteContact
Dim originalContact = GetContact(contactToDelete.Id)
_entities.DeleteObject(originalContact)
_entities.SaveChanges()
End Sub
End Class
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.vb
Public Class ContactController
Inherits System.Web.Mvc.Controller
Private _repository As IContactManagerRepository
Sub New()
Me.New(new EntityContactManagerRepository())
End Sub
Sub New(repository As IContactManagerRepository)
_repository = repository
End Sub
Protected Sub ValidateContact(contactToValidate As Contact)
If contactToValidate.FirstName.Trim().Length = 0 Then
ModelState.AddModelError("FirstName", "First name is required.")
End If
If contactToValidate.LastName.Trim().Length = 0 Then
ModelState.AddModelError("LastName", "Last name is required.")
End If
If (contactToValidate.Phone.Length > 0 AndAlso Not Regex.IsMatch(contactToValidate.Phone, "((\(\d{3}\) ?)|(\d{3}-))?\d{3}-\d{4}"))
ModelState.AddModelError("Phone", "Invalid phone number.")
End If
If (contactToValidate.Email.Length > 0 AndAlso Not Regex.IsMatch(contactToValidate.Email, "^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$"))
ModelState.AddModelError("Email", "Invalid email address.")
End If
End Sub
Function Index() As ActionResult
Return View(_repository.ListContacts())
End Function
Function Create() As ActionResult
Return View()
End Function
<AcceptVerbs(HttpVerbs.Post)> _
Function Create(<Bind(Exclude:="Id")> ByVal contactToCreate As Contact) As ActionResult
' Validation logic
ValidateContact(contactToCreate)
If Not ModelState.IsValid Then
Return View()
End If
' Database logic
Try
_repository.CreateContact(contactToCreate)
Return RedirectToAction("Index")
Catch
Return View()
End Try
End Function
Function Edit(ByVal id As Integer) As ActionResult
Return View(_repository.GetContact(id))
End Function
<AcceptVerbs(HttpVerbs.Post)> _
Function Edit(ByVal contactToEdit As Contact) As ActionResult
' Validation logic
ValidateContact(contactToEdit)
If Not ModelState.IsValid Then
Return View()
End If
' Database logic
Try
_repository.EditContact(contactToEdit)
Return RedirectToAction("Index")
Catch
Return View()
End Try
End Function
Function Delete(ByVal id As Integer) As ActionResult
Return View(_repository.GetContact(id))
End Function
<AcceptVerbs(HttpVerbs.Post)> _
Function Delete(ByVal contactToDelete As Contact) As ActionResult
Try
_repository.DeleteContact(contactToDelete)
Return RedirectToAction("Index")
Catch
Return View()
End Try
End Function
End Class
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 de contato 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 Gerenciador de Contatos.
Observação
Se você quiser separar completamente a classe 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
Você deve ter 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.vb
Public Class ContactManagerService
Implements IContactManagerService
Private _validationDictionary As IValidationDictionary
Private _repository As IContactManagerRepository
Public Sub New(ByVal validationDictionary As IValidationDictionary)
Me.New(validationDictionary, New EntityContactManagerRepository())
End Sub
Public Sub New(ByVal validationDictionary As IValidationDictionary, ByVal repository As IContactManagerRepository)
_validationDictionary = validationDictionary
_repository = repository
End Sub
Public Function ValidateContact(ByVal contactToValidate As Contact) As Boolean
If contactToValidate.FirstName.Trim().Length = 0 Then
_validationDictionary.AddError("FirstName", "First name is required.")
End If
If contactToValidate.LastName.Trim().Length = 0 Then
_validationDictionary.AddError("LastName", "Last name is required.")
End If
If contactToValidate.Phone.Length > 0 AndAlso (Not Regex.IsMatch(contactToValidate.Phone, "((\(\d{3}\) ?)|(\d{3}-))?\d{3}-\d{4}")) Then
_validationDictionary.AddError("Phone", "Invalid phone number.")
End If
If contactToValidate.Email.Length > 0 AndAlso (Not Regex.IsMatch(contactToValidate.Email, "^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$")) Then
_validationDictionary.AddError("Email", "Invalid email address.")
End If
Return _validationDictionary.IsValid
End Function
#Region "IContactManagerService Members"
Public Function CreateContact(ByVal contactToCreate As Contact) As Boolean Implements IContactManagerService.CreateContact
' Validation logic
If Not ValidateContact(contactToCreate) Then
Return False
End If
' Database logic
Try
_repository.CreateContact(contactToCreate)
Catch
Return False
End Try
Return True
End Function
Public Function EditContact(ByVal contactToEdit As Contact) As Boolean Implements IContactManagerService.EditContact
' Validation logic
If Not ValidateContact(contactToEdit) Then
Return False
End If
' Database logic
Try
_repository.EditContact(contactToEdit)
Catch
Return False
End Try
Return True
End Function
Public Function DeleteContact(ByVal contactToDelete As Contact) As Boolean Implements IContactManagerService.DeleteContact
Try
_repository.DeleteContact(contactToDelete)
Catch
Return False
End Try
Return True
End Function
Public Function GetContact(ByVal id As Integer) As Contact Implements IContactManagerService.GetContact
Return _repository.GetContact(id)
End Function
Public Function ListContacts() As IEnumerable(Of Contact) Implements IContactManagerService.ListContacts
Return _repository.ListContacts()
End Function
#End Region
End Class
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 detalhadamente 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.vb
Public Interface IContactManagerService
Function CreateContact(ByVal contactToCreate As Contact) As Boolean
Function DeleteContact(ByVal contactToDelete As Contact) As Boolean
Function EditContact(ByVal contactToEdit As Contact) As Boolean
Function GetContact(ByVal id As Integer) As Contact
Function ListContacts() As IEnumerable(Of Contact)
End Interface
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.vb
Public Class ContactController
Inherits System.Web.Mvc.Controller
Private _service As IContactManagerService
Sub New()
_service = new ContactManagerService(New ModelStateWrapper(ModelState))
End Sub
Sub New(service As IContactManagerService)
_service = service
End Sub
Function Index() As ActionResult
Return View(_service.ListContacts())
End Function
Function Create() As ActionResult
Return View()
End Function
<AcceptVerbs(HttpVerbs.Post)> _
Function Create(<Bind(Exclude:="Id")> ByVal contactToCreate As Contact) As ActionResult
If _service.CreateContact(contactToCreate) Then
Return RedirectToAction("Index")
End If
Return View()
End Function
Function Edit(ByVal id As Integer) As ActionResult
Return View(_service.GetContact(id))
End Function
<AcceptVerbs(HttpVerbs.Post)> _
Function Edit(ByVal contactToEdit As Contact) As ActionResult
If _service.EditContact(contactToEdit) Then
Return RedirectToAction("Index")
End If
Return View()
End Function
Function Delete(ByVal id As Integer) As ActionResult
Return View(_service.GetContact(id))
End Function
<AcceptVerbs(HttpVerbs.Post)> _
Function Delete(ByVal contactToDelete As Contact) As ActionResult
If _service.DeleteContact(contactToDelete) Then
return RedirectToAction("Index")
End If
Return View()
End Function
End Class
Nosso aplicativo não é mais executado de acordo com o SRP (Princípio de Responsabilidade Única). O controlador de contato na Listagem 6 foi removido de todas as responsabilidades que não sejam 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 de decorador
Queremos poder 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 de nossa camada de 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.vb
Public Class ModelStateWrapper
Implements IValidationDictionary
Private _modelState As ModelStateDictionary
Public Sub New(ByVal modelState As ModelStateDictionary)
_modelState = modelState
End Sub
Public Sub AddError(ByVal key As String, ByVal errorMessage As String) Implements IValidationDictionary.AddError
_modelState.AddModelError(key, errorMessage)
End Sub
Public ReadOnly Property IsValid() As Boolean Implements IValidationDictionary.IsValid
Get
Return _modelState.IsValid
End Get
End Property
End Class
Listagem 8 – Models\Validation\IValidationDictionary.vb
Public Interface IValidationDictionary
Sub AddError(ByVal key As String, ByVal errorMessage As String)
ReadOnly Property IsValid() As Boolean
End Interface
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 contact cria o serviço ContactManager, o controlador encapsula seu ModelState da seguinte maneira:
_service = new ContactManagerService(New ModelStateWrapper(ModelState))
Resumo
Nesta iteração, não adicionamos nenhuma nova funcionalidade ao aplicativo Contact Manager. A meta dessa iteração era refatorar o aplicativo Contact Manager para que seja mais fácil 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 de nossa 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 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.