Cet article fera figure d’introduction à la programmation orientée aspect et sera, je l’espère, le premier d’une longue série, étant donné les très nombreuses possibilités offertes par cette technique. Ici, nous décrirons brièvement en quoi consiste la programmation orientée aspect, puis nous étudierons par des exemples concrets (loin d’être exhaustifs !) son fonctionnement en C# via la librairie PostSharp.
1. Qu’est–ce que l’AOP (Aspect Oriented Programming)
Au quotidien, le développeur doit se préoccuper non seulement du code métier de la ou les applications qu’il a à sa charge, mais aussi de ce que l’on appelle les préoccupations transverses (traduction assez approximative de l’anglais cross-cutting concerns).
Sous cette appellation quelque peu pompeuse se cache tout le code qui n’est pas clairement décomposé du reste du système, et qui au contraire affecte celui-ci de manière global.
Voici une liste non exhaustive des préoccupations transverses que l’on peut retrouver dans la plupart des applications :
- Journalisation
- Gestion des autorisations
- Gestion des exceptions
- Monitoring/Benchmark
- Tracing/Debug
- Caching
- Gestion des ressources
- Validation des inputs/outputs
- Etc…
Ces préoccupations transverses sont indispensables à l’application. Et pourtant, elles soulèvent un grave problème en matière de design. En effet, du fait que celles-ci sont éparpillées partout dans le code de l’application, elles ont pour effet de tendre à nettement alourdir le code, jusqu’à le rendre difficile à lire et donc à maintenir. De plus, ces préoccupations transverses sont les premières fautives en ce qui concerne la violation du principe de responsabilité unique (le S de SOLID principles). Sans parler de la duplication de code que cela engendre…
Et c’est précisément là qu’intervient la programmation par aspect (AOP), en proposant une solution permettant de réduire le couplage fort induit par les préoccupations transverses et recentrant la gestion de celles-ci dans des classes particulières (appelles advices) appelables via un mécanisme d’annotation sur la zone de code que l’on souhaite affecter (appelée pointcut). C’est ce lien entre advice et pointcut qui constitue ce que l’on appelle l’aspect.
Ça suffit pour la théorie, passons maintenant à la pratique !
2. Mise en pratique avec Post Sharp
Post Sharp est une librairie .NET qui s’intègre à Visual Studio .NET et est l’une des implémentations de l’AOP les plus utilisées.
2.1. Installation
Vous pouvez vous procurer la dernière version (4.0.41) sur ce lien : https://www.postsharp.net/download ou via le gestionnaire de package NuGet manager via Visual Studio.
Notez que si la version professionnelle de PostSharp est payante, il existe une version gratuite qui reprend les fonctionnalités les plus importantes de la librairie.
2.2. Cas pratiques
Voici une série d’exemples concrets directement inspirés de ce que j’ai eu l’occasion de mettre en production dans le cadre des différents projets sur lesquels j’ai travaillé. Chaque exemple sera suivi d’un bref commentaire expliquant les spécificités de l’implémentation ainsi que le but du code.
2.2.1. Mesurer le temps d’exécution d’une méthode
namespace AutomatonsMonitoring.Aspects { [Serializable] public class TimerMonitorAttribute: PostSharp.Aspects.OnMethodBoundaryAspect { [NonSerialized] static private Stopwatch _sW = new Stopwatch(); public override void OnEntry(PostSharp.Aspects.MethodExecutionArgs args) { _sW.Start(); base.OnEntry(args); } public override void OnExit(PostSharp.Aspects.MethodExecutionArgs args) { base.OnExit(args); _sW.Stop(); Trace.TraceInformation(String.Format( "It took {0} milliseconds to execute {1}.", _sW.ElapsedMilliseconds, args.Method.Name)); } } } [TimerMonitor] public void Start(IAutomaton autom) { // random stuff... }
Ce premier exemple très simple permettant de mesurer la durée d’exécution d’une méthode va nous permettre de voir plus en détails les principes généraux d’un aspect PostSharp.
Tout d’abord comme vous pouvez le voir, la déclaration de la méthode est précédée de l’attribut Serializable. Celui-ci est indispensable et est lié aux détails d’implémentation de PostSharp (ce point mériterait un article a part entière…). A noter l’ajout de l’attribut NonSerialized sur notre objet StopWatch pour signifier au serializer de l’ignorer. Ensuite, nous déclarons notre classe avec comme nom TimerMonitorAttribute qui nous permettra de signaler les méthodes auxquelles on souhaite appliquer cet aspect. Notez que notre classe dérive d’une class PostSharp OnMethodBoundaryAspect dont nous surchargeons les méthodes qui seront appelées à l’entrée (OnEntry) et à la sortie (OnExit) de la méthode Start dans notre exemple.
Avouez que c’est un moyen très simple et très peu intrusif pour monitorer son code…
Dans cet exemple, la portée de l’aspect est sur la méthode, mais il est tout a fait possible de signaler un aspect au niveau de la classe (en précédant celle-ci de l’attribut TimerMonitor, voire même au niveau de l’assembly tout entière.
2.2.2. Dumper les paramètres et valeurs de retour
[Serializable] class DumpObjectAttribute : PostSharp.Aspects.OnMethodBoundaryAspect { public override void OnEntry(PostSharp.Aspects.MethodExecutionArgs args) { Trace.WriteLine(String.Format("{0}----INPUT PARAMETERS [{1}]----", args.Method.Name, args.Arguments.Count)); foreach (var param in args.Arguments) { TraceObject(param); } base.OnEntry(args); } public override void OnExit(PostSharp.Aspects.MethodExecutionArgs args) { Trace.WriteLine(String.Format("{0}----RETURN VALUE [{1}]----", args.Method.Name, args.ReturnValue != null ? 1:0)); TraceObject(args.ReturnValue); base.OnExit(args); } private void TraceObject(object o) { if (o == null) { return ; } using (var sw = new StringWriter()) { ObjectDumper.Dumper.Dump(o, o.GetType().Name, sw); Trace.WriteLine(sw.ToString()); } } }
Cet exemple est un moyen efficace de dumper non seulement le contenu des objets passés en paramètre de la méthode signalée par l’attribut DumpObject mais aussi l’éventuelle valeur de retour. Ce petit bout de code m’a souvent été d’une aide très précieuse pour le débogage. A noter que l’utilisation de la classe ObjectDumper nous permet de sérialiser un objet sans qu’il soit nécessairement Serializable.
Voici un exemple de dump généré par cet aspect :
GetClient—-INPUT PARAMETERS [1]—- UInt32 = 0 [System.UInt32] GetClient—-RETURN VALUE [1]—- #1: Client [AOP_Acensi.Models.Client] { properties { ClientID = 0 [System.UInt32] #2: Name = « Zappa » [System.String] #3: FirstName = « Frank » [System.String] Balance = 100000 [System.Double] Orders = <null> } fields { <ClientID>k__BackingField = 0 [System.UInt32] <Name>k__BackingField = « Zappa » [System.String] (see #2) <FirstName>k__BackingField = « Frank » [System.String] (see #3) <Balance>k__BackingField = 100000 [System.Double] <Orders>k__BackingField = <null> } }
Sur la classe suivante :
[DumpObject] public Client GetClient(uint clientId) { return _clientDB.SingleOrDefault(x => x.ClientID == clientId); }
2.2.3. Gestion des autorisations
[Serializable] public class AuthenticationRequiredAttribute : PostSharp.Aspects.OnMethodBoundaryAspect { public override void OnEntry(PostSharp.Aspects.MethodExecutionArgs args) { if (AccessManager.IsAuthenticated()) { base.OnEntry(args); } else { throw new UnauthorizedAccessException(); } } }
Cet exemple permet d’implémenter un mécanisme permettant de vérifier qu’un utilisateur est habilité à exécuter une action. Si celui-ci ne l’est pas le comportement est changé et une exception est levée. Cela permet d’illustrer simplement que la gestion des erreurs peut être déportée dans l’aspect permettant à la méthode signalant l’aspect de se concentrer uniquement sur le code métier.
[Serializable] public class AuthenticationRequiredAttribute : PostSharp.Aspects.OnMethodBoundaryAspect { public override void OnEntry(PostSharp.Aspects.MethodExecutionArgs args) { if (AccessManager.IsAuthenticated()) { base.OnEntry(args); } else { Trace.WriteLine("Unauthorized access."); args.FlowBehavior = PostSharp.Aspects.FlowBehavior.Return; MethodInfo mI = args.Method as MethodInfo; args.ReturnValue = Activator.CreateInstance(mI.ReturnType); } } }
Cet exemple a le même rôle que le précèdent mais permet d’illustrer le fait que l’aspect peut modifier le flot d’exécution de la méthode signalée. Ainsi, si l’utilisateur n’est pas identifié, la méthode se finira prématurément en retournant une valeur par défaut correspondant au type de la valeur de retour de la méthode.
2.2.4. Gestion d’erreurs
[Serializable] class TransactionManagementAttribute : PostSharp.Aspects.MethodInterceptionAspect { public uint MaxRetries { get; set; } public int Delay { get; set; } public TransactionManagementAttribute(uint retries = 5, int delay = 500) { MaxRetries = retries; Delay = delay; } public override void OnInvoke(PostSharp.Aspects.MethodInterceptionArgs args) { int i = 0; while (i < MaxRetries) { try { Console.WriteLine(String.Format("{0} attempts to connect.", i)); args.Proceed(); break; } catch (SystemBusyDatabaseException e) { if (i < MaxRetries) { //Thread.Sleep(Delay); i++; } else { throw; } } catch (Exception e) { throw; } } } }
Ici encore une implémentation d’une gestion d’erreurs sur une dépendance peut être sujette à des problèmes de ressources. Contrairement au précèdent exemple, notre aspect ne dérive pas de OnMethodBoundaryAspect mais de MethodInterceptionAspect, qui nous permet ici d’avoir un contrôle sur l’exécution de la méthode elle-même. Ainsi, si l’appel de notre méthode signalée renvoie une exception de type SystemBusyDatabaseException, OnInvoke attrape l’exception et décide d’exécuter à nouveau la méthode si le nombre d’essais maximal n’est pas dépassé. A noter aussi que nous ajoutons ici notre constructeur afin de nous permettre d’ajouter le maximum de tentatives (MaxRetries) ainsi que le délai entre chaque tentative (Delay) dans la déclaration de notre attribut.
Voici le code signalant cet aspect ainsi que ce qu’il génère sur la sortie standard.
[TransactionManagement(4, 500)] public bool SaveClient(Client c) { throw new SystemBusyDatabaseException(); throw new SystemBusyDatabaseException(); throw new SystemBusyDatabaseException(); return true; }
0 attempts to connect.
1 attempts to connect.
2 attempts to connect.
3 attempts to connect.
Conclusion
Au cours de cet article, nous avons pu aborder la puissance que permet la programmation orientée aspect a travers plusieurs exemples illustrant certaines préoccupations transverses qui font le quotidien du développeur. J’espère sincèrement que ce modeste article aura pu participer a vous convaincre de la réelle solution que représente la programmation orientée aspect pour en finir avec le caractère intrusif des préoccupations transverses dans le code.