Поделиться через


Итерация 4. Создание слабых связей в приложении (VB)

от Майкрософт

Скачивание кода

В этой четвертой итерации мы воспользуемся преимуществами нескольких шаблонов проектирования программного обеспечения, чтобы упростить обслуживание и изменение приложения Диспетчера контактов. Например, мы рефакторинг приложения для использования шаблона репозитория и шаблона внедрения зависимостей.

Создание приложения управления контактами ASP.NET MVC (VB)

В этой серии учебников мы создадим все приложение управления контактами от начала до конца. Приложение Диспетчер контактов позволяет хранить контактные данные ( имена, номера телефонов и адреса электронной почты) для списка людей.

Мы создаем приложение с помощью нескольких итераций. С каждой итерацией мы постепенно улучшаем приложение. Цель этого подхода с несколькими итерациями — дать возможность понять причину каждого изменения.

  • Итерация 1. Создание приложения. В первой итерации мы создадим диспетчер контактов самым простым способом. Добавлена поддержка базовых операций с базами данных: создание, чтение, обновление и удаление (CRUD).

  • Итерация 2. Сделайте приложение красивым. В этой итерации мы улучшаем внешний вид приложения, изменив ASP.NET по умолчанию представление MVC master страницу и каскадную таблицу стилей.

  • Итерация 3. Добавление проверки формы. В третьей итерации мы добавим базовую проверку формы. Мы запрещаем пользователям отправлять форму без заполнения обязательных полей формы. Мы также проверяем адреса электронной почты и номера телефонов.

  • Итерация 4. Сделайте приложение слабосвязанным. В этой четвертой итерации мы воспользуемся преимуществами нескольких шаблонов проектирования программного обеспечения, чтобы упростить обслуживание и изменение приложения Диспетчера контактов. Например, мы рефакторинг приложения для использования шаблона репозитория и шаблона внедрения зависимостей.

  • Итерация 5. Создание модульных тестов. В пятой итерации мы упростим обслуживание и изменение приложения, добавив модульные тесты. Мы имитируем классы модели данных и создаем модульные тесты для наших контроллеров и логики проверки.

  • Итерация 6. Использование разработки на основе тестирования. В этой шестой итерации мы добавим новые функции в приложение, сначала написав модульные тесты и написав код для модульных тестов. В этой итерации мы добавим группы контактов.

  • Итерация 7. Добавление функциональных возможностей Ajax. В седьмой итерации мы повышаем скорость реагирования и производительность приложения, добавляя поддержку Ajax.

Эта итерация

В этой четвертой итерации приложения Диспетчера контактов мы рефакторинг приложения, чтобы сделать приложение более слабосвязанным. Если приложение слабо связано, вы можете изменить код в одной части приложения, не изменяя код в других частях приложения. Слабосвязанные приложения более устойчивы к изменениям.

В настоящее время вся логика доступа к данным и проверки, используемая приложением Диспетчера контактов, содержится в классах контроллера. Это плохая идея. Всякий раз, когда вам потребуется изменить одну часть приложения, вы рискуете обнаружить ошибки в другой части приложения. Например, при изменении логики проверки вы рискуете ввести новые ошибки в логику доступа к данным или контроллера.

Примечание

(SRP) класс никогда не должен иметь более одной причины для изменения. Смешивание логики контроллера, проверки и базы данных является массовым нарушением принципа единой ответственности.

Существует несколько причин, по которым может потребоваться изменить приложение. Может потребоваться добавить новую функцию в приложение, исправить ошибку в приложении или изменить способ реализации функции приложения. Приложения редко являются статическими. Они, как правило, растут и мутируют с течением времени.

Представьте, например, что вы решили изменить способ реализации уровня доступа к данным. Сейчас приложение Диспетчера контактов использует Microsoft Entity Framework для доступа к базе данных. Однако вы можете перейти на новую или альтернативную технологию доступа к данным, например ADO.NET Data Services или NHibernate. Однако, так как код доступа к данным не изолирован от кода проверки и контроллера, невозможно изменить код доступа к данным в приложении без изменения другого кода, не связанного непосредственно с доступом к данным.

С другой стороны, если приложение слабо связано, можно вносить изменения в одну часть приложения, не затрагивая другие части приложения. Например, можно переключать технологии доступа к данным, не изменяя логику проверки или контроллера.

В этой итерации мы воспользуемся преимуществами нескольких шаблонов проектирования программного обеспечения, которые позволяют нам выполнить рефакторинг приложения Диспетчера контактов в более слабо связанное приложение. Когда мы закончим, диспетчер контактов не будет делать ничего, что он не делал раньше. Однако в будущем мы сможем легко изменить приложение.

Примечание

Рефакторинг — это процесс перезаписи приложения таким образом, чтобы оно не потеряло существующую функциональность.

