Проверка с помощью уровня службы (VB)

Стивен Уолтер (Stephen Walther)

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

Цель этого руководства — описать один из методов проверки в ASP.NET приложении MVC. В этом руководстве описано, как переместить логику проверки из контроллеров в отдельный уровень служб.

Разделение проблем

При создании приложения ASP.NET MVC не следует помещать логику базы данных в действия контроллера. Сочетание логики базы данных и контроллера усложняет обслуживание приложения с течением времени. Рекомендуется поместить всю логику базы данных в отдельный уровень репозитория.

Например, в листинге 1 содержится простой репозиторий с именем ProductRepository. Репозиторий продукта содержит весь код доступа к данным для приложения. В списке также содержится интерфейс IProductRepository, который реализует репозиторий продуктов.

Листинг 1. Models\ProductRepository.vb

Public Class ProductRepository
Implements IProductRepository

    Private _entities As New ProductDBEntities()


Public Function ListProducts() As IEnumerable(Of Product) Implements IProductRepository.ListProducts
    Return _entities.ProductSet.ToList()
End Function


Public Function CreateProduct(ByVal productToCreate As Product) As Boolean Implements IProductRepository.CreateProduct
    Try
        _entities.AddToProductSet(productToCreate)
        _entities.SaveChanges()
        Return True
    Catch
        Return False
    End Try
End Function

End Class

Public Interface IProductRepository
Function CreateProduct(ByVal productToCreate As Product) As Boolean
Function ListProducts() As IEnumerable(Of Product)
End Interface

Контроллер, приведенный в листинге 2, использует уровень репозитория в действиях Index() и Create(). Обратите внимание, что этот контроллер не содержит логику базы данных. Создание уровня репозитория позволяет поддерживать четкое разделение задач. Контроллеры отвечают за логику управления потоком приложений, а репозиторий отвечает за логику доступа к данным.

Листинг 2. Controllers\ProductController.vb

Public Class ProductController
Inherits Controller

    Private _repository As IProductRepository

Public Sub New()
    Me.New(New ProductRepository())
End Sub


Public Sub New(ByVal repository As IProductRepository)
    _repository = repository
End Sub


Public Function Index() As ActionResult
    Return View(_repository.ListProducts())
End Function


'
' GET: /Product/Create

Public Function Create() As ActionResult
    Return View()
End Function

'
' POST: /Product/Create

<AcceptVerbs(HttpVerbs.Post)> _
Public Function Create(<Bind(Exclude:="Id")> ByVal productToCreate As Product) As ActionResult
    _repository.CreateProduct(productToCreate)
    Return RedirectToAction("Index")
End Function

End Class

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

Таким образом, логика управления потоком приложения принадлежит контроллеру, а логика доступа к данным — в репозитории. В этом случае, куда поместить логику проверки? Одним из вариантов является размещение логики проверки на уровне служб.

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

Например, уровень службы продукта в листинге 3 имеет метод CreateProduct(). Метод CreateProduct() вызывает метод ValidateProduct() для проверки нового продукта перед передачей продукта в репозиторий продуктов.

Листинг 3. Models\ProductService.vb

Public Class ProductService
Implements IProductService

Private _modelState As ModelStateDictionary
Private _repository As IProductRepository

Public Sub New(ByVal modelState As ModelStateDictionary, ByVal repository As IProductRepository)
    _modelState = modelState
    _repository = repository
End Sub

Protected Function ValidateProduct(ByVal productToValidate As Product) As Boolean
    If productToValidate.Name.Trim().Length = 0 Then
        _modelState.AddModelError("Name", "Name is required.")
    End If
    If productToValidate.Description.Trim().Length = 0 Then
        _modelState.AddModelError("Description", "Description is required.")
    End If
    If productToValidate.UnitsInStock

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

Листинг 4. Controllers\ProductController.vb

Public Class ProductController
Inherits Controller

Private _service As IProductService

Public Sub New()
    _service = New ProductService(Me.ModelState, New ProductRepository())
End Sub

Public Sub New(ByVal service As IProductService)
    _service = service
End Sub


Public Function Index() As ActionResult
    Return View(_service.ListProducts())
End Function


'
' GET: /Product/Create

Public Function Create() As ActionResult
    Return View()
End Function

