Dans les posts précédents nous avons vus comment créer différents types de contrats. Maintenant nous allons voir une petite fonctionnalité très pratique de Code Contracts : Les Contract Abbreviators.
Ce post fait partie d’une série de post consacré à Code Contracts :
- Partie 01 – Introduction
- Partie 02 – Les pré-conditions
- Partie 03 – Les post-conditions
- Partie 04 – Les invariants
- Partie 05 – Les contrats sur les interfaces
- Partie 06 – Les contracts abbreviators
Introduction
Lorsque l’on définie des contrats avec Code Contracts, il arrive souvent de définir des contrats avec plusieurs instructions Contract.Requires() ou Contract.Ensures(). Par exemple, lorsque l’on veut tester si un argument de type IList<T> contient des éléments non nullable il est nécessaire d’écrire le code suivant :
public static class ListHelper { public static void Display<T>(this IList<T> liste) where T : class { Contract.Requires<ArgumentNullException>(liste != null); Contract.Requires<ArgumentNullException>(Contract.ForAll(liste, e => e != null)); foreach (T élément in liste) { Console.WriteLine(élément.ToString()); } } }
Dans cet exemple on remarque que pour tester si notre liste ne contient pas d’élément null, il est nécessaire d’ajouter 2 contrats : Le premier pour vérifier si la liste est non null, le second pour tester la non nullité de chaque éléments de la liste. Bien évidemment il est possible d’écrire ces 2 conditions en un seul contrat :
Contract.Requires<ArgumentNullException>(liste != null && Contract.ForAll(liste, e => e != null));
Mais comme on le peut constater cela rend le contrat beaucoup plus difficile à relire.
Pour palier à ce problème, on pourrait faire une méthode « helper » générique qui contient nos 2 contrats comme ceci :
public static void AllItemsAreNotNull<T>(IEnumerable<T> sequence) { Contract.Requires<ArgumentNullException>(sequence != null); Contract.Requires<ArgumentNullException>(Contract.ForAll(sequence, e => e != null)); }
Et utiliser ce helper ainsi :
public static void Display<T>(this IList<T> list) where T : class { ValidationHelper.AllItemsAreNotNull(list); foreach (T élément in list) { Console.WriteLine(élément.ToString()); } }
Cela fonctionne bien évidemment, mais si on isole notre classe ListHelper dans un assembly utilitaire, on remarque que le non respect de la pré-condition provoque le message suivant au niveau dans l’exception déclenchée :
Ce message d’erreur peut-être déroutant pour le développeur qui utilise notre ListHelper, en effet le message d’exception porte sur une paramètre ayant comme nom « sequence » alors que notre méthode Display() de ListHelper contient un paramètre intitulé « liste ».
Les contracts abbreviator
Pour palier au problème précédent, l’équipe de Code Contract propose de construire de nouveaux contrats en utilisant des méthodes utilitaires (« helper »). Ces méthodes ne sont plus considérées comme des méthodes .NET « classiques » qui seraient appelées à l’exécution, mais comme des méthodes contenant des contrats qu’il faudrait juste remplacer au niveau de l’appelant.
Par exemple, si on dispose de la méthode helper suivante qui contient ces pré-conditions :
public static void AllItemsAreNotNull<T>(IEnumerable<T> sequence) { Contract.Requires<ArgumentNullException>(sequence != null); Contract.Requires<ArgumentNullException>(Contract.ForAll(sequence, e => e != null)); }
Et on utilise cette méthode utilitaire comme une pré-condition dans notre méthode Display() :
public static void Display<T>(this IList<T> liste) where T : class { ValidationHelper.AllItemsAreNotNull(liste); foreach (T élément in liste) { Console.WriteLine(élément.ToString()); } }
On aurait voulu que cette méthode soit « transformée » comme ceci afin d’avoir les bons noms de variables au niveau des paramètres :
public static void Display<T>(this IList<T> liste) where T : class { Contract.Requires<ArgumentNullException>(liste != null); Contract.Requires<ArgumentNullException>(Contract.ForAll(liste, e => e != null)); foreach (T élément in liste) { Console.WriteLine(élément.ToString()); } }
Cette fonctionnalité est tout à fait possible si on utilise les contracts abbreviator de Code Contracts. Pour mettre en œuvre cette fonctionnalité, il est nécessaire d’ajouter un fichier source dans votre projet. Ce fichier source est inclus au moment de l’installation de Code Contracts dans le répertoire « C:\Program Files (x86)\Microsoft\Contracts\Languages\CSharp » et se nomme « ContractExtensions.cs ». Si vous regardez le contenu de ce fichier, vous pouvez constater qu’il contient un attribut « ContractAbbreviatorAttribute ». C’est cet attribut qu’il faudra placer sur la méthode helper qui contient nos contrats comme ceci :
[ContractAbbreviator] public static void AllItemsAreNotNull<T>(IEnumerable<T> sequence) { Contract.Requires<ArgumentNullException>(sequence != null); Contract.Requires<ArgumentNullException>(Contract.ForAll(sequence, e => e != null)); }
En utilisant notre contract abbreviator voici le message de l’exception que nous obtiendrons en cas de non respect de notre pré-conditions :
Bon à savoir !
- Les contract abbreviators sont des méthodes qui sont compilées mais jamais appelées directement. Il est donc possible de stocker les contract abbreviators dans un assembly « utilitaire » (par exemple dans le cœur de votre Framework), afin que vous puissiez réutiliser vos contracts abbreviator dans d’autres assemblys.
- Les contract abbreviators peuvent contenir des pré-conditions ou des post-conditions ou les deux !
Conseils sur l’utilisation des contracts abbreviator.
N’hésitez pas à utiliser les contracts abbreviator pour simplifier l’écriture de vos contrats. Voici des exemples de contract abbreviators permettant de rendre votre code beaucoup plus lisible :
public static class ValidationHelper { [ContractAbbreviator] public static void AllItemsAreNotNull<T>(IEnumerable<T> sequence) { Contract.Requires<ArgumentNullException>(sequence != null); Contract.Requires<ArgumentNullException>(Contract.ForAll(sequence, e => e != null)); } [ContractAbbreviator] public static void IsUtc(DateTime time) { Contract.Requires<ArgumentException>(time.Kind == DateTimeKind.Utc); } [ContractAbbreviator] public static void IsNotNull<T>(T value) where T : class { Contract.Requires<ArgumentNullException>(value != null); } [ContractAbbreviator] public static void ReturnNonNull<T>() where T : class { Contract.Ensures(Contract.Result<T>() != null); } }
Et un exemple d’utilisation de ces contrats :
public class FacturationServices { public DateTime GetMaxFactureDates(IEnumerable<Facture> factures, DateTime date) { ValidationHelper.AllItemsAreNotNull(factures); ValidationHelper.IsUtc(date); return factures.Max(f => f.Date); } public Facture CréerFacture() { ValidationHelper.ReturnNonNull<Facture>(); return new Facture(); } }