Patterns & Practices – Unity 2.0 Part 2
Par Franck Lizzi-Chardon, posté le 21/06/2011
Profil : Développeur | Niveau : Intermédiaire (200)
L'AOP (aspect oriented programing) est un concept qui a pour but d'isoler les parties de codes non liés au « business » de chaque application et qui se retrouvent éparpillées dans les différents endroits d'un programme. En effet, tous les paradigmes de programmation objet fournissent assez d'outils pour encapsuler la logique fonctionnelle (Interfaces, classe abstraites, méthodes, classes, ..). Il subsiste néanmoins des parties de codes qui vont être redondantes et qui n'ont strictement aucun rapport avec le métier de l'application. On pourra citer par exemple les logs, la gestion des exceptions, le monitoring des performances, .
Ces morceaux de code sont appelé « crosscutting concern » car on les retrouve un peu partout dans un programme. Ils interviennent de façon transversale sans tenir compte du niveau d'abstraction.
Comment ça marche ?
Chaque routine de code transversale va être formalisée sous la forme d'un aspect. Ensuite, un tisseur va être chargé d'injecter ces aspects à différents endroits, ces derniers étant précisés en général de façon déclarative (fichier de configuration, attributs, .)
Il existe deux types de tisseurs :
La plateforme .Net ne gère malheureusement pas nativement le tissage statique. Il va donc falloir se tourner vers une solution spécifique comme l'excellent Postsharp (http://www.sharpcrafters.com/)
Coté dynamique, il existe de nombreux Framework capable de faire de l'aop (Castle Windsor, Spring.NET, Ninject, .) mais aujourd'hui, comme je sujet de l'article le précise, nous allons nous intéresser à Unity.
Le modèle Objet
Afin de mettre en place le mécanisme d'interception dans Unity, nous allons commencer par créer un modèle objet. Nous allons créer une interface générique « IStoreData » qui expose les méthodes « AddToStore » et « ExistInStore ».
Nous allons également ajouter deux implémentations « InsertOptimizedStore » et «ReadOptimizedStore » qui implémenteront l'interface avec respectivement une Liste et un SortedSet comme stockage interne.
IstoreData :
1: public interface IStoreData
2: {
3: void AddToStore(T value);
4: bool ExistInStore(T value);
5: }
InsertOptimizedStore :
1: class InsertOptimizedStore : IStoreData where T : IComparable
2: {
3: private List _store = new List();
4:
5: public void AddToStore(T value)
6: {
7: _store.Add(value);
8: }
9:
10: public bool ExistInStore(T value)
11: {
12: return _store.Exists(x => x.CompareTo(value) == 0);
13: }
14: }
ReadOptimizedStore:
1: class ReadOptimizedStore : IStoreData
2: {
3: SortedSet _Store = new SortedSet();
4:
5: public void AddToStore(T value)
6: {
7: _Store.Add(value);
8: }
9:
10: public bool ExistInStore(T value)
11: {
12: return _Store.Contains(value);
13: }
14: }
Afin de pouvoir résoudre les types concrets lors de l'exécution, nous allons également configurer le fichier de configuration pour préciser le mapping dans la section Unity.
App.config :
1: <xml version="1.0"?>
2: <configuration>
3:
4: <configSections>
5: <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration, Version=2.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
6: <configSections>
7: <unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
8:
9: <assembly name="ConsoleApplication2"/>
10:
11: <namespace name="ConsoleApplication2"/>
12:
13: <sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension, Microsoft.Practices.Unity.Interception.Configuration" />
14:
15: <alias alias="IStoreData" type="ConsoleApplication2.IStoreData`1, ConsoleApplication2" />
16: <alias alias="InsertOptimizedStore" type="ConsoleApplication2.InsertOptimizedStore`1, ConsoleApplication2" />
17: <alias alias="ReadOptimizedStore" type="ConsoleApplication2.ReadOptimizedStore`1, ConsoleApplication2" />
18:
19: <container name="Inteception">
20:
21: <register type="IStoreData" mapTo="ReadOptimizedStore">
22: </register>
23:
24: </container>
25:
26: </unity>
27:
28: </configuration>
Nous venons de configurer par déclaration le conteneur Unity. Maintenant, lors de la résolution de l'interface « IstoreData », le Framework nous retournera une nouvelle instance de la classe « ReadOptimizedStore ». Il est a noté l'enregistrement des alias, En effet « IStoreData`1 » permet de spécifier que l'interface est générique et prend un type en paramètre.
Voici pour rappel comment configurer le conteneur Unity et invoquer la résolution :
1: UnityContainer myContainer = new UnityContainer();
2:
3: UnityConfigurationSection section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
4:
5: section.Configure(myContainer, "Inteception");
6:
7: var store = myContainer.Resolve<IStoreData<Int32>>();
L'interception
Il est maintenant temps de configurer le mécanisme d'interception. Nous allons coder un aspect « Chronomètre ». Cet aspect va nous servir à mettre en évidence la différence de performance entre nos 2 implémentations.
La première étape consiste à créer une classe qui implémente « IInterceptionBehavior ». L'essentiel de l'aspect va se retrouver dans le corps de la méthode « Invoke ». C'est à cet endroit que nous allons pouvoir injecter du code en amont et en aval de la méthode interceptée.
ChronoBehavior :
1: public class ChronoBehavior : IInterceptionBehavior, IDisposable
2: {
3: public IEnumerable GetRequiredInterfaces() { return Type.EmptyTypes; }
4:
5: public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
6: {
7: System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
8:
9: stopwatch.Start();
10:
11: var methodReturn = getNext().Invoke(input, getNext);
12:
13: Console.WriteLine("Invoking {0} in {1} ticks", input.MethodBase.ToString(), stopwatch.ElapsedTicks);
14:
15: return methodReturn;
16: }
17:
18: public bool WillExecute { get { return true; } }
19:
20: public void Dispose()
21: {
22:
23: }
24: }
La séquence des actions effectuée est la suivante :
Maintenant que l'aspect est codé, nous allons configurer Unity pour injecter notre aspect à l'appel des méthodes de notre interface.
App.config :
1: <xml version="1.0"?>
2: <configuration>
3:
4: <configSections>
5: <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration, Version=2.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
6: </configSections>
7: <unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
8:
9: <assembly name="ConsoleApplication2"/>
10:
11: <namespace name="ConsoleApplication2"/>
12:
13: <sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension, Microsoft.Practices.Unity.Interception.Configuration" />
14:
15: <alias alias="IStoreData" type="ConsoleApplication2.IStoreData`1, ConsoleApplication2" />
16: <alias alias="InsertOptimizedStore" type="ConsoleApplication2.InsertOptimizedStore`1, ConsoleApplication2" />
17: <alias alias="ReadOptimizedStore" type="ConsoleApplication2.ReadOptimizedStore`1, ConsoleApplication2" />
18:
19: <container name="Inteception">
20:
21: <extension type="Interception" />
22:
23: <register type="IStoreData" mapTo="ReadOptimizedStore">
24: <interceptor type="InterfaceInterceptor"/>
25: <interceptionBehavior type="ChronoBehavior" />
26: </register>
27:
28: <register type="ChronoBehavior"/>
29:
30: </container>
31:
32: </unity>
33:
34: </configuration>
Deux lignes supplémentaires ont été insérées lors de l'enregistrement de l'interface.
La première décrit le type d'interception que nous allons utiliser. Dans notre cas, nous utilisons un « InterfaceInterceptor », C'est la méthode la moins couteuse en terme de performance mais qui n'agit qu'un niveau des interfaces. Il existe d'autre type d'interception. Un comparatif détaillé est disponible sur MSDN (http://msdn.microsoft.com/en-us/library/ff660861(v=PandP.20).aspx)
La deuxième ligne va enregistrer l'aspect pour qu'il soit exécuté à chaque appel des méthodes de l'interface.
Protocole de test :
1: UnityContainer myContainer = new UnityContainer();
2: UnityConfigurationSection section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
3: section.Configure(myContainer, "Inteception");
4:
5: var store = myContainer.Resolve<IStoreData<Int32>>();
6:
7: for (int i = 0; i < 100000; i++) { store.AddToStore(i); }
8:
9: store.ExistInStore(64);
10: store.ExistInStore(128);
11: store.ExistInStore(256);
12: store.ExistInStore(512);
13: store.ExistInStore(1024);
14: store.ExistInStore(2048);
15: store.ExistInStore(4096);
16: store.ExistInStore(8192);
17: store.ExistInStore(16384);
18: store.ExistInStore(32768);
19: store.ExistInStore(65536);
20: store.ExistInStore(99999);
21:
22: Console.ReadLine();
On appel 100000 fois la méthode « AddToStore », puis on recherche des éléments à différents endroits
ReadOptimizedStore :
1: Invoking Void AddToStore(T) in 7 ticks
2: Invoking Void AddToStore(T) in 6 ticks
3: Invoking Void AddToStore(T) in 6 ticks
4: Invoking Void AddToStore(T) in 6 ticks
5: Invoking Void AddToStore(T) in 6 ticks
6: Invoking Void AddToStore(T) in 6 ticks
7: Invoking Void AddToStore(T) in 6 ticks
8: Invoking Void AddToStore(T) in 5 ticks
9: Invoking Void AddToStore(T) in 6 ticks
10: Invoking Void AddToStore(T) in 6 ticks
11: Invoking Void AddToStore(T) in 6 ticks
12: Invoking Void AddToStore(T) in 6 ticks
13: Invoking Void AddToStore(T) in 6 ticks
14: Invoking Void AddToStore(T) in 5 ticks
15: Invoking Void AddToStore(T) in 6 ticks
16: Invoking Void AddToStore(T) in 6 ticks
17: Invoking Void AddToStore(T) in 6 ticks
18: Invoking Void AddToStore(T) in 6 ticks
19: Invoking Boolean ExistInStore(T) in 2179 ticks
20: Invoking Boolean ExistInStore(T) in 29 ticks
21: Invoking Boolean ExistInStore(T) in 6 ticks
22: Invoking Boolean ExistInStore(T) in 7 ticks
23: Invoking Boolean ExistInStore(T) in 7 ticks
24: Invoking Boolean ExistInStore(T) in 7 ticks
25: Invoking Boolean ExistInStore(T) in 8 ticks
26: Invoking Boolean ExistInStore(T) in 7 ticks
27: Invoking Boolean ExistInStore(T) in 8 ticks
28: Invoking Boolean ExistInStore(T) in 7 ticks
29: Invoking Boolean ExistInStore(T) in 8 ticks
30: Invoking Boolean ExistInStore(T) in 7 ticks
InserOptimizedStore:
1: Invoking Void AddToStore(T) in 5 ticks
2: Invoking Void AddToStore(T) in 4 ticks
3: Invoking Void AddToStore(T) in 6 ticks
4: Invoking Void AddToStore(T) in 5 ticks
5: Invoking Void AddToStore(T) in 6 ticks
6: Invoking Void AddToStore(T) in 4 ticks
7: Invoking Void AddToStore(T) in 4 ticks
8: Invoking Void AddToStore(T) in 5 ticks
9: Invoking Void AddToStore(T) in 4 ticks
10: Invoking Void AddToStore(T) in 4 ticks
11: Invoking Void AddToStore(T) in 5 ticks
12: Invoking Void AddToStore(T) in 5 ticks
13: Invoking Boolean ExistInStore(T) in 4422 ticks
14: Invoking Boolean ExistInStore(T) in 25 ticks
15: Invoking Boolean ExistInStore(T) in 11 ticks
16: Invoking Boolean ExistInStore(T) in 16 ticks
17: Invoking Boolean ExistInStore(T) in 27 ticks
18: Invoking Boolean ExistInStore(T) in 48 ticks
19: Invoking Boolean ExistInStore(T) in 88 ticks
20: Invoking Boolean ExistInStore(T) in 171 ticks
21: Invoking Boolean ExistInStore(T) in 336 ticks
22: Invoking Boolean ExistInStore(T) in 668 ticks
23: Invoking Boolean ExistInStore(T) in 1328 ticks
24: Invoking Boolean ExistInStore(T) in 2602 ticks
Comme nous pouvons le constater, l'aspect à été injecté de façon transparente pour tous les appels aux méthodes exposées par notre interface.
Policy Injection
Après avoir créé notre propre aspect en implantant l'interface « IInterceptionBehavior », nous allons nous intéresser à une autre implémentation de « IInterceptionBehavior », faisant intégralement partie du Framework Unity : « PolicyInjectionBehavior »
Cet aspect bien particulier va servir de routeur vers de nouveaux aspects qui n'implémenteront plus «IInterceptionBehavior », mais « ICallHandler ».
PolicyInjectionBehavior c'est :
Nous allons donc recoder notre aspect de chronomètre en implémentant cette fois l'interface « ICallHandler ». Pas de grand changement puisque le code va fortement ressembler au précédent :
ChronoCallHandler :
1: public class ChronoCallHandler : ICallHandler
2: {
3: public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
4: {
5: System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
6:
7: stopwatch.Start();
8:
9: var methodReturn = getNext().Invoke(input, getNext);
10:
11: Console.WriteLine("Invoking {0} in {1} ticks", input.MethodBase.ToString(), stopwatch.ElapsedTicks);
12:
13: return methodReturn;
14: }
15:
16: public int Order { get; set; }
17: }
Maintenant, nous allons modifier le fichier de configuration pour y intégrer la section « policy » :
App.config :
1: <xml version="1.0"?>
2: <configuration>
3:
4: <configSections>
5: <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration, Version=2.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
6: </configSections>
7: <unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
8:
9: <assembly name="ConsoleApplication2"/>
10:
11: <namespace name="ConsoleApplication2"/>
12:
13: <sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension, Microsoft.Practices.Unity.Interception.Configuration" />
14:
15: <alias alias="IStoreData" type="ConsoleApplication2.IStoreData`1, ConsoleApplication2" />
16: <alias alias="InsertOptimizedStore" type="ConsoleApplication2.InsertOptimizedStore`1, ConsoleApplication2" />
17: <alias alias="ReadOptimizedStore" type="ConsoleApplication2.ReadOptimizedStore`1, ConsoleApplication2" />
18:
19: <container name="Inteception">
20:
21: <extension type="Interception" />
22:
23: <register type="IStoreData" mapTo="InsertOptimizedStore">
24: <interceptor type="InterfaceInterceptor"/>
25: <interceptionBehavior type="PolicyInjectionBehavior" />
26: </register>
27:
28: <interception>
29: <policy name="Chono">
30: <matchingRule name="rule1" type="MemberNameMatchingRule">
31: <constructor>
32: <param name="namesToMatch">
33: <array type="string[]">
34: <value value="ExistInStore" />
35: </array>
36: </param>
37: </constructor>
38: </matchingRule>
39: <callHandler name="handler1" type="ChronoCallHandler">
40: <lifetime type="singleton" />
41: </callHandler>
42: </policy>
43: </interception>
44:
45: </container>
46:
47: </unity>
48:
49: </configuration>
Dans cette configuration, le Handler « ChronoCallHandler » sera déclenché seulement à l'appel de la méthode « ExistInStore ». Dans l'exemple ci-dessous, nous utilisons une règle de type « MemberNameMatchingRule » qui se base sur le nom de la méthode à appeler. Il existe bien d'autre classes dans le framework unity dont voici la liste :
Handler Attribute
Unity va permettre également d'appeler directement vos aspects sans passer par le fichier de configuration des policy. Il suffit pour cela de créer un Attribut qui Hérite de « HandlerAttribute ». Grâce à cet attribut, nous allons pouvoir « tagger » nos méthodes/Propriétés pour que le handler soit directement injecté.
ChronoCallHandlerAttribute :
1: [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Method)]
2: public class ChronoCallHandlerAttribute : HandlerAttribute
3: {
4: public override ICallHandler CreateHandler(Microsoft.Practices.Unity.IUnityContainer container)
5: {
6: return new ChronoCallHandler();
7: }
8: }
Le corps de la méthode « CreateHandler » va simplement retourner une nouvelle instance du handler « ChronoCallHandler ». Ensuite, il ne reste plus qu'à décorer notre code avec cet attribut :
1: public interface IStoreData
2: {
3: [ChronoCallHandler()]
4: void AddToStore(T value);
5:
6: bool ExistInStore(T value);
7: }
Seul l'appel à la méthode « AddToStore » déclenchera l'injection de l'aspect « Chronomètre ». Cette méthode d'injection est très souple et va permettre d'avoir la granularité la plus fine possible.
Conclusion
Le Framework Unity permet d'obtenir une souplesse assez conséquente pour réaliser l'injection d'aspect. Grace à cet outil, vous allez pouvoir vous concentrer sur le code métier de vos applications et ainsi déporter les « crosscutting concern» dans un aspect qui leur est dédié.