Использование шаблона разработки программного обеспечения репозитория

Первое изменение заключается в том, чтобы воспользоваться шаблоном проектирования программного обеспечения, который называется шаблоном репозитория. Мы будем использовать шаблон репозитория, чтобы изолировать код доступа к данным от остальной части приложения.

Для реализации шаблона репозитория необходимо выполнить следующие два шага:

  1. Создание интерфейса
  2. Создание конкретного класса, реализующего интерфейс

Во-первых, необходимо создать интерфейс, описывающий все методы доступа к данным, которые необходимо выполнить. Интерфейс IContactManagerRepository содержится в листинге 1. Этот интерфейс описывает пять методов: CreateContact(), DeleteContact(), EditContact(), GetContact и ListContacts().

Листинг 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

Далее необходимо создать конкретный класс, реализующий интерфейс IContactManagerRepository. Так как мы используем Microsoft Entity Framework для доступа к базе данных, мы создадим новый класс с именем EntityContactManagerRepository. Этот класс содержится в листинге 2.

Листинг 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

Обратите внимание, что класс EntityContactManagerRepository реализует интерфейс IContactManagerRepository. Класс реализует все пять методов, описанных в этом интерфейсе.

Вы можете задаться вопросом, почему нам нужно беспокоиться с интерфейсом. Зачем нужно создавать интерфейс и класс, который его реализует?

За одним исключением, оставшаяся часть приложения будет взаимодействовать с интерфейсом, а не с конкретным классом. Вместо вызова методов, предоставляемых классом EntityContactManagerRepository, мы будем вызывать методы, предоставляемые интерфейсом IContactManagerRepository.

Таким образом, мы сможем реализовать интерфейс с помощью нового класса, не изменяя оставшуюся часть приложения. Например, в будущем может потребоваться реализовать класс DataServicesContactManagerRepository, реализующий интерфейс IContactManagerRepository. Класс DataServicesContactManagerRepository может использовать ADO.NET Data Services для доступа к базе данных вместо Microsoft Entity Framework.

Если код приложения запрограммирован на интерфейс IContactManagerRepository вместо конкретного класса EntityContactManagerRepository, мы можем переключать конкретные классы, не изменяя остальную часть кода. Например, можно переключиться с класса EntityContactManagerRepository на класс DataServicesContactManagerRepository, не изменяя логику доступа к данным или проверки.

Программирование на основе интерфейсов (абстракций) вместо конкретных классов делает наше приложение более устойчивым к изменениям.

Примечание

Вы можете быстро создать интерфейс из конкретного класса в Visual Studio, выбрав пункт меню Рефакторинг, Извлечь интерфейс. Например, можно сначала создать класс EntityContactManagerRepository, а затем использовать extract Interface для автоматического создания интерфейса IContactManagerRepository.

Использование шаблона проектирования программного обеспечения внедрения зависимостей

Теперь, когда мы перенесли код доступа к данным в отдельный класс Repository, необходимо изменить контроллер Contact для использования этого класса. Мы воспользуемся шаблоном проектирования программного обеспечения, который называется внедрением зависимостей, для использования класса Repository в контроллере.

Измененный контроллер контактов содержится в листинге 3.

Листинг 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

Обратите внимание, что контроллер Contact в листинге 3 содержит два конструктора. Первый конструктор передает во второй конструктор конкретный экземпляр интерфейса IContactManagerRepository. Класс контроллера Contact использует внедрение зависимостей конструктора.

Единственное место, где используется класс EntityContactManagerRepository, находится в первом конструкторе. Оставшаяся часть класса использует интерфейс IContactManagerRepository вместо конкретного класса EntityContactManagerRepository.

Это упрощает переключение реализаций класса IContactManagerRepository в будущем. Если вы хотите использовать класс DataServicesContactRepository вместо класса EntityContactManagerRepository, просто измените первый конструктор.

Внедрение зависимостей конструктора также делает класс контроллера Contact очень тестируемым. В модульных тестах можно создать экземпляр контроллера Contact, передав макет реализации класса IContactManagerRepository. Эта функция внедрения зависимостей будет очень важна для нас в следующей итерации при создании модульных тестов для приложения Диспетчера контактов.

Примечание

Если вы хотите полностью отделить класс контроллера Contact от определенной реализации интерфейса IContactManagerRepository, вы можете воспользоваться преимуществами платформы, поддерживающей внедрение зависимостей, например StructureMap или Microsoft Entity Framework (MEF). Используя платформу внедрения зависимостей, вам никогда не нужно ссылаться на конкретный класс в коде.

Создание уровня служб

Возможно, вы заметили, что наша логика проверки по-прежнему смешивается с логикой контроллера в измененном классе контроллера в листинге 3. По той же причине, по которой рекомендуется изолировать логику доступа к данным, рекомендуется изолировать логику проверки.

