déc. 05 2010

[ASP.NET] Design Patterns et Best Pratices - Partie 4 : Cas pratique et Refactoring

Category: ASP.NET | Design PatternsNicolas Esprit @ 17:45

Au programme de ce quatrième billet de la série sur les Design Patterns et les Best Pratices appliqueés à l'ASP.NET, nous allons nous concentrer sur la pratique avec un exemple de refactoring. Vous pouvez consulter les précédents billets :

C'est bien beau de décrire les différents principes de conception et les Design Patterns, mais il est important de les voir en action. Ainsi nous allons voir dans ce billet comment un simple morceau de code ASP.NET que vous avez probablement vu d'innombrables fois avant peut être amélioré en appliquant les principes SOLID et quelques Patterns.

Vu que Window Phone 7 est d'actualité (j'ai acheté le mien cette semaine), je vais baser mon exemple sur le Marketplace. Supposons que le code qui récupère toutes les applications dans une catégorie donnée (Applications, Jeux, Socialisation, etc.) ressemble au diagramme ci-dessous. On peut y voir une classe nommée ApplicationService avec pour seule méthode GetAllApplicationsIn, une classe Application qui représente les applications disponibles sur le Marketplace, et une classe ApplicationRepository qui est utilisée pour récupérer des applications à partir d'une base de données :

Le travail de la classe ApplicationService est de coordonner la récupération d'une liste d'applications WP7 à partir du référentiel pour une catégorie donnée et de stocker les résultats dans le cache de sorte que le prochain appel puisse être exécuté plus rapidement. Avant d'aller plus loin, regardons rapidement le code de ces différentes classes (assez simple pour faciliter la compréhension et se focaliser sur l'essentiel). La classe Application est une simple classe POCO avec un attribut Name donc je vous passe la lecture du code. La classe ApplicationRepository :

public class ApplicationRepository
{
public IList<Application> GetAllApplicationsIn(int categoryId)
{
IList<Application> apps = new List<Application>();
// On récupère en base les applications
return apps;
}
}

Enfin, la classe ApplicationService qui est la plus importante :

public class ApplicationService
{
private ApplicationRepository _appRepository;

public ApplicationService()
{
_appRepository = new ApplicationRepository();
}

public IList<Application> GetAllApplicationsIn(int categoryId)
{
IList<Application> apps;
string storageKey = string.Format("apps_in_category_id_{0}", categoryId);

apps = (List<Application>)HttpContext.Current.Cache.Get(storageKey);
if (apps == null)
{
apps = _appRepository.GetAllApplicationsIn(categoryId);
HttpContext.Current.Cache.Insert(storageKey, apps);
}

return apps;
}
}


Les classes Application et ApplicationRepository ne nécessitent pas d'explication particulière. La classe ProductService possède une seule méthode qui est simple et coordonne la récupération des applications WP7 à partir du cache. Bien entendu, dans le cas où le cache est vide, elle s'occupe de la récupération des applications à partir du Repository et s'occupe de l'insérer dans le cache. Maintenant, que vous avez bien pris connaissance du rôle et du code de chaque classe, la question à se poser est : "qu'est ce qui ne va pas dans cet exemple ?"

  • La classe ApplicationService dépend de la classe ApplicationRepository. Si cette dernière change son API, il faudra modifier la classe ApplicationService pour prendre en compte ces changements.
  • Le code n'est pas testable. Sans classe ApplicationRepository avec un rattachement à une base de données réelle, il est impossible de tester la méthode de récupération des applications dans la classe ApplicationService. Il existe donc un étroit couplage entre ces deux classes.
  • Un autre problème lié aux tests unitaires est la dépendance du contexte HTTP pour la mise en cache de la liste des applications. Il est difficile de tester le code qui est étroitement associé au contexte HTTP.
  • La classe ApplicationService fige le mode de gestion de la mise ne cache, c'est à dire que seule l'utilisation du contexte HTTP est possible. Ainsi avec le code actuel, l'utilisation d'un moteur de cache différent comme Velocity ou Memcached exigera la modification de la classe ApplicationService et toute autre classe qui utilise la mise en cache. Velocity et Memcached sont deux systèmes de mise en cache qui peuvent être utilisés à la place du mécanisme fourni par défaut par ASP.NET

Maintenant que nous avons identifié les problèmes du code de ces différentes classes, il faut les corriger.

Refactoring

