Iterace č. 4 – vytvoření volně spárované aplikace (VB)
od Microsoftu
V této čtvrté iteraci využijeme několik vzorů návrhu softwaru, abychom usnadnili údržbu a úpravu aplikace Contact Manager. Například refaktorujeme aplikaci tak, aby používala vzor Úložiště a injektáž závislostí.
Vytvoření ASP.NET aplikace MVC (VB) pro správu kontaktů
V této sérii kurzů sestavíme celou aplikaci Pro správu kontaktů od začátku do konce. Aplikace Contact Manager umožňuje ukládat kontaktní informace – jména, telefonní čísla a e-mailové adresy – pro seznam lidí.
Aplikaci sestavíme pomocí několika iterací. S každou iterací aplikaci postupně vylepšujeme. Cílem tohoto přístupu k vícenásobné iteraci je pochopit důvod každé změny.
Iterace č. 1 – vytvořte aplikaci. V první iteraci vytvoříme Správce kontaktů nejjednodušším možným způsobem. Přidáváme podporu pro základní databázové operace: vytvoření, čtení, aktualizace a odstranění (CRUD).
Iterace č. 2 – aby aplikace vypadala hezky. V této iteraci vylepšujeme vzhled aplikace úpravou výchozí stránky předlohy zobrazení ASP.NET zobrazení MVC a šablony stylů CSS.
Iterace č. 3 – přidejte ověření formuláře. Ve třetí iteraci přidáme základní ověření formuláře. Bráníme uživatelům v odesílání formuláře bez vyplnění požadovaných polí formuláře. Ověřujeme také e-mailové adresy a telefonní čísla.
Iterace č. 4 – Nastavte aplikaci volně provázanou. V této čtvrté iteraci využijeme několik vzorů návrhu softwaru, abychom usnadnili údržbu a úpravu aplikace Contact Manager. Například refaktorujeme aplikaci tak, aby používala vzor Úložiště a injektáž závislostí.
Iterace č. 5 – vytvoření testů jednotek V páté iteraci usnadňujeme údržbu a úpravy naší aplikace přidáním testů jednotek. Napodobení tříd datového modelu a sestavení testů jednotek pro naše kontrolery a logiku ověřování.
Iterace č. 6 – použijte vývoj řízený testy. V této šesté iteraci přidáme do naší aplikace nové funkce tím, že nejprve napíšeme testy jednotek a napíšeme kód proti testům jednotek. V této iteraci přidáme skupiny kontaktů.
Iterace č. 7 – přidání funkcí Ajax. V sedmé iteraci vylepšujeme rychlost odezvy a výkon naší aplikace přidáním podpory pro Ajax.
Tato iterace
V této čtvrté iteraci aplikace Contact Manager refaktorujeme aplikaci, aby byla aplikace volněji svázána. Pokud je aplikace volně svázána, můžete upravit kód v jedné části aplikace, aniž byste museli upravovat kód v jiných částech aplikace. Volně propojené aplikace jsou odolnější vůči změnám.
V současné době je veškerá logika přístupu k datům a ověřování používaná aplikací Contact Manager obsažena v třídách kontroleru. To je špatný nápad. Kdykoli potřebujete upravit jednu část aplikace, riskujete, že chyby zavedou do jiné části aplikace. Pokud například upravíte logiku ověřování, riskujete, že do logiky přístupu k datům nebo kontroleru zavedou nové chyby.
Poznámka
(SRP), třída by nikdy neměla mít více než jeden důvod ke změně. Míchání kontroleru, ověřování a logiky databáze je masivním porušením principu jednotné odpovědnosti.
Existuje několik důvodů, proč můžete aplikaci upravit. Možná budete muset do aplikace přidat novou funkci, možná budete muset opravit chybu v aplikaci nebo budete muset upravit způsob implementace funkce aplikace. Aplikace jsou zřídka statické. Mají tendenci růst a mutovat v průběhu času.
Představte si například, že se rozhodnete změnit způsob implementace vrstvy přístupu k datům. V současné chvíli aplikace Contact Manager používá pro přístup k databázi rozhraní Microsoft Entity Framework. Můžete se ale rozhodnout migrovat na novou nebo alternativní technologii přístupu k datům, jako je ADO.NET Data Services nebo NHibernate. Vzhledem k tomu, že kód pro přístup k datům není izolovaný od kódu ověřování a kontroleru, neexistuje způsob, jak upravit přístupový kód k datům ve vaší aplikaci bez úpravy jiného kódu, který přímo nesouvisí s přístupem k datům.
Pokud je aplikace volně svázána, můžete na druhé straně provádět změny v jedné části aplikace, aniž byste se dotýkali jiných částí aplikace. Můžete například přepínat technologie přístupu k datům beze změny logiky ověřování nebo kontroleru.
V této iteraci využijeme několik vzorů návrhu softwaru, které nám umožňují refaktorovat aplikaci Contact Manager do volněji propojené aplikace. Až skončíme, správce kontaktů nebude dělat nic, co předtím neudělal. V budoucnu ale budeme moct aplikaci snadněji změnit.
Poznámka
Refaktoring je proces přepsání aplikace takovým způsobem, aby neztratil žádné existující funkce.
Použití vzoru návrhu softwaru úložiště
Naší první změnou je využít vzor návrhu softwaru označovaný jako model úložiště. Model Úložiště použijeme k izolaci přístupového kódu k datům od zbytku naší aplikace.
Implementace modelu úložiště vyžaduje, abychom dokončili následující dva kroky:
- Vytvoření rozhraní
- Vytvoření konkrétní třídy, která implementuje rozhraní
Nejprve musíme vytvořit rozhraní, které popisuje všechny metody přístupu k datům, které musíme provést. Rozhraní IContactManagerRepository je obsaženo v výpisu 1. Toto rozhraní popisuje pět metod: CreateContact(), DeleteContact(), EditContact(), GetContact a ListContacts().
Výpis 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
Dále musíme vytvořit konkrétní třídu, která implementuje rozhraní IContactManagerRepository. Vzhledem k tomu, že pro přístup k databázi používáme Microsoft Entity Framework, vytvoříme novou třídu s názvem EntityContactManagerRepository. Tato třída je obsažena ve výpisu 2.
Výpis 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
Všimněte si, že Třída EntityContactManagerRepository implementuje rozhraní IContactManagerRepository. Třída implementuje všech pět metod popsaných tímto rozhraním.
Možná se divíte, proč se musíme obtěžovat s rozhraním. Proč potřebujeme vytvořit rozhraní i třídu, která ho implementuje?
Až na jednu výjimku bude zbytek naší aplikace pracovat s rozhraním, a ne s konkrétní třídou. Místo volání metod vystavených EntityContactManagerRepository třídy, budeme volat metody vystavené rozhraním IContactManagerRepository.
Tímto způsobem můžeme implementovat rozhraní s novou třídou, aniž bychom museli upravovat zbytek naší aplikace. Například k určitému budoucímu datu můžeme chtít implementovat DataServicesContactManagerRepository třídy, která implementuje rozhraní IContactManagerRepository. DataServicesContactManagerRepository Třída může používat ADO.NET Data Services pro přístup k databázi místo Microsoft Entity Framework.
Pokud je kód aplikace naprogramován proti rozhraní IContactManagerRepository místo konkrétní entityContactManagerRepository třídy, můžeme přepnout konkrétní třídy beze změny některého ze zbytku našeho kódu. Můžeme například přepnout z EntityContactManagerRepository třídy DataServicesContactManagerRepository třídy beze změny našeho přístupu k datům nebo logiky ověřování.
Programování proti rozhraním (abstrakcí) místo konkrétních tříd činí naši aplikaci odolnější vůči změnám.
Poznámka
V sadě Visual Studio můžete rychle vytvořit rozhraní z konkrétní třídy výběrem možnosti nabídky Refaktorovat, Extrahovat rozhraní. Můžete například nejprve vytvořit třídu EntityContactManagerRepository a pak použít Extrahovat rozhraní k automatickému vygenerování rozhraní IContactManagerRepository.
Použití vzoru návrhu softwaru pro injektáž závislostí
Teď, když jsme migrovali přístupový kód k datům do samostatné třídy Repository, musíme upravit kontroler kontaktů tak, aby tuto třídu používal. K použití třídy Repository v našem kontroleru využijeme vzor návrhu softwaru s názvem Dependency Injection.
Upravený kontroler kontaktu je obsažen ve výpisu 3.
Výpis 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
Všimněte si, že kontroler kontaktu v seznamu 3 má dva konstruktory. První konstruktor předává konkrétní instanci IContactManagerRepository rozhraní do druhého konstruktoru. Třída kontroleru kontaktu používá injektáž závislostí konstruktoru.
Jediné místo, které EntityContactManagerRepository třídy je v prvním konstruktoru. Zbytek třídy používá IContactManagerRepository rozhraní místo konkrétní EntityContactManagerRepository třídy.
To usnadňuje přepínání implementací IContactManagerRepository třídy v budoucnu. Pokud chcete použít Třídu DataServicesContactRepository místo třídy EntityContactManagerRepository, stačí upravit první konstruktor.
Injektáž závislostí konstruktoru také velmi testuje třídu kontroleru kontaktů. V testech jednotek můžete vytvořit instanci kontroleru kontaktu předáním napodobené implementace IContactManagerRepository třídy. Tato funkce injektáže závislostí bude pro nás velmi důležitá v další iteraci při sestavování testů jednotek pro aplikaci Contact Manager.
Poznámka
Pokud chcete zcela oddělit třídu kontroleru kontaktů od konkrétní implementace rozhraní IContactManagerRepository pak můžete využít rozhraní, které podporuje injektáž závislostí, jako je Například StructureMap nebo Microsoft Entity Framework (MEF). Když využijete architekturu injektáže závislostí, nemusíte v kódu odkazovat na konkrétní třídu.
Vytvoření vrstvy služby
Možná jste si všimli, že naše logika ověřování je stále smíšená s logikou kontroleru ve třídě upraveného kontroleru ve výpisu 3. Ze stejného důvodu, jako je vhodné izolovat logiku přístupu k datům, je vhodné izolovat logiku ověřování.
Abychom tento problém vyřešili, můžeme vytvořit samostatnou vrstvu služby. Vrstva služby je samostatná vrstva, kterou můžeme vložit mezi třídu kontroleru a úložiště. Vrstva služby obsahuje naši obchodní logiku včetně veškeré logiky ověřování.
Služba ContactManagerService je obsažena ve výpisu 4. Obsahuje logiku ověřování z třídy Kontroleru kontaktů.
Výpis 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
Všimněte si, že konstruktor pro ContactManagerService vyžaduje ValidationDictionary. Vrstva služby komunikuje s vrstvou kontroleru prostřednictvím tohoto ověřovacího slovníku. Ověřovací slovník probereme podrobně v následující části, kde probereme model Dekorator.
Všimněte si také, že ContactManagerService implementuje rozhraní IContactManagerService. Měli byste se vždy snažit programovat proti rozhraním místo konkrétních tříd. Jiné třídy v aplikaci Contact Manager nepracují přímo s třídou ContactManagerService. Místo toho, s jednou výjimkou, zbytek aplikace Contact Manager je naprogramován na IContactManagerService rozhraní.
Rozhraní IContactManagerService je obsaženo ve výpisu 5.
Výpis 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
Upravená třída kontroleru kontaktů je obsažena ve výpisu 6. Všimněte si, že kontroler kontaktů už nepracuje s úložištěm ContactManager. Místo toho kontakt kontroler komunikuje se službou ContactManager. Každá vrstva je co nejvíce izolovaná od ostatních vrstev.
Výpis 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
Naše aplikace už není v souladu s principem SRP (Single Responsibility Principle). Kontroleru kontaktů ve výpisu 6 byly odebrány všechny zodpovědnosti kromě řízení toku provádění aplikace. Veškerá logika ověřování byla odebrána z kontroleru kontaktu a vložena do vrstvy služby. Veškerá logika databáze byla vložena do vrstvy úložiště.
Použití vzoru dekorátoru
Chceme mít možnost zcela oddělit vrstvu služby od vrstvy kontroleru. V zásadě bychom měli být schopni zkompilovat naši vrstvu služby v samostatném sestavení, než je vrstva kontroleru, aniž bychom museli přidávat odkaz na naši aplikaci MVC.
Naše vrstva služby ale musí být schopná předávat chybové zprávy ověřování zpět vrstvě kontroleru. Jak můžeme vrstvě služby povolit, aby předávala chybové zprávy ověřování bez propojení kontroleru a vrstvy služby? Můžeme využít vzor návrhu softwaru s názvem Dekorátor.
Kontroler používá ModelStateDictionary s názvem ModelState k reprezentaci chyb ověření. Proto můžete být v pokušení předat ModelState z vrstvy kontroleru do vrstvy služby. Pokud ale použijete ModelState ve vrstvě služby, bude vaše vrstva služby závislá na funkci architektury ASP.NET MVC. To by bylo špatné, protože jednou budete chtít použít vrstvu služby s aplikací WPF místo aplikace ASP.NET MVC. V takovém případě byste nechtěli odkazovat na architekturu ASP.NET MVC pro použití třídy ModelStateDictionary.
Model Dekorátor umožňuje zabalit existující třídu do nové třídy, aby bylo možné implementovat rozhraní. Náš projekt Contact Manageru zahrnuje třídu ModelStateWrapper obsaženou ve výpisu 7. ModelStateWrapper Třída implementuje rozhraní ve výpisu 8.
Výpis 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
Výpis 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
Pokud se na výpis 5 podíváte zblízka, uvidíte, že vrstva služby ContactManager používá výhradně rozhraní IValidationDictionary. Služba Není závislá na ModelStateDictionary třídy. Když kontroler kontaktů vytvoří službu ContactManager, zabalí kontroler modelState takto:
_service = new ContactManagerService(New ModelStateWrapper(ModelState))
Souhrn
V této iteraci jsme do aplikace Contact Manager nepřidali žádné nové funkce. Cílem této iterace bylo refaktorovat aplikaci Contact Manager tak, aby se snadněji udržovala a upravovala.
Nejprve jsme implementovali vzor návrhu softwaru repository. Veškerý kód pro přístup k datům jsme migrovali do samostatné třídy úložiště ContactManager.
Také jsme oddělili logiku ověřování od logiky kontroleru. Vytvořili jsme samostatnou vrstvu služby, která obsahuje veškerý ověřovací kód. Vrstva kontroleru komunikuje s vrstvou služby a vrstva služby komunikuje s vrstvou úložiště.
Při vytváření vrstvy služby jsme využili vzor Decorator k izolování modelu ModelState od vrstvy služby. V naší vrstvě služby jsme naprogramovali rozhraní IValidationDictionary místo modelState.
Nakonec jsme využili vzor návrhu softwaru s názvem Model injektáže závislostí. Tento model nám umožňuje programovat proti rozhraním (abstrakcím) místo konkrétních tříd. Implementace vzoru návrhu injektáže závislostí také zpřístupňuje testování našeho kódu. V další iteraci přidáme do projektu testy jednotek.