Dans les 3 derniers posts, nous avons vu comment définir les 3 types de contrats qui sont les pré-conditions, post-conditions et invariants. Nous allons voir maintenant une des grandes fonctionnalités de Code Contracts qui est la possibilité de définir ces contrats dans des interfaces (et les classes abstraites).
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
Pourquoi définir des contrats dans des interfaces ?
Lorsque l’on défini une interface qui contient une méthode, il est fort probable que l’on souhaite définir des pré-conditions afin de contrôler la validité de ces arguments. Avant Code Contracts, les validations devait se faire sur chacune des classes qui implémentait l’interface concernée. Les inconvénients d’une telle méthode sont bien évidemment, la duplication de code (copier/coller), mais aussi (et c’est le plus grave !) l’oubli de la définition d’un contrat dans une des implémentations !
Un autre problème se pose avec les interfaces, et en particulier lorsque l’on travaille avec les intégrateurs, est l’assurance que le valeur retournée par une méthode implémentée (ou une propriété) est bien celle que l’on attendait.
Prenons un exemple simple : en tant que développeur, définissons une interface IProduitRepository qui contient une méthode GetProduits() permettant de récupérer un tableau de Produit en fonction de critères spécifiés dans une classe ProduitQuery :
public interface IProduitRepository { Produit[] GetProduits(ProduitQuery query); }
Toujours en tant que développeur, on dispose maintenant d’une classe ProduitsConsole qui utilise l’interface que l’on a précédemment défini. On défini une méthode GetProduits() faisant appel à la méthode GetProduits() afin d’afficher les différents produits qui y sont retournés :
public class ProduitsConsole { private IProduitRepository repository; public ProduitsConsole(IProduitRepository repository) { this.repository = repository; } public void AfficherProduits() { Produit[] produits; produits = this.repository.GetProduits(new ProduitQuery()); foreach (Produit p in produits) { Console.WriteLine(p.Nom); } } }
La classe ProduitsConsole, s’appuie donc sur une interface IProduitRepository qui sera implémenté par un intégrateur. En ce mettant dans la peau du développeur de la classe ProduitsConsole, on peut remarquer la présence potentiel d’un bogue lors de l’appel à la méthode GetProduits(). En effet, si l’implémentation de la méthode GetProduits() de l’interface IProduitRepository réalisé par l’intégrateur retourne la valeur null, le code précédent va automatiquement lever une exception dans la boucle foreach.
Bien évidemment, pour palier à ce problème, il serait plus judicieux de contrôler la valeur retournée par l’appel à GetProduits() et de déclencher une exception si la valeur retournée est null :
public void AfficherProduits() { Produit[] produits; produits = this.repository.GetProduits(new ProduitQuery()); if (produits == null) { throw new InvalidOperationException("Le repository a retourné une liste de produits null"); } foreach (Produit p in produits) { Console.WriteLine(p.Nom); } }
Grâce à Code Contracts on a la possibilité de définir des contrats sur les interfaces, on peut ainsi éviter ce genre de problème en imposant des conditions que devra respecter l’intégrateur lors de l’implémentation de notre classe.
Comment définir des contrats dans une interface.
Dans les posts précédents , nous avons vu que la définition des contrats n’était possible qu’en spécifiant du code C# faisant appel aux méthodes de la classe Contract. Cela va nous poser problème pour les interfaces car, tout le monde le sait, il est impossible de coder dans les interfaces !
Pour palier à ce problème, l’équipe de Code Contracts a eu l’idée de spécifier les contrats dans une classe “bidon” (que l’on appellera classe de contrats) qui implémente l’interface en question. Cette classe ne doit contenir que des contrats et ne doit pas être utilisé dans le code de production (c’est à dire instancié !).
Afin d’aider les outils de réécriture, il faut ajouter un attribut ContractClassAttribute sur l’interface afin de référencer la classe de contrats, et un autre attribut ContractClassForAttribute dans la classe de contrats faisant référence à l’interface.
Voici un exemple qui défini des contrats dans la méthode GetProduits() de l’interface IProduitService.
[ContractClass(typeof(IProduitRepositoryContract))] public interface IProduitRepository { Produit[] GetProduits(ProduitQuery query); } [ContractClassFor(typeof(IProduitRepository))] internal abstract class IProduitRepositoryContract : IProduitRepository { Produit[] IProduitRepository.GetProduits(ProduitQuery query) { Contract.Requires(query != null); Contract.Ensures(Contract.Result<Produit[]>() != null); throw new NotImplementedException(); } }
Même s’il n’est pas nécessaire d’implémenter explicitement les méthodes de l’interface, je vous conseille de le faire ! Cela vous permettra en cas de refactoring de l’interface (et en particulier en cas de suppression ou de changement de la signature d’une méthode) d’obtenir des erreurs de compilation afin de répercuter ces modifications dans la classe de contrats.
La vision intégrateur des pré-conditions et des post-conditions sur une interface.
Lorsque l’on est intégrateur, les contrats sur les interfaces doivent être vue de manière différente par rapport à ce que l’on a vue dans les articles précédents.
En effet, en orienté objet, une interface est un mécanisme qui permet à l’intégrateur d’étendre “une fonctionnalité” d’une classe codée par le développeur. L’interface est écrite par le développeur et ce dernier appel lorsqu’il est nécessaire, les différents services de cette interface. L’intégrateur quant à lui doit implémenter les différents services de l’interface exigé par le développeur.
A l’aide de Code Contracts, le développeur a la possibilité d’indiquer à l’intégrateur :
- La validité des paramètres d’entrées des services d’une interface. En effet, lors de l’appel d’une implémentation de service de l’interface, l’intégrateur n’a aucune idée de la validité des paramètres qui lui seront passé. Grâce à Code Contracts, et tout particulièrement à l’aide des pré-conditions, le développeur peut indiquer la validité des paramètres qu’il s’engage à valider lorsqu’il fera appel à une implémentation de service de l’intégrateur.
- Les paramètres de sortie (retour d’une méthode ou paramètre de sortie d’une méthode) des services d’une interface. En effet, lorsque l’intégrateur implémente des services qui renvois des données, rien n’indique au développeur que ces données récupérées sont valides. Pour cela grâce aux post-conditions, le développeur peut spécifier les paramètres de sortie exigés. Ce sera donc à l’intégrateur de respecter les post-conditions dans son implémentation.
Les post-conditions dans les interfaces un atout majeur !
Les post-conditions seront majoritairement utilisées dans les interfaces. Elles permettent de définir les valeurs de retour attendu par une implémentation réalisée par l’intégrateur.
L’exemple le plus fréquent est lorsque le développeur spécifie une méthode dans une interface qui retourne une collection. Dans majoritairement des cas, le développeur ne souhaite pas que l’intégrateur lui retourne null. Pour palier à ce problème, le développeur peut spécifier une post-conditions permettant d’indiquer à l’intégrateur que la collection retournée ne doit pas être nulle.
Produit[] IProduitRepository.GetProduits(ProduitQuery query) { Contract.Ensures(Contract.Result<Produit[]>() != null); throw new NotImplementedException(); }
Bien évidemment, dans le cas présenté ci-dessus, il est possible de spécifier encore d’autres de post-conditions tel que :
- Les éléments de la collection ne doivent pas être null.
- La collection doit contenir au moins un élément.
- …