En guise d’introduction…
Ca fait long temps que je n’ai rien écrit. J’ai un peu été pris par des préoccupations professionnelles donc j’ai préféré de faire une pause plutôt que d’essayer de maintenir artificiellement mon blog à jour en postant des liens vers d’autres blogs ou de vous annoncer les dernières news que tout le monde a lu la veille sur le sites web américains ;) C’est pas mon style :)
Concrètement…
En étudiant le code d’un projet en ASP.NET MVC 2 sur lequel je travaille, j’ai remarqué que dans chaque service de domaine, un service de cache est injecté par le constructeur. Pour que vous-vous rendiez compte de quoi je parle, regardez ce petit schéma simplifié (classique) qui résume l’interaction entre les différents tiers :
Rien d’extraordinaire. Dans la pratique le Repository est injecté par le constructeur dans le service de domaine qui lui, à son tour est injecté par le constructeur au contrôleur ASP.NET MVC. Tout ceci se fait automatiquement grâce au conteneur DI Unity.
Et le cache dans tout ça ?
Ensuite vient la gestion du cache. Il y a un service qui permet d’ajouter et de supprimer les données du cache. Puisqu’on utilise Unity pour résoudre les graphes d’objet au runtime, la solution la plus simple consiste à injecter le service de cache par le constructeur directement dans le service. Je vous montre un extrait de code d’un tel service utilisant le cache qui appartient à mon tiers de domaine métier:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class FooService : IFooService { private readonly IFooRepository _repository; private readonly ICacheService _cacheService;
public FooService(IFooRepository repository, ICacheService cacheService) { if (repository == null) throw new ArgumentNullException("repository", "Service needs a non null container in order to initalize.");
if (cacheService == null) throw new ArgumentNullException("cacheService", "Service needs a non null cache service in order to initalize.");
_repository = repository; _cacheService = cacheService; } ... |
Donc lorsqu’Unity résout IFooService pour le contrôleur ASP.NET MVC il injecte automatiquement IFooRepository et ICacheService qui est notre service de cache. Ensuite le service de cache est utilisé de la manière suivante (example simplifié d’une méthode) :
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public IEnumarable<string> GetFields(Type entityType) { IEnumarable<string> fields = _cacheService.Get(entityType.FullName);
if (fields == null) { fields = _repository.GetFields(entityType); _cahceService.Put(entityType.FullName, fields) }
return fields; } |
La méthode est volontairement très simple mais il y a quelque chose qui me dérange :
- le service intègre le code d’infrastructure concernant la gestion du cache. C’est contraire aux principes SRP et au OPC…
- normalement nous ne voulons pas gérer le cache de données au niveau du service mais au niveau du repository.
- le service injecté par le constructeur devrait servir dans toutes les méthodes de l’application.
La gestion du cache étant un Cross Cutting Concern, elle ne devrait pas être mélangée avec le code métier. Je vais toujours insister que pour modifier la manière dont le cache est géré on est obligé non seulement de modifier le code à tous les endroits ou le service de cache a été injecté mais également si on a envie de modifier le côté fonctionnel on est obligé de retoucher les lignes responsables pour gérer le cache. Pas top ! (On me dit toujours que j’exagère avec le SRP mais bon, c’est parce que ceux qui me le reproche n’ont pas envie d’écrire du code propre sous prétexte que le code perde de son pragmatisme :)…)
Configuration dans Unity…
Tout ceci est configuré dans mon bootstraper de la manière suivante :
1 2 3 |
container.RegisterType<ICacheService, CacheService>();
container.RegisterType<IFooRepository, FooRepository>(new InjectionConstructor("connexionstring")); |
Donc tout simplement on enregistre les mappings entre les abstractions et les classes concrètes. Lorsqu’on demandera le service FooService, Unity injectera la chaîne de connexion dans FooRepsoitory et passera l’instance au constructeur de FooService ainsi que l’instance de CacheService.
Refactoring…
Pour ma part j’ai envie de descendre la gestion du cache de données au niveau du Repository. La première tâche consiste à nettoyer le service de tout code qui contient la gestion du cache. Le constructeur est dorénavant épuré :
1 2 3 4 5 6 7 |
public FooService(IFooRepository repository) { if (repository == null) throw new ArgumentNullException("repository", "Service needs a non null container in order to initalize.");
_repository = repository; } |
Etant donné que le service de cache n’est plus injecté par le constructeur nous pouvons donc nettoyer toutes les méthodes de la gestion du cache. La méthode GetFields devient donc beacoup plus claire que précédemment :
1 2 3 4 |
public IEnumarable<string> GetFields(Type entityType) { return _repository.GetFields(entityType); } |
Imaginez le service avec quelques méthodes de plus. Juste, sur une seule simple méthode nous avons supprimé 7 ligne de code, alors que si vous avez 5 ou 6 méthodes de plus, le gain sera beaucoup plus visible.
Regardons de plus prêt le repository d’origine :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public partial class FooRepository: ObjectContext, IFooRepository { public FooRepository(string connectionString) : base(connectionString, "FooRepository") { this.ContextOptions.LazyLoadingEnabled = true; OnContextCreated(); } public virtual string[] GetFields(Type entityType) { return (from entityDescriptor in EntityDescriptors where entityDescriptor.EntityType == entityType.AssemblyQualifiedName && entityDescriptor.Enabled == true select entityDescriptor.PropertyFullPath).ToArray(); } } |
Comme vous pouvez le voir, le Repository est en fait une abstraction autour du containeur EF4. Pour simplifier l’exemple, je présente juste un constructeur, celui, qui est utilisé par Unity pour injecter la chaîne de connexion lors de la résolution du graphe d’objet.
Pour intégrer le cache, je n’ai pas du tout envie de modifier l’implémentation actuelle de mon Repository. Je vais donc utiliser la pattern Decorateur pour l’implémenter. Pour parler simplement, le décorateur que nous allons créer englobera l’implémentation actuelle sans la modifier. Voici son implémentation :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
public class CachingFooRepository : FooRepository { private readonly FooRepository _innerRepository; private readonly ICacheService _cacheService;
public CachingFooRepository(IFooRepository repository, ICacheService cacheService) { if (repository == null) { throw new ArgumentNullException("repository"); }
if (cacheService == null) { throw new ArgumentNullException("cacheService"); }
_innerRepository = repository; _cacheService = cacheService; }
public override IEnumarable<string> GetFields(Type entityType) { IEnumarable<string> fields = _cacheService.Get(entityType.FullName);
if (fields == null) { fields = _innerRepository.GetFields(entityType); _cahceService.Put(entityType.FullName, fields) }
return fields; } } |
Les étapes pour fabriquer le décorateur (un décorateur peut être fabriqué de différentes manières, donc ne prenez pas ce que je présente ici comme LA MANIERE de le faire) :
- J’ai dérivé mon CachingFooRepository du repository de base FooRepository. Une légère modification dans FooRepository consistait à passer les membres en virtual afin qu’on puisse les surcharger. Cela me permet de surcharger uniquement les méthodes qui m’intéressent pour intégrer le caching.
- Le constructeur comme vous pouvez le constater prend en paramètre IFooRepository qui est une implémentation par défaut et le service de cache ICacheService. Ils sont sauvegardés ensuite dans les champs privés.
- On surcharge la méthode GetFields pour intégrer le cache est on délègue le reste du travail au Repository interne _innerRepository.
Quels sont les avantages ?
L’avantage principal est que la logique de gestion du cache est déconnectée de la logique du Repository et surtout de la logique du service de domaine. Nous pouvons faire évoluer l’un indépendamment de l’autre. Ce qui est surtout intéressant ce que le service FooService prendre en paramètre une abstraction IFooRepository. Donc nous pouvons injecter soit l’implémentation par défaut FooRepository soit CachingFooRepository (car il dérive de ce dernier) et cela sans changer mon service.
L’enregistrement dans Unity des décorateurs.
La seule nuance consiste à enregistrer correctement tout cela dans Unity. Si on enregistre FooRepository et CachingFooRepository en tant que mapping pour l’abstraction IFooRepository, alors lors de la résolution du FooService, Unity ne saura pas qu’elle implémentation injecter de IFooRepository. Pour cela nous devons tout d’abord enregistrer l’implémentation par défaut du Reposiutory :
1 |
container.RegisterType<FooRepository>(new InjectionConstructor("connexionstring")); |
Remarquez qu’on ne mappe plus FooRepository vers l’abstraction IFooRepository. On enregistre juste le type et la manière dont la chaîne de connexion doit être injectée. Ensuite nous devons enregistrer le décorateur :
1 |
container.RegisterType<IFooRepository, CachingFooRepository>(new InjectionConstructor(new ResolvedParameter<FooRepository>(), new ResolvedParameter<ICacheService>())); |
Cette fois ci, nous mappons notre décorateur vers l’abstraction IFooRepository. On indique également à Unity, comment le constructeur doit être résolu, en passant l’instance précédemment enregistrée de FooRepository et de notre service de cache. De cette manière le service FooService sera correctement résolu.
Conclusion
Comme vous pouvez le voir l’utilisation des décorateurs est très utile lors de l’implémentation de Cross Cutting Concerns comme le cache, le logging ou la sécurité. Vous respecterez de cette manière le principe SRP et OCP. Il y a cependant quelques désavantages dont je n’ai pas parlé comme par exemple l’augmentation du nombre de classes de décorateurs et la répétition du code qui gère le cache. Cette fois-ci le principe DRY (don’t repeat yourself) risque d’en pâtir. Cela n’est pas très grave si la gestion du cache doit être implémenté dans quelques repositories ou services. Dans d’autres cas il y a une bien meilleure technique dont je vais parler dans mon prochain billet (si j’ai le temps) qui est l’Interception. Un autre mangnifique aspect de la DI !
A bientôt !