Compartilhar via


Iteração nº 4 – tornar o aplicativo fracamente acoplado (VB)

pela Microsoft

Baixar código

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:

  1. Criar uma interface
  2. 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.