Чтобы устранить эту проблему, можно создать отдельный уровень служб. Уровень служб — это отдельный слой, который можно вставить между классами контроллера и репозитория. Уровень служб содержит нашу бизнес-логику, включая всю логику проверки.

Объект ContactManagerService содержится в листинге 4. Он содержит логику проверки из класса контроллера Contact.

Листинг 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

Обратите внимание, что конструктору contactManagerService требуется ValidationDictionary. Уровень службы взаимодействует с уровнем контроллера через этот ValidationDictionary. Мы подробно рассмотрим ValidationDictionary в следующем разделе при обсуждении шаблона декоратора.

Обратите внимание, что ContactManagerService реализует интерфейс IContactManagerService. Всегда следует стараться программировать по интерфейсам, а не к конкретным классам. Другие классы в приложении Диспетчера контактов не взаимодействуют с классом ContactManagerService напрямую. Вместо этого, за одним исключением, оставшаяся часть приложения диспетчера контактов программируется на интерфейс IContactManagerService.

Интерфейс IContactManagerService содержится в листинге 5.

Листинг 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

Измененный класс контроллера Contact содержится в листинге 6. Обратите внимание, что контроллер контактов больше не взаимодействует с репозиторием ContactManager. Вместо этого контроллер контактов взаимодействует со службой ContactManager. Каждый слой максимально изолирован от других слоев.

Листинг 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

Наше приложение больше не работает в соответствие с принципом единой ответственности (SRP). Контроллер контактов, приведенный в листинге 6, был лишен всех обязанностей, кроме управления потоком выполнения приложения. Вся логика проверки была удалена из контроллера контактов и отправлена на уровень служб. Вся логика базы данных была отправлена на уровень репозитория.

Использование шаблона декоратора

Мы хотим иметь возможность полностью отделить уровень служб от уровня контроллера. В принципе, мы должны иметь возможность компилировать уровень служб в отдельной сборке из уровня контроллера без необходимости добавлять ссылку на наше приложение MVC.

Однако наш уровень служб должен иметь возможность передавать сообщения об ошибках проверки обратно на уровень контроллера. Как включить уровень служб для передачи сообщений об ошибках проверки без связывания контроллера и уровня служб? Мы можем воспользоваться шаблоном разработки программного обеспечения, который называется шаблоном декоратора.

Контроллер использует ModelStateDictionary с именем ModelState для представления ошибок проверки. Поэтому может возникнуть соблазн передать ModelState со уровня контроллера на уровень службы. Однако использование ModelState на уровне служб сделает уровень службы зависимым от функции платформы ASP.NET MVC. Это может быть плохо, потому что когда-нибудь вы захотите использовать уровень служб с приложением WPF вместо приложения ASP.NET MVC. В этом случае не требуется ссылаться на ASP.NET платформы MVC для использования класса ModelStateDictionary.

Шаблон Декоратора позволяет упаковать существующий класс в новый класс, чтобы реализовать интерфейс. Наш проект Диспетчера контактов включает класс ModelStateWrapper, содержащийся в листинге 7. Класс ModelStateWrapper реализует интерфейс, приведенный в листинге 8.

Листинг 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

Листинг 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

Если вы внимательно изучите листинг 5, то увидите, что уровень служб ContactManager использует исключительно интерфейс IValidationDictionary. Служба ContactManager не зависит от класса ModelStateDictionary. Когда контроллер Contact создает службу ContactManager, контроллер создает оболочку modelState следующим образом:

_service = new ContactManagerService(New ModelStateWrapper(ModelState))

Итоги

В этой итерации мы не добавили никаких новых функций в приложение Диспетчера контактов. Цель этой итерации состояла в рефакторинге приложения Диспетчера контактов, чтобы упростить обслуживание и изменение.

Во-первых, мы реализовали шаблон проектирования программного обеспечения репозитория. Мы перенесли весь код доступа к данным в отдельный класс репозитория ContactManager.

Мы также изолировали логику проверки от логики контроллера. Мы создали отдельный уровень служб, содержащий весь код проверки. Уровень контроллера взаимодействует со службой, а уровень службы — со слоем репозитория.

При создании уровня служб мы воспользовались шаблоном Декоратора, чтобы изолировать ModelState от уровня служб. На уровне служб мы запрограммировали интерфейс IValidationDictionary вместо ModelState.

Наконец, мы воспользовались шаблоном разработки программного обеспечения, который называется шаблоном внедрения зависимостей. Этот шаблон позволяет программировать на основе интерфейсов (абстракций) вместо конкретных классов. Реализация шаблона проектирования внедрения зависимостей также делает наш код более тестируемым. В следующей итерации мы добавим модульные тесты в проект.