Tout d'abord, considérons le problème de dépendance de la classe ApplicationService avec la classe ApplicationRepository. Dans son état actuel, la classe ApplicationService est fragile, si l'API de la classe ApplicationRepository change, la classe ApplicationService aura peut-être besoin d'être modifiée à son tour. Cela rompt les deux principes que nous avons vu dans le deuxième billet : le principe Separation of Concerns (Soc) et le principe SOLID Single Responsability (SRP).

Maintenant, rappelez-vous également le principe SOLID Dependency Inversion (DIP). Nous avons vu dans le second billet que le principe DIP consiste à isoler vos classes d'implémentations concrètes et de les faire dépendre uniquement de classes abstraites ou interfaces. Nous pouvons utiliser ce principe pour découpler la classe ApplicationService de la classe ApplicationRepository. Nous allons dans un premier temps extraire une interface de la classe ApplicationRepository. Au passage c'est l'occasion de rappeler que Visual Studio fournit des outils bien pratiques pour cela. Vous pouvez rapidement extraire l'interface en utilisant le raccourci Ctrl+ R, Ctrl + I, ou en faisant un clic droit puis Refactor puis Extract Interface. N'oublions pas de cocher la méthode GetAllApplicationsIn afin que celle-ci soit inclue dans l'interface. Nous obtenons ainsi l'interface IApplicationRepository suivante :

interface IApplicationRepository
{
IList<Application> GetAllApplicationsIn(int categoryId);
}

Maintenant, nous pouvons modifier la classe ApplicationRepository pour que celle-ci implémente l'interface nouvellement créée :

public class ApplicationRepository : IApplicationRepository
{
public IList<Application> GetAllApplicationsIn(int categoryId)
{
IList<Application> apps = new List<Application>();
// On récupère en base les applications
return apps;
}
}

En fait je raconte n'importe quoi, Visual Studio l'a déjà fait pour nous ! Retenez bien ce raccourci clavier : Ctrl + R, Ctrl + I :-). La dernière chose à faire est de modifier la classe ApplicationService afin que celle-ci référencie une interface plutôt qu'une implémentation concrète :

public class ApplicationService
{
private IApplicationRepository _appRepository;

public ApplicationService()
{
_appRepository = new ApplicationRepository();
}

public IList<Application> GetAllApplicationsIn(int categoryId)
{ ...}
}

Qu'avons-nous réalisé par l'introduction d'une nouvelle interface ? La classe ApplicationService ne dépend plus que d'une abstraction plutôt qu'une implémentation concrète, ce qui signifie que la classe ApplicationService est complètement ignorante de toute implémentation. On s'assure ainsi d'avoir un code plus résistant aux changements. Cependant, il reste un léger problème : la classe ApplicationService est toujours responsable de la création de l'implémentation de l'interface IApplicationRepository. Actuellement il est impossible de tester le code sans classe ApplicationRepository valide.

Injection de dépendance

ApplicationService est encore couplée à l'implémentation concrète de ApplicationRepository car c'est actuellement son travail de créer l'instance (comme on peut le voir dans le constructeur de la classe ApplicationService). L'injection de dépendance peut déplacer cette responsabilité de créer la mise en œuvre d'ApplicationRepository à l'extérieur de la classe ApplicationService et avoir celle-ci injectée par le constructeur de la classe, comme on peut le voir dans le code suivant:

public class ApplicationService
{
private IApplicationRepository _appRepository;

public ApplicationService(IApplicationRepository appRepository)
{
_appRepository = appRepository;
}

public IList<Application> GetAllApplicationsIn(int categoryId)
{...}
}

En supprimant la responsabilité de l'implémentation de ApplicationRepository de la classe ApplicationService, on s'arrange ainsi à ce que la classe ApplicationService respecte le principe de responsabilité unique. Cette classe n'a donc plus qu'un seul rôle : la coordination de la récupération des données du cache (ou du Repository si le cache est vide) et non pas d'implémenter IApplicationRepository. L'injection de dépendance peut prendre trois formes :

  • l'injection à l'instanciation (via le constructeur)
  • l'injection par méthode
  • l'injection via une property (on utilise le setter)

Dans l'exemple précédent, nous venons d'utiliser l'injection à l'instanciation. Je reviendrais surement plus en détails sur ces différentes formes dans les prochains billets. En tout cas il est important de garder à l'esprit que le principe de l'injection de dépendance est justement de découpler les liens de dépendances entre objets. En POO, les objets de type A dépendent d'un objet de type B si au moins une des conditions suivantes est vérifiée :

  • A possède un attribut de type B (dépendance par composition)
  • A est de type B (dépendance par héritage)
  • A dépend d'un autre objet de type C qui dépend d'un objet de type B (dépendance par transitivité)
  • une méthode de A appelle une méthode de B

