I. Le traçage c'est pénible !▲
Le traçage, c'est pénible ! Il existe une multitude de bibliothèques pour le traçage, mais aucune ne vous épargne ce travail pénible : changer chaque méthode du code source pour qu'elle appelle cette bibliothèque. Et que devriez-vous faire si, au milieu du projet, vous choisissiez une nouvelle bibliothèque de traçage ? Il vous faudrait modifier toutes les méthodes tracées, et il peut en avoir des milliers ! Le traçage est pénible parce que, comme la plupart des besoins non fonctionnels (sécurité, transactions, cache…), il entrecroise tous les besoins fonctionnels. Si vous avez une centaine de processus métiers et que chacun doit être tracé, sécurisé et transactionnel, vous allez sans doute insérer dans l'implémentation de chacun de ces processus (donc dans une centaine d'objets) des instructions relatives au traçage, à la sécurité et aux transactions. Quel travail et, surtout, quel travail stupide ! C'est pourquoi il nous faut une meilleure façon d'encapsuler les fonctionnalités transversales (crosscutting concern), une façon qui ne nous force pas à modifier chaque méthode à laquelle ces fonctionnalités s'appliquent. Les custom attributs apportent une très bonne solution à ce problème.
II. Un custom attribut trivial pour le traçage▲
Ce que nous voulons est simple : un custom attribut qui écrit un message avant et après l'exécution des méthodes sur lequel il est appliqué. Nous voulons aussi spécifier la catégorie du message. Donc, idéalement, nous voudrions être capables d'utiliser le custom attribut comme suit :
[Trace("MyCategory"
)]
void
SomeTracedMethod()
{
// Method body.
}
Maintenant que nous savons ce que nous voulons, au travail ! D'abord nous déclarons le custom attribut comme nous sommes habitués à le faire. Tout ce dont nous avons besoin est un champ nommé category et un constructeur initialisant ce champ :
public
sealed class
TraceAttribute : Attribute
{
private
readonly string category;
public
TraceAttribute( string category )
{
this
.category =
category;
}
public
string Category {
get {
return
category; }
}
}
Pour l'instant, cet attribut n'est qu'une annotation purement informative. Nous pouvons ajouter ce qui va faire en sorte que cet attribut va effectivement modifier les méthodes auxquelles il est appliqué. Comme annoncé en introduction, nous utiliserons pour ce faire PostSharp, commençons donc par l'ajouter au projet :
La seule chose à faire maintenant est de faire dériver l'attribut de PostSharp.Laos.OnMethodBoundaryAspect au lieu de System.Attribute. Cette classe définit des méthodes qui seront appelées lorsque les méthodes auxquelles il est appliqué seront exécutées :
- OnEntry - avant l'exécution de la méthode ;
- OnSuccess - lorsque l'exécution se termine avec succès ;
- OnException - lorsque l'exécution se solde par une exception ;
- OnExit - lorsque l'exécution se termine, avec succès ou exception.
Nous implémentons seulement les méthodes qui nous intéressent : OnEntry and OnExit.
public
override
void
OnEntry(MethodExecutionEventArgs eventArgs)
{
// Add trace code here.
}
public
override
void
OnExit(MethodExecutionEventArgs eventArgs)
{
// Add trace code here.
}
L'implémentation de ces méthodes devrait appeler System.Diagnostics.Trace.WriteLine. Mais comment pouvons-nous savoir le nom de la méthode dans laquelle nous sommes actuellement ? Pas de problème, toutes les informations nécessaires sont contenues dans l'objet MethodExecutionEventArgs, qui est passé à OnEntry et OnExit. Ce qui nous intéresse, c'est la propriété eventArgs.Method ; mais si nous voulions aussi imprimer la valeur des paramètres reçus, nous pourrions les obtenir grâce à la méthode GetArguments(). Voici l'implémentation finale de notre attribut de traçage :
[Serializable]
public
sealed class
TraceAttribute : OnMethodBoundaryAspect
{
private
readonly string category;
public
TraceAttribute( string category )
{
this
.category =
category;
}
public
string Category {
get {
return
category; }
}
public
override
void
OnEntry( MethodExecutionEventArgs eventArgs )
{
Trace.WriteLine(
string.Format( "Entering {0}.{1}."
,
eventArgs.Method.DeclaringType.Name,
eventArgs.Method.Name ),
this
.category );
}
public
override
void
OnExit( MethodExecutionEventArgs eventArgs )
{
Trace.WriteLine(
string.Format( "Leaving {0}.{1}."
,
eventArgs.Method.DeclaringType.Name,
eventArgs.Method.Name ),
this
.category );
}
}
Avez-vous remarqué ? La classe est affectée de l'attribut Serializable. C'est requis pour tous les custom attributs de PostSharp Laos.
Essayons maintenant notre attribute sur un morceau de code :
internal static
class
Program
{
private
static
void
Main()
{
Trace.Listeners.Add(new
TextWriterTraceListener( Console.Out));
SayHello();
SayGoodBye();
}
[Trace( "MyCategory"
)]
private
static
void
SayHello()
{
Console.WriteLine("Hello, world."
);
}
[Trace("MyCategory"
)]
private
static
void
SayGoodBye()
{
Console.WriteLine("Good bye, world."
);
}
}
Exécutons le programme et… abracadabra, les appels aux méthodes sont tracés !
Comment cela fonctionne-t-il ? Si vous jetez un coup d'œil à la fenêtre de sortie (output window) de Visual Studio, vous verrez que PostSharp a été invoqué pendant le processus de construction du programme.
PostSharp a effectivement modifié la sortie du compilateur C# et a « amélioré » le programme de sorte que les méthodes de notre attribut de traçage soient appelées durant l'exécution du programme. Il est particulièrement intéressant d'inspecter le programme ainsi produit avec le Reflector de Lutz Roeder :
La sortie de reflector montre bien que du code a été ajouté
Comme vous voyez, notre méthode, à l'origine minuscule, est maintenant bien plus complexe. C'est parce que PostSharp a ajouté les instructions pour appeler notre attribut au début et à la fin de l'exécution.
III. Déjà vu ?▲
Si vous pensez que tout ceci est très similaire à la programmation orientée aspect (aspect-oriented programming, AOP), vous avez raison - PostSharp Laos n'est rien d'autre qu'un framework AOP.
Un aspect est défini dans Wikipedia comme « a part of a program that cross-cuts its core concerns, therefore violating its separation of concerns ». La plupart du temps, dans les applications métier, un aspect correspond à un besoin non fonctionnel comme le traçage, la sécurité, ou la gestion des transactions et des exceptions. La séparation des fonctionnalités (separation of concern) est l'un des principes de base en génie logiciel. Il stipule que les fragments de code implémentant la même fonction doivent être regroupés en composants les plus autonomes possible. Une mesure de la qualité de la conception d'un logiciel et la haute cohésion, mais le faible couplage de ses composants.
Les frameworks AOP rendent possible d'encapsuler les aspects dans des entités modulaires ; avec PostSharp Laos, ces entités sont des custom attributes. L'avantage principal de cette approche est sa simplicité : vous accédez à la programmation orientée aspect sans la courbe d'apprentissage qui lui est typique. De plus, PostSharp Laos est indépendant du langage (C#, VB.NET, J#…) et son intégration avec Visual Studio (Intellisense, débogueur…) est excellente.
Une autre caractéristique qui différencie PostSharp est qu'il opère au niveau du code intermédiaire (MSIL). Il n'est donc pas sujet aux limitations des solutions basées sur des proxys : vous pouvez ajouter des aspects sur des méthodes privées, les classes ne doivent pas être dérivées de MarshalByRefObject, etc.
Si AOP vous intéresse, vous pouvez également jeter un coup d'œil sur le Policy Injection Application Block de Microsoft Enterprise Library ou sur Spring .NET Framework, deux autres solutions AOP viables pour l'environnement .NET.
IV. Un pas plus loin : le multicasting des attributs▲
Super ! Nous avons un custom attribut de traçage. Mais que faire si nous avons des centaines de méthodes à tracer ? Devons-nous ajouter cet attribut à chaque méthode ? Bien sûr que non ! Grâce à une fonctionnalité appelée la propagation multiple - appelons-la par son petit nom : multicasting - il est possible d'appliquer un attribut à plusieurs méthodes en une seule ligne.
Par exemple, en ajoutant TraceAttribute sur la classe Program, nous appliquons en fait l'attribut sur chaque méthode de cette classe :
[Trace( "MyCategory"
)]
internal static
class
Program
{
...
Et si nous ne voulons pas que l'attribut soit appliqué sur la méthode Main, nous pouvons restreindre l'ensemble des méthodes auxquelles l'attribut s'applique :
Trace( "MyCategory"
, AttributeTargetMembers =
"Say*"
)]
internal static
class
Program
{
...
Autre possibilité, nous pouvons ajouter l'attribut au niveau racine de l'assemblage, et dans ce cas toutes les méthodes contenues dans l'assemblage seront tracées :
[assembly: Trace("MyCategory"
)]
Il est néanmoins nécessaire d'empêcher l'attribut d'être appliqué sur la classe TraceAttribute elle-même, parce que les aspects ne peuvent pas être eux-mêmes la cible d'aspects :
[Trace( null, AttributeExclude =
true
)]
[Serializable]
public
sealed class
TraceAttribute : OnMethodBoundaryAspect
{
...
V. Conclusion▲
J'ai essayé de montrer, dans cet article, comment développer des custom attributs qui ajoutent de nouveaux comportements à vos programmes .NET. Pour cette démonstration, j'ai utilisé PostSharp Laos, une solution pour la programmation orientée aspect sur .NET.
Avec trois ans d'existence, PostSharp est un projet qui a déjà atteint un certain stade de maturité. Au moment de rédiger cet article, PostSharp a déjà été téléchargé des milliers de fois. Il est actuellement en version 1.0 et entame la phase des release candidates (RC). PostSharp a déjà gagné la confiance de nombreuses entreprises, tant des sociétés de consultance que de développement logiciel. Le projet est porté par un développeur à temps plein qui offre du support, de la consultance et du développement sur mesure. Les erreurs sont la plupart du temps corrigées en une semaine.
Comme j'ai essayé de l'illustrer dans cet article, PostSharp modifie en fait les instructions MSIL de telle sorte que les aspects soient invoqués lors de l'exécution. On peut voir comment cela fonctionne en utilisant Reflector de Lutz Roeder. Cet article a seulement présenté un attribute qui ajoute un block try-catch autour des méthodes, mais il est également possible d'intercepter les appels faits dans un assemblage extérieur, d'intercepter les accès aux champs ou d'injecter de nouvelles interfaces dans les types.
Dans la seconde partie de cet article, je montrerai comment développer deux nouveaux attributs : un compteur de performance et un validateur de champ. D'ici là, bonne découverte de PostSharp !