이 네 번째 반복에서는 여러 소프트웨어 디자인 패턴을 활용하여 Contact Manager 애플리케이션을 더 쉽게 유지 관리하고 수정할 수 있습니다. 예를 들어 리포지토리 패턴 및 종속성 주입 패턴을 사용하도록 애플리케이션을 리팩터링합니다.
VB(연락처 관리 ASP.NET MVC 애플리케이션) 빌드
이 자습서 시리즈에서는 처음부터 끝까지 전체 연락처 관리 애플리케이션을 빌드합니다. Contact Manager 애플리케이션을 사용하면 사용자 목록에 대한 연락처 정보(이름, 전화 번호 및 전자 메일 주소)를 저장할 수 있습니다.
여러 반복을 통해 애플리케이션을 빌드합니다. 반복할 때마다 애플리케이션을 점진적으로 개선합니다. 이 여러 반복 방법의 목표는 각 변경의 이유를 이해할 수 있도록 하는 것입니다.
반복 #1 - 애플리케이션을 만듭니다. 첫 번째 반복에서는 가능한 가장 간단한 방법으로 연락처 관리자를 만듭니다. CRUD(만들기, 읽기, 업데이트 및 삭제)의 기본 데이터베이스 작업에 대한 지원을 추가합니다.
반복 #2 - 애플리케이션을 멋지게 만듭니다. 이 반복에서는 기본 ASP.NET MVC 보기 master 페이지 및 계단식 스타일시트를 수정하여 애플리케이션의 모양을 개선합니다.
반복 #3 - 양식 유효성 검사를 추가합니다. 세 번째 반복에서는 기본 양식 유효성 검사를 추가합니다. 사용자가 필요한 양식 필드를 완료하지 않고 양식을 제출하지 못하도록 합니다. 또한 전자 메일 주소 및 전화 번호의 유효성을 검사합니다.
반복 #4 - 애플리케이션을 느슨하게 결합합니다. 이 네 번째 반복에서는 여러 소프트웨어 디자인 패턴을 활용하여 Contact Manager 애플리케이션을 더 쉽게 유지 관리하고 수정할 수 있습니다. 예를 들어 리포지토리 패턴 및 종속성 주입 패턴을 사용하도록 애플리케이션을 리팩터링합니다.
반복 #5 - 단위 테스트 만들기 다섯 번째 반복에서는 단위 테스트를 추가하여 애플리케이션을 더 쉽게 유지 관리하고 수정할 수 있습니다. 데이터 모델 클래스를 모의하고 컨트롤러 및 유효성 검사 논리에 대한 단위 테스트를 빌드합니다.
반복 #6 - 테스트 기반 개발을 사용합니다. 이 여섯 번째 반복에서는 먼저 단위 테스트를 작성하고 단위 테스트에 대한 코드를 작성하여 애플리케이션에 새로운 기능을 추가합니다. 이 반복에서는 연락처 그룹을 추가합니다.
반복 #7 - Ajax 기능 추가 일곱 번째 반복에서는 Ajax에 대한 지원을 추가하여 애플리케이션의 응답성과 성능을 향상시킵니다.
이 반복
Contact Manager 애플리케이션의 네 번째 반복에서는 애플리케이션을 더 느슨하게 결합하도록 애플리케이션을 리팩터링합니다. 애플리케이션이 느슨하게 결합된 경우 애플리케이션의 다른 부분에서 코드를 수정할 필요 없이 애플리케이션의 한 부분에서 코드를 수정할 수 있습니다. 느슨하게 결합된 애플리케이션은 변경에 더 탄력적입니다.
현재 Contact Manager 애플리케이션에서 사용하는 모든 데이터 액세스 및 유효성 검사 논리는 컨트롤러 클래스에 포함되어 있습니다. 이것은 나쁜 생각이다. 애플리케이션의 한 부분을 수정해야 할 때마다 애플리케이션의 다른 부분에 버그가 발생할 위험이 있습니다. 예를 들어 유효성 검사 논리를 수정하면 데이터 액세스 또는 컨트롤러 논리에 새 버그가 도입될 위험이 있습니다.
참고
(SRP) 클래스는 변경할 이유가 두 개 이상 없어야 합니다. 컨트롤러, 유효성 검사 및 데이터베이스 논리를 혼합하는 것은 단일 책임 원칙을 크게 위반하는 것입니다.
애플리케이션을 수정해야 하는 몇 가지 이유가 있습니다. 애플리케이션에 새 기능을 추가해야 하거나, 애플리케이션에서 버그를 수정해야 하거나, 애플리케이션의 기능을 구현하는 방법을 수정해야 할 수도 있습니다. 애플리케이션은 거의 정적입니다. 그들은 성장하고 시간이 지남에 따라 돌연변이하는 경향이있다.
예를 들어 데이터 액세스 계층을 구현하는 방법을 변경하기로 결정한다고 상상해 보십시오. 현재 Contact Manager 애플리케이션은 Microsoft Entity Framework를 사용하여 데이터베이스에 액세스합니다. 그러나 ADO.NET Data Services 또는 NHibernate와 같은 새로운 또는 대체 데이터 액세스 기술로 마이그레이션하기로 결정할 수 있습니다. 그러나 데이터 액세스 코드는 유효성 검사 및 컨트롤러 코드에서 격리되지 않으므로 데이터 액세스와 직접 관련이 없는 다른 코드를 수정하지 않고 애플리케이션에서 데이터 액세스 코드를 수정할 수 있는 방법은 없습니다.
반면에 애플리케이션이 느슨하게 결합되면 애플리케이션의 다른 부분을 건드리지 않고 애플리케이션의 한 부분을 변경할 수 있습니다. 예를 들어 유효성 검사 또는 컨트롤러 논리를 수정하지 않고 데이터 액세스 기술을 전환할 수 있습니다.
이 반복에서는 Contact Manager 애플리케이션을 보다 느슨하게 결합된 애플리케이션으로 리팩터링할 수 있는 여러 소프트웨어 디자인 패턴을 활용합니다. 작업이 완료되면 연락처 관리자가 이전에 수행하지 않은 작업을 수행하지 않습니다. 그러나 나중에 애플리케이션을 더 쉽게 변경할 수 있습니다.
참고
리팩터링 은 기존 기능을 잃지 않는 방식으로 애플리케이션을 다시 작성하는 프로세스입니다.
리포지토리 소프트웨어 디자인 패턴 사용
첫 번째 변경 내용은 리포지토리 패턴이라는 소프트웨어 디자인 패턴을 활용하는 것입니다. 리포지토리 패턴을 사용하여 애플리케이션의 나머지 부분과 데이터 액세스 코드를 격리합니다.
리포지토리 패턴을 구현하려면 다음 두 단계를 완료해야 합니다.
- 인터페이스 만들기
- 인터페이스를 구현하는 구체적인 클래스 만들기
먼저 수행해야 하는 모든 데이터 액세스 방법을 설명하는 인터페이스를 만들어야 합니다. 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 인터페이스를 구현합니다. 클래스는 해당 인터페이스에서 설명하는 5가지 메서드를 모두 구현합니다.
인터페이스를 사용해야 하는 이유가 궁금할 수 있습니다. 인터페이스와 인터페이스를 구현하는 클래스를 모두 만들어야 하는 이유는 무엇인가요?
한 가지 예외를 제외하고 애플리케이션의 나머지 부분에서는 구체적인 클래스가 아닌 인터페이스와 상호 작용합니다. EntityContactManagerRepository 클래스에서 노출되는 메서드를 호출하는 대신 IContactManagerRepository 인터페이스에서 노출하는 메서드를 호출합니다.
이렇게 하면 애플리케이션의 나머지 부분을 수정할 필요 없이 새 클래스를 사용하여 인터페이스를 구현할 수 있습니다. 예를 들어 나중에 IContactManagerRepository 인터페이스를 구현하는 DataServicesContactManagerRepository 클래스를 구현할 수 있습니다. DataServicesContactManagerRepository 클래스는 ADO.NET Data Services를 사용하여 Microsoft Entity Framework 대신 데이터베이스에 액세스할 수 있습니다.
애플리케이션 코드가 구체적인 EntityContactManagerRepository 클래스 대신 IContactManagerRepository 인터페이스에 대해 프로그래밍된 경우 나머지 코드를 수정하지 않고 구체적인 클래스를 전환할 수 있습니다. 예를 들어 데이터 액세스 또는 유효성 검사 논리를 수정하지 않고 EntityContactManagerRepository 클래스에서 DataServicesContactManagerRepository 클래스로 전환할 수 있습니다.
구체적인 클래스 대신 인터페이스(추상화)에 대해 프로그래밍하면 애플리케이션의 변경 복원력이 향상됩니다.
참고
메뉴 옵션 리팩터링, 인터페이스 추출을 선택하여 Visual Studio 내의 구체적인 클래스에서 인터페이스를 빠르게 만들 수 있습니다. 예를 들어 EntityContactManagerRepository 클래스를 먼저 만든 다음, 인터페이스 추출을 사용하여 IContactManagerRepository 인터페이스를 자동으로 생성할 수 있습니다.
종속성 주입 소프트웨어 디자인 패턴 사용
이제 데이터 액세스 코드를 별도의 리포지토리 클래스로 마이그레이션했으므로 이 클래스를 사용하도록 Contact 컨트롤러를 수정해야 합니다. 종속성 주입이라는 소프트웨어 디자인 패턴을 활용하여 컨트롤러의 리포지토리 클래스를 사용합니다.
수정된 연락처 컨트롤러는 목록 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
목록 3의 Contact 컨트롤러에는 두 개의 생성자가 있습니다. 첫 번째 생성자는 IContactManagerRepository 인터페이스의 구체적인 instance 두 번째 생성자에 전달합니다. Contact 컨트롤러 클래스는 생성자 종속성 주입을 사용합니다.
EntityContactManagerRepository 클래스가 사용되는 유일한 위치는 첫 번째 생성자에 있습니다. 클래스의 나머지 부분에서는 구체적인 EntityContactManagerRepository 클래스 대신 IContactManagerRepository 인터페이스를 사용합니다.
이렇게 하면 나중에 IContactManagerRepository 클래스의 구현을 쉽게 전환할 수 있습니다. EntityContactManagerRepository 클래스 대신 DataServicesContactRepository 클래스를 사용하려면 첫 번째 생성자를 수정하기만 하면 됩니다.
생성자 종속성 주입은 또한 Contact 컨트롤러 클래스를 매우 테스트 가능하게 만듭니다. 단위 테스트에서 IContactManagerRepository 클래스의 모의 구현을 전달하여 Contact 컨트롤러를 인스턴스화할 수 있습니다. 이 종속성 주입 기능은 Contact Manager 애플리케이션에 대한 단위 테스트를 빌드할 때 다음 반복에서 매우 중요합니다.
참고
Contact 컨트롤러 클래스를 IContactManagerRepository 인터페이스의 특정 구현과 완전히 분리하려면 StructureMap 또는 MEF(Microsoft Entity Framework)와 같은 종속성 주입을 지원하는 프레임워크를 활용할 수 있습니다. 종속성 주입 프레임워크를 활용하여 코드에서 구체적인 클래스를 참조할 필요가 없습니다.
서비스 계층 만들기
유효성 검사 논리가 목록 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 인터페이스를 구현합니다. 항상 구체적인 클래스 대신 인터페이스에 대해 프로그래밍하려고 노력해야 합니다. Contact Manager 애플리케이션의 다른 클래스는 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 애플리케이션에 대한 참조를 추가할 필요 없이 컨트롤러 계층과 별도의 어셈블리로 서비스 계층을 컴파일할 수 있어야 합니다.
그러나 서비스 계층은 유효성 검사 오류 메시지를 컨트롤러 계층에 다시 전달할 수 있어야 합니다. 서비스 계층이 컨트롤러와 서비스 계층을 결합하지 않고 유효성 검사 오류 메시지를 통신할 수 있도록 하려면 어떻게 해야 할까요? 데코레이터 패턴이라는 소프트웨어 디자인 패턴을 활용할 수 있습니다.
컨트롤러는 ModelState라는 ModelStateDictionary를 사용하여 유효성 검사 오류를 나타냅니다. 따라서 컨트롤러 계층에서 서비스 계층으로 ModelState를 전달하려고 할 수 있습니다. 그러나 서비스 계층에서 ModelState를 사용하면 서비스 계층이 ASP.NET MVC 프레임워크의 기능에 종속됩니다. 언젠가는 ASP.NET MVC 애플리케이션 대신 WPF 애플리케이션과 함께 서비스 계층을 사용할 수 있으므로 이는 좋지 않을 수 있습니다. 이 경우 ModelStateDictionary 클래스를 사용하기 위해 ASP.NET MVC 프레임워크를 참조하지 않을 것입니다.
데코레이터 패턴을 사용하면 인터페이스를 구현하기 위해 기존 클래스를 새 클래스로 래핑할 수 있습니다. Contact Manager 프로젝트에는 목록 7에 포함된 ModelStateWrapper 클래스가 포함되어 있습니다. 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))
요약
이 반복에서는 연락처 관리자 애플리케이션에 새 기능을 추가하지 않았습니다. 이 반복의 목표는 더 쉽게 유지 관리하고 수정할 수 있도록 Contact Manager 애플리케이션을 리팩터링하는 것이었습니다.
먼저 리포지토리 소프트웨어 디자인 패턴을 구현했습니다. 모든 데이터 액세스 코드를 별도의 ContactManager 리포지토리 클래스로 마이그레이션했습니다.
또한 유효성 검사 논리를 컨트롤러 논리와 격리했습니다. 모든 유효성 검사 코드를 포함하는 별도의 서비스 계층을 만들었습니다. 컨트롤러 계층은 서비스 계층과 상호 작용하고 서비스 계층은 리포지토리 계층과 상호 작용합니다.
서비스 계층을 만들 때 데코레이터 패턴을 활용하여 ModelState를 서비스 계층에서 격리했습니다. 서비스 계층에서 ModelState 대신 IValidationDictionary 인터페이스에 대해 프로그래밍했습니다.
마지막으로 종속성 주입 패턴이라는 소프트웨어 디자인 패턴을 활용했습니다. 이 패턴을 사용하면 구체적인 클래스 대신 인터페이스(추상화)에 대해 프로그래밍할 수 있습니다. 종속성 주입 디자인 패턴을 구현하면 코드를 테스트할 수도 있습니다. 다음 반복에서는 단위 테스트를 프로젝트에 추가합니다.