N'oubliez pas que si A dépend de B, cela implique que pour créer A, on a besoin de B ce qui, en pratique, n'est pas toujours le cas.

Revenons maintenant à notre code dont nous n'avons pas corrigé tous les problèmes. Il reste à régler la dépendance sur le contexte HTTP pour la mise en cache. Pour cela, nous allons utiliser un Design Pattern simple du GoF présenté dans le billet précédent.

Le Pattern Adapter

Pour régler le problème de dépendance, l'idéal aurait été de créer une interface comme nous l'avons fait avec la classe ApplicationRepository. Seulement, nous ne sommes pas à même de le faire avec la classe HTTPContext vu qu'elle fait partie intégrante du Framework .NET. Heureusement, ce type de problème a été résolu de nombreuses fois avant, et il existe déjà un Design Pattern prenant en compte ce problème à résoudre. Il s'agit du Pattern Adapter qui, pour rappel, fait partie du groupe des Patterns structuraux qui visent à structurer la façon dont les objets sont assemblés.

Ce Pattern permet à des classes d'interface incompatibles à être utilisées ensembles. Ainsi nous pouvons utiliser celui-ci pour ne plus dépendre de la classe HttpContext mais d'une interface compatible avec HttpContext. Pour illustrer cela nous allons créer une nouvelle interface nommée ICacheStorage.

public interface ICacheStorage
{
void Remove(string key);

void Store(string key, object data);

T Retrieve<T>(string key);
}

Maintenant que nous avons cette interface, nous pouvons mettre à jour la classe ApplicationService pour l'utiliser en remplacement de la classe HttpContext :

public class ApplicationService
{
private IApplicationRepository _appRepository;
private ICacheStorage _cacheStorage;

public ApplicationService(IApplicationRepository appRepository, ICacheStorage cacheStorage)
{
_appRepository = appRepository;
_cacheStorage = cacheStorage;
}

public IList<Application> GetAllApplicationsIn(int categoryId)
{
IList<Application> apps;
string storageKey = string.Format("apps_in_category_id_{0}", categoryId);

apps = _cacheStorage.Retrieve<List<Application>>(storageKey);
if (apps == null)
{
apps = _appRepository.GetAllApplicationsIn(categoryId);
_cacheStorage.Store(storageKey, apps);
}

return apps;
}
}

Le problème est maintenant que la classe HttpContext ne peut implémenter la nouvelle interface ICacheStorage (à moins de vouloir réécrire le Framework). Comment le Pattern Adapter peut résoudre notre problème ? Regardons plus en détails comment il fonctionne via cette illustration (source Objects by Design) ainsi que le schéma UML correspondant :

Comme vous pouvez le constater, un client a une référence sur une abstraction : la cible (ou Target sur le schéma). Dans notre cas il s'agit de l'interface ICacheStorage, le client étant bien entendu ApplicationService. L'adaptateur (ou Adapter sur le schéma) est une implémentation de l'interface, donc de ICacheStorage. On peut également voir que l'adaptateur encapsule simplement une instance de l'objet adapté et lui délègue le boulot en mettant en œuvre le contrat de l'interface cible.

Pour résumer note implémentation du Pattern Adapter nous aurons :

  • Un client qui utilise l'interface ICacheStorage : la classe ApplicationService
  • Une cible : l'interface ICacheStorage
  • Un adaptateur : une nouvelle classe nommée HttpContextCacheAdapter qui implémentera l'interface ICacheStorage
  • Enfin l'adapté : la classe HttpContext de base. L'adaptateur lui délèguera le boulot.

Pour implémenter le Pattern Adapter, il ne nous reste plus qu'à coder la nouvelle classe HttpContextCacheAdapter :

public class HttpContextCacheAdapter : ICacheStorage
{
public void Remove(string key)
{
HttpContext.Current.Cache.Remove(key);
}

public void Store(string key, object data)
{
HttpContext.Current.Cache.Insert(key, data);
}

public T Retrieve<T>(string key)
{
T itemStored = (T)HttpContext.Current.Cache.Get(key);

if (itemStored == null)
itemStored = default(T);

return itemStored;
}
}

Et le tour est joué : il est maintenant facile de mettre en œuvre une solution de mise en cache différente sans affecter le code existant. Par exemple, si nous souhaitons utiliser Velocity, tout ce que nous devons faire est de créer un adaptateur implémentant ICacheStorage, cela resta transparent pour la classe client ApplicationService vu qu'elle ne connaît que la cible à savoir l'interface ICacheStorage. Cet adaptateur pour Velocity pourrait être nommé VelocityAdaptater : n'oubliez pas qu'il faut utiliser le vocabulaire commun des Patterns afin de faciliter votre relecture du code mais aussi la compréhension par les autres développeurs qui participent (ou participeront) au projet.

