Itération #4 : Rendre l’application faiblement couplée (VB)
par Microsoft
Dans cette quatrième itération, nous profitons de plusieurs modèles de conception logicielle pour faciliter la maintenance et la modification de l’application Gestionnaire de contacts. Par exemple, nous refactorisons notre application pour utiliser le modèle référentiel et le modèle d’injection de dépendances.
Génération d’une application Gestion des contacts ASP.NET MVC (VB)
Dans cette série de tutoriels, nous créons une application de gestion des contacts complète du début à la fin. L’application Gestionnaire de contacts vous permet de stocker des informations de contact (noms, numéros de téléphone et adresses e-mail) pour une liste de personnes.
Nous créons l’application sur plusieurs itérations. À chaque itération, nous améliorons progressivement l’application. L’objectif de cette approche à itérations multiples est de vous permettre de comprendre la raison de chaque modification.
Itération #1 : créez l’application. Dans la première itération, nous créons le Gestionnaire de contacts de la manière la plus simple possible. Nous ajoutons la prise en charge des opérations de base de données de base de données : Create, Read, Update et Delete (CRUD).
Itération n°2 : rendre l’application agréable. Dans cette itération, nous améliorons l’apparence de l’application en modifiant la ASP.NET vue MVC par défaut master page et la feuille de style en cascade.
Itération n°3 - Ajouter la validation de formulaire. Dans la troisième itération, nous ajoutons la validation de formulaire de base. Nous empêcherons les utilisateurs d’envoyer un formulaire sans remplir les champs de formulaire requis. Nous validons également les adresses e-mail et les numéros de téléphone.
Itération n° 4 : rendre l’application faiblement couplée. Dans cette quatrième itération, nous profitons de plusieurs modèles de conception logicielle pour faciliter la maintenance et la modification de l’application Gestionnaire de contacts. Par exemple, nous refactorisons notre application pour utiliser le modèle référentiel et le modèle d’injection de dépendances.
Itération #5 - Créer des tests unitaires. Dans la cinquième itération, nous rendons notre application plus facile à gérer et à modifier en ajoutant des tests unitaires. Nous modélisons nos classes de modèle de données et créons des tests unitaires pour nos contrôleurs et notre logique de validation.
Itération n°6 - Utiliser le développement piloté par les tests. Dans cette sixième itération, nous ajoutons de nouvelles fonctionnalités à notre application en écrivant d’abord des tests unitaires et en écrivant du code sur les tests unitaires. Dans cette itération, nous ajoutons des groupes de contacts.
Itération n°7 - Ajouter une fonctionnalité Ajax. Dans la septième itération, nous améliorons la réactivité et les performances de notre application en ajoutant la prise en charge d’Ajax.
Cette itération
Dans cette quatrième itération de l’application Gestionnaire de contacts, nous refactorisons l’application pour qu’elle soit plus faiblement couplée. Lorsqu’une application est faiblement couplée, vous pouvez modifier le code dans une partie de l’application sans avoir à modifier le code dans d’autres parties de l’application. Les applications faiblement couplées sont plus résilientes aux changements.
Actuellement, toutes les logiques d’accès et de validation des données utilisées par l’application Gestionnaire de contacts sont contenues dans les classes de contrôleur. C’est une mauvaise idée. Chaque fois que vous devez modifier une partie de votre application, vous risquez d’introduire des bogues dans une autre partie de votre application. Par exemple, si vous modifiez votre logique de validation, vous risquez d’introduire de nouveaux bogues dans votre logique d’accès aux données ou de contrôleur.
Notes
(SRP), une classe ne doit jamais avoir plus d’une raison de changer. La combinaison du contrôleur, de la validation et de la logique de base de données est une violation massive du principe de responsabilité unique.
Il existe plusieurs raisons pour lesquelles vous devrez peut-être modifier votre application. Vous devrez peut-être ajouter une nouvelle fonctionnalité à votre application, corriger un bogue dans votre application ou modifier la façon dont une fonctionnalité de votre application est implémentée. Les applications sont rarement statiques. Ils ont tendance à croître et à muter au fil du temps.
Imaginez, par exemple, que vous décidiez de modifier la façon dont vous implémentez votre couche d’accès aux données. À l’heure actuelle, l’application Gestionnaire de contacts utilise Microsoft Entity Framework pour accéder à la base de données. Toutefois, vous pouvez décider de migrer vers une technologie d’accès aux données nouvelle ou alternative, telle que ADO.NET Data Services ou NHibernate. Toutefois, étant donné que le code d’accès aux données n’est pas isolé du code de validation et du contrôleur, il n’existe aucun moyen de modifier le code d’accès aux données dans votre application sans modifier un autre code qui n’est pas directement lié à l’accès aux données.
Quand une application est faiblement couplée, en revanche, vous pouvez apporter des modifications à une partie d’une application sans toucher d’autres parties d’une application. Par exemple, vous pouvez changer de technologies d’accès aux données sans modifier votre logique de validation ou de contrôleur.
Dans cette itération, nous profitons de plusieurs modèles de conception logicielle qui nous permettent de refactoriser notre application Contact Manager en une application plus faiblement couplée. Lorsque nous avons terminé, le gestionnaire de contacts ne fera rien de ce qu’il n’a pas fait auparavant. Toutefois, nous pourrons modifier l’application plus facilement à l’avenir.
Notes
La refactorisation est le processus de réécriture d’une application de telle sorte qu’elle ne perde aucune fonctionnalité existante.
Utilisation du modèle de conception de logiciels de dépôt
Notre première modification consiste à tirer parti d’un modèle de conception logicielle appelé modèle de dépôt. Nous allons utiliser le modèle Référentiel pour isoler notre code d’accès aux données du reste de notre application.
L’implémentation du modèle référentiel nous oblige à effectuer les deux étapes suivantes :
- Créer une interface
- Créer une classe concrète qui implémente l’interface
Tout d’abord, nous devons créer une interface qui décrit toutes les méthodes d’accès aux données que nous devons effectuer. L’interface IContactManagerRepository est contenue dans la liste 1. Cette interface décrit cinq méthodes : CreateContact(), DeleteContact(), EditContact(), GetContact et ListContacts().
Listing 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
Ensuite, nous devons créer une classe concrète qui implémente l’interface IContactManagerRepository. Étant donné que nous utilisons Microsoft Entity Framework pour accéder à la base de données, nous allons créer une classe nommée EntityContactManagerRepository. Cette classe est contenue dans la liste 2.
Listing 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
Notez que la classe EntityContactManagerRepository implémente l’interface IContactManagerRepository. La classe implémente les cinq méthodes décrites par cette interface.
Vous vous demandez peut-être pourquoi nous devons nous embêter avec une interface. Pourquoi devons-nous créer à la fois une interface et une classe qui l’implémente ?
À une exception près, le reste de notre application interagit avec l’interface et non avec la classe concrète. Au lieu d’appeler les méthodes exposées par la classe EntityContactManagerRepository, nous allons appeler les méthodes exposées par l’interface IContactManagerRepository.
De cette façon, nous pouvons implémenter l’interface avec une nouvelle classe sans avoir à modifier le reste de notre application. Par exemple, à une date ultérieure, nous pourrions implémenter une classe DataServicesContactManagerRepository qui implémente l’interface IContactManagerRepository. La classe DataServicesContactManagerRepository peut utiliser ADO.NET Data Services pour accéder à une base de données au lieu de Microsoft Entity Framework.
Si notre code d’application est programmé par rapport à l’interface IContactManagerRepository au lieu de la classe EntityContactManagerRepository concrète, nous pouvons changer de classes concrètes sans modifier le reste de notre code. Par exemple, nous pouvons passer de la classe EntityContactManagerRepository à la classe DataServicesContactManagerRepository sans modifier notre logique d’accès aux données ou de validation.
La programmation sur des interfaces (abstractions) au lieu de classes concrètes rend notre application plus résiliente aux changements.
Notes
Vous pouvez créer rapidement une interface à partir d’une classe concrète dans Visual Studio en sélectionnant l’option de menu Refactoriser, Extraire l’interface. Par exemple, vous pouvez d’abord créer la classe EntityContactManagerRepository, puis utiliser Extract Interface pour générer automatiquement l’interface IContactManagerRepository.
Utilisation du modèle de conception du logiciel d’injection de dépendances
Maintenant que nous avons migré notre code d’accès aux données vers une classe référentiel distincte, nous devons modifier notre contrôleur de contacts pour utiliser cette classe. Nous allons tirer parti d’un modèle de conception logicielle appelé Injection de dépendances pour utiliser la classe Repository dans notre contrôleur.
Le contrôleur de contact modifié est contenu dans la liste 3.
Listing 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
Notez que le contrôleur de contact dans la liste 3 a deux constructeurs. Le premier constructeur transmet une instance concrète de l’interface IContactManagerRepository au second constructeur. La classe du contrôleur de contact utilise l’injection de dépendances de constructeur.
Le seul emplacement utilisé par la classe EntityContactManagerRepository se trouve dans le premier constructeur. Le reste de la classe utilise l’interface IContactManagerRepository au lieu de la classe EntityContactManagerRepository concrète.
Cela facilite le changement d’implémentations de la classe IContactManagerRepository à l’avenir. Si vous souhaitez utiliser la classe DataServicesContactRepository au lieu de la classe EntityContactManagerRepository, modifiez simplement le premier constructeur.
L’injection de dépendances du constructeur rend également la classe du contrôleur de contact très testable. Dans vos tests unitaires, vous pouvez instancier le contrôleur de contacts en passant une implémentation fictive de la classe IContactManagerRepository. Cette fonctionnalité de l’injection de dépendances sera très importante pour nous lors de la prochaine itération lorsque nous générerons des tests unitaires pour l’application Contact Manager.
Notes
Si vous souhaitez dissocier complètement la classe du contrôleur de contact d’une implémentation particulière de l’interface IContactManagerRepository, vous pouvez tirer parti d’une infrastructure qui prend en charge l’injection de dépendances, telle que StructureMap ou Microsoft Entity Framework (MEF). En tirant parti d’une infrastructure d’injection de dépendances, vous n’avez jamais besoin de faire référence à une classe concrète dans votre code.
Création d’une couche de service
Vous avez peut-être remarqué que notre logique de validation est toujours mélangée à notre logique de contrôleur dans la classe de contrôleur modifiée dans la liste 3. Pour la même raison qu’il est judicieux d’isoler notre logique d’accès aux données, il est judicieux d’isoler notre logique de validation.
Pour résoudre ce problème, nous pouvons créer une couche de service distincte. La couche de service est une couche distincte que nous pouvons insérer entre nos classes de contrôleur et de dépôt. La couche de service contient notre logique métier, y compris toute notre logique de validation.
ContactManagerService est contenu dans la liste 4. Il contient la logique de validation de la classe du contrôleur de contact.
Listing 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
Notez que le constructeur de ContactManagerService a besoin d’un ValidationDictionary. La couche de service communique avec la couche contrôleur par le biais de ce ValidationDictionary. Nous abordons le ValidationDictionary en détail dans la section suivante lorsque nous abordons le modèle Décorateur.
Notez, en outre, que ContactManagerService implémente l’interface IContactManagerService. Vous devez toujours vous efforcer de programmer sur des interfaces plutôt que sur des classes concrètes. Les autres classes de l’application Contact Manager n’interagissent pas directement avec la classe ContactManagerService. Au lieu de cela, à une exception près, le reste de l’application Contact Manager est programmé sur l’interface IContactManagerService.
L’interface IContactManagerService est contenue dans listing 5.
Listing 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
La classe de contrôleur contact modifiée est contenue dans la liste 6. Notez que le contrôleur de contacts n’interagit plus avec le référentiel ContactManager. Au lieu de cela, le contrôleur de contacts interagit avec le service ContactManager. Chaque couche est isolée autant que possible des autres couches.
Listing 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
Notre application ne respecte plus le principe de responsabilité unique (SRP). Le contrôleur de contacts de la liste 6 a été supprimé de toutes les responsabilités autres que le contrôle du flux d’exécution de l’application. Toute la logique de validation a été supprimée du contrôleur de contacts et poussée dans la couche de service. Toute la logique de base de données a été poussée dans la couche du référentiel.
Utilisation du modèle décorateur
Nous voulons être en mesure de dissocier complètement notre couche de service de notre couche de contrôleur. En principe, nous devons être en mesure de compiler notre couche de service dans un assembly distinct de notre couche de contrôleur sans avoir besoin d’ajouter une référence à notre application MVC.
Toutefois, notre couche de service doit être en mesure de transmettre les messages d’erreur de validation à la couche contrôleur. Comment pouvons-nous permettre à la couche de service de communiquer des messages d’erreur de validation sans associer le contrôleur et la couche de service ? Nous pouvons tirer parti d’un modèle de conception de logiciel nommé motif décorateur.
Un contrôleur utilise un ModelStateDictionary nommé ModelState pour représenter les erreurs de validation. Par conséquent, vous pouvez être tenté de passer ModelState de la couche contrôleur à la couche de service. Toutefois, l’utilisation de ModelState dans la couche de service rend votre couche de service dépendante d’une fonctionnalité de l’infrastructure MVC ASP.NET. Cela serait mauvais, car, un jour, vous voudrez peut-être utiliser la couche de service avec une application WPF au lieu d’une application MVC ASP.NET. Dans ce cas, vous ne souhaitez pas référencer l’infrastructure MVC ASP.NET pour utiliser la classe ModelStateDictionary.
Le modèle Décorateur vous permet d’encapsuler une classe existante dans une nouvelle classe afin d’implémenter une interface. Notre projet Gestionnaire de contacts inclut la classe ModelStateWrapper contenue dans listing 7. La classe ModelStateWrapper implémente l’interface dans listing 8.
Listing 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
Listing 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
Si vous examinez de près la description 5, vous verrez que la couche de service ContactManager utilise exclusivement l’interface IValidationDictionary. Le service ContactManager ne dépend pas de la classe ModelStateDictionary. Lorsque le contrôleur de contacts crée le service ContactManager, le contrôleur encapsule son ModelState comme suit :
_service = new ContactManagerService(New ModelStateWrapper(ModelState))
Résumé
Dans cette itération, nous n’avons ajouté aucune nouvelle fonctionnalité à l’application Gestionnaire de contacts. L’objectif de cette itération était de refactoriser l’application Contact Manager afin de faciliter la maintenance et la modification.
Tout d’abord, nous avons implémenté le modèle de conception du logiciel référentiel. Nous avons migré tout le code d’accès aux données vers une classe de référentiel ContactManager distincte.
Nous avons également isolé notre logique de validation de notre logique de contrôleur. Nous avons créé une couche de service distincte qui contient tout notre code de validation. La couche de contrôleur interagit avec la couche de service et la couche de service interagit avec la couche du référentiel.
Lorsque nous avons créé la couche de service, nous avons tiré parti du modèle Décorateur pour isoler ModelState de notre couche de service. Dans notre couche de service, nous avons programmé l’interface IValidationDictionary au lieu de ModelState.
Enfin, nous avons tiré parti d’un modèle de conception logicielle nommé modèle d’injection de dépendances. Ce modèle nous permet de programmer sur des interfaces (abstractions) au lieu de classes concrètes. L’implémentation du modèle de conception Injection de dépendances rend également notre code plus testable. Dans l’itération suivante, nous ajoutons des tests unitaires à notre projet.