'
' POST: /Product/Create

<AcceptVerbs(HttpVerbs.Post)> _
Public Function Create(<Bind(Exclude := "Id")> ByVal productToCreate As Product) As ActionResult
    If Not _service.CreateProduct(productToCreate) Then
        Return View()
    End If
    Return RedirectToAction("Index")
End Function

End Class

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

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

Мы не смогли изолировать уровни контроллера и службы в одном отношении. Уровни контроллера и службы взаимодействуют через состояние модели. Другими словами, уровень служб зависит от определенной функции платформы ASP.NET MVC.

Мы хотим максимально изолировать уровень служб от уровня контроллера. Теоретически мы должны иметь возможность использовать уровень служб с любым типом приложения, а не только с приложением ASP.NET MVC. Например, в будущем может потребоваться создать интерфейс WPF для нашего приложения. Мы должны найти способ удалить зависимость от ASP.NET состояния модели MVC с уровня служб.

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

Листинг 5. Models\ProductService.vb (разделенный)

Public Class ProductService
Implements IProductService

Private _validatonDictionary As IValidationDictionary
Private _repository As IProductRepository

Public Sub New(ByVal validationDictionary As IValidationDictionary, ByVal repository As IProductRepository)
    _validatonDictionary = validationDictionary
    _repository = repository
End Sub

Protected Function ValidateProduct(ByVal productToValidate As Product) As Boolean
    If productToValidate.Name.Trim().Length = 0 Then
        _validatonDictionary.AddError("Name", "Name is required.")
    End If
    If productToValidate.Description.Trim().Length = 0 Then
        _validatonDictionary.AddError("Description", "Description is required.")
    End If
    If productToValidate.UnitsInStock

Интерфейс IValidationDictionary определен в листинге 6. Этот простой интерфейс имеет один метод и одно свойство.

Листинг 6. Models\IValidationDictionary.cs

Public Interface IValidationDictionary
Sub AddError(ByVal key As String, ByVal errorMessage As String)
ReadOnly Property IsValid() As Boolean
End Interface

Класс в листинге 7 с именем класса ModelStateWrapper реализует интерфейс IValidationDictionary. Вы можете создать экземпляр класса ModelStateWrapper, передав в конструктор словарь состояния модели.

Листинг 7. Models\ModelStateWrapper.vb

Public Class ModelStateWrapper
Implements IValidationDictionary

Private _modelState As ModelStateDictionary

Public Sub New(ByVal modelState As ModelStateDictionary)
    _modelState = modelState
End Sub

#Region "IValidationDictionary Members"

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 Region

End Class

Наконец, обновленный контроллер в листинге 8 использует ModelStateWrapper при создании уровня службы в конструкторе.

Листинг 8. Controllers\ProductController.vb

Public Class ProductController
Inherits Controller

Private _service As IProductService

Public Sub New()
    _service = New ProductService(New ModelStateWrapper(Me.ModelState), New ProductRepository())
End Sub

Public Sub New(ByVal service As IProductService)
    _service = service
End Sub


Public Function Index() As ActionResult
    Return View(_service.ListProducts())
End Function


'
' GET: /Product/Create

Public Function Create() As ActionResult
    Return View()
End Function

'
' POST: /Product/Create

<AcceptVerbs(HttpVerbs.Post)> _
Public Function Create(<Bind(Exclude := "Id")> ByVal productToCreate As Product) As ActionResult
    If Not _service.CreateProduct(productToCreate) Then
        Return View()
    End If
    Return RedirectToAction("Index")
End Function

End Class

Использование интерфейса IValidationDictionary и класса ModelStateWrapper позволяет полностью изолировать уровень служб от уровня контроллера. Уровень служб больше не зависит от состояния модели. Вы можете передать любой класс, реализующий интерфейс IValidationDictionary, на уровень службы. Например, приложение WPF может реализовать интерфейс IValidationDictionary с простым классом коллекции.

Итоги

Цель этого руководства состояла в том, чтобы обсудить один из подходов к выполнению проверки в приложении ASP.NET MVC. В этом руководстве вы узнали, как переместить всю логику проверки из контроллеров в отдельный уровень служб. Вы также узнали, как изолировать уровень службы от уровня контроллера, создав класс ModelStateWrapper.