Pour plus de détails sur le Pattern Adapter, je vous invite à consulter cet article de Florian Casabianca. Il est à noter qu'Adaptater n'est pas le seul Pattern que nous aurions pu utiliser pour gérer la mise en cache, je vous invite également à regarder le Pattern Proxy.

Avec notre version actuelle du code, nous avons résolu pas mal de problèmes et surtout retiré les dépendances entre les différentes classes. Cependant, il reste un petit problème que nous pourrions résoudre. En effet, que ce passe-t-il si pour une raison ou une autre nous souhaitons utiliser la classe ApplicationService sans vouloir utiliser la mise en cache ? Avec le code actuel il est nécessaire de fournir au constructeur de la classe ApplicationService un objet implémentant l'interface ICacheStorage. Une option consiste à fournir une référence null, mais cela nous obligerait à gérer le cas ou l'objet est null ou non null. Bref ce n'est pas la meilleure des façons de procéder.

Le Pattern Null Object

Le Pattern Null Object est un autre modèle d'une simplicité trompeuse. Ce dernier est très utile lorsque vous ne voulez pas préciser ou ne pouvez pas spécifier une instance valide d'une classe, et que vous n'avez pas vraiment envie de passer une référence null. Le Pattern Null Object propose de remplacer la référence null par un objet implémentant la bonne interface mais sans avoir de comportement. L'exemple ci-dessous pour l'interface ICacheStorage utilisée dans la classe ApplicationService sera plus parlant :

public class NullObjectCache : ICacheStorage
{
public void Remove(string key)
{ //Ne fait rien }
public void Store(string key, object data)
{ //Ne fait rien}

public T Retrieve<T>(string storageKey)
{
return default(T);
}
}

Maintenant, lorsque nous ne voudrons pas utiliser la mise en cache, nous pourrons transmettre un objet NullObjectCache à la classe ApplicationService.


C'est tout pour aujourd'hui. Nous avons eu l'occasion de découvrir plus en détails deux Design Patterns : Adapter et Null Object. Nous avons également vu comment identifier les problèmes d'un code source et comment, grâce à un simple refactoring et l'utilisation de l'injection de dépendance, nous pouvons les résoudre en respectant ainsi les principes de conceptions.

Tags: , , ,

Commentaires

1.
pingback topsy.com says:

Pingback from topsy.com

Twitter Trackbacks for
        
        [ASP.NET] Design Patterns et Best Pratices - Partie 4 : Cas pratique et Refactoring
        [nicolasesprit.com]
        on Topsy.com

2.
Maxence Maxence Luxembourg says:

Bravo pour cet article très utile.
Tu as choisi un cas simple, clair, très concret et répandu. On voit clairement le bien fondé des principes SOLID et des design patterns.

On a hate de lire d'autres cas d'application!

3.
Nicolas Nicolas France says:

Merci, ça fait plaisir de voir que l'article est utile Smile

D'autres cas pratiques vont venir concernant d'autres Patterns, mais également dans le contexte du développement en couche des applications ASP.NET.

4.
Julien Julien France says:

Très bon article. Bien écris, clair et très utile.
J'attends les suivants Smile

5.
Jean-Michel Jean-Michel France says:

Vite la suite!

6.
Nicolas Nicolas France says:

Ça vient ça vient Smile

Disons que là je suis entrain de rédiger une série d'articles avec Philippe Vialatte sur les nouveautés d'ASP.NET MVC 3, le moteur Razor, NuGet, WebMatrix, etc...

Du coup je n'ai pas le temps pour la suite de cette série en ce moment. Mais pas d'inquiétude je compte bien la continuer Wink

Et ton article sur les Design Patterns ça avance ?

7.
Jean-Michel Jean-Michel France says:

J'ai pas du tout avancé. Frown

Il faudrait que je m'y remette sérieusement. Je pense que le mois prochain, j'aurai bouclé le premier article et entamé le second.

Et toi? Tu as pu avancer un peu sur l'article ?

8.
Nicolas Nicolas France says:

Et oui c'est du boulot mine de rien. J'avance bien sur MVC 3 et j'en ai fini un sur IIS Express pour le magazine Programmez (je ne sais pas s'il sera publié).

Bref, je bosse sur plusieurs trucs à la fois. La motivation est là, c'est juste le temps qui manque Smile

Les commentaires sont clos