À la pointe
Invariants et héritage dans des contrats de code
Dino Esposito
Au cours des derniers articles de cette colonne, j'ai abordé deux des types de contrat logiciel les plus courants, les conditions préalables et les post-conditions, et analysé leur syntaxe et leur sémantique du point de vue de l'API des contrats de code dans Microsoft .NET Framework 4. Ce mois-ci, je commencerai par présenter le troisième type de contrat le plus important, l'invariant, puis j'examinerai le comportement des classes basées sur les contrats lorsque vous appliquez l'héritage.
Invariants
Dans son sens général, un invariant est une condition qui donne est toujours vraie dans un contexte donné. Appliqué au logiciel orienté objet, l'invariant indique une condition qui est toujours évaluée comme vraie sur chaque instance d'une classe. L'invariant est un outil formidable qui vous informe rapidement dès que l'état d'une instance de classe donnée devient non valide. En d'autres termes, un contrat d'invariant définit formellement les conditions selon lesquelles une instance de classe est considérée comme étant en bon état. Aussi catégorique que cela puisse sembler, il s'agit simplement du premier concept que vous devez d'abord comprendre, puis implémenter, lors de la modélisation d'un domaine métier via les classes. La conception orientée domaine est aujourd'hui la technologie avérée qui permet de modéliser des scénarios métier complexes. De plus, elle attribue à la logique d'invariant une position prédominante dans la conception. En fait, ce type de conception vous recommande fortement de ne jamais traiter les instances de classes qui ont un état non valide. De la même manière, il vous recommande d'écrire des fabriques de vos classes qui retournent les objets dans un état valide et que vos objets reviennent également dans un état valide après chaque opération.
La conception orientée domaine est une méthodologie qui vous laisse gérer l'implémentation réelle des contrats. Dans .NET Framework 4, les contrats de code aident grandement à effectuer une implémentation réussie, avec des efforts minimum à fournir de votre part. Découvrons plus en détail les invariants dans .NET Framework 4.
Où se trouve la logique de l'invariant ?
Les invariants sont-ils d'une façon ou d'une autre associés à la modélisation d'objet et à la conception de logiciel correctes ? Il est essentiel de comprendre ce domaine en détail. Une compréhension approfondie du domaine vous mène naturellement à la découverte de vos invariants. Et certaines classes n'ont tout simplement pas besoin d'invariants. L'absence d'invariants n'est pas un symptôme inquiétant en soi. Une classe sans contraintes relatives à ce qu'elle doit contenir et faire n'a tout simplement aucune condition d'invariant. Si votre analyse vous conduit à cette constatation, tout va bien.
Imaginez une classe représentant des actualités à publier. Cette classe aura certainement un titre, un résumé et une date de publication. Où se trouvent les invariants ? Eh bien, cela dépend du domaine d'entreprise. La date de publication est-elle obligatoire ? Si tel est le cas, vous devez veiller à ce que l'actualité contienne toujours une date valide et la définition de ce qu'est une « date valide » vient également du contexte. Si la date est facultative, vous pouvez enregistrer un invariant et veiller à ce que le contenu de la propriété soit validé avant d'être utilisé dans le contexte dans lequel il est utilisé. Il en va de même pour le titre et le résumé. Est-il judicieux d'avoir des actualités sans titre ni contenu ? Si tel est le cas dans le scénario professionnel que vous envisagez, vous avez une classe sans invariants. Sinon, soyez prêt à ajouter des vérifications de l'absence de titre et de contenu.
De façon plus générale, une classe sans comportement et jouant le rôle d'un contenu de données vaguement liées n'aura probablement pas d'invariants. Si vous avez un doute, je vous suggère de vous poser la question « Puis-je stocker une valeur ici ? » pour chaque propriété de la classe, qu'elle soit publique, protégée ou privée si elle est définie via des méthodes. Cela devrait vous aider à comprendre concrètement s'il vous manque des points importants du modèle.
À l'instar de nombreux autres aspects de la conception, la recherche d'invariants est plus fructueuse si elle est effectuée tôt lors du processus de conception. L'ajout d'un invariant tard dans le processus de développement est toujours possible, mais cela sera plus coûteux en termes de refactoring. Si vous optez pour cette solution, soyez prudent et attentif avec la régression.
Invariants dans des contrats de code
Dans .NET Framework 4, un contrat d'invariant pour une classe est la collection de conditions qui devraient toujours être vraies pour toutes les instances de la classe. Lorsque vous ajoutez des contrats à une classe, les conditions préalables concernent la recherche de bogues dans l'appelant de la classe, alors que les post-conditions et les invariants concernent la recherche de bogues dans votre classe et ses sous-classes.
Vous définissez le contrat d'invariant via une ou plusieurs méthodes ad hoc. Il s'agit de méthodes d'instance, privées, qui renvoient void et sont décorées avec un attribut spécial, ContractInvariantMethod. De plus, les méthodes d'invariant ne sont pas autorisées à contenir un code autre que les appels nécessaires à la définition de conditions d'invariant. Par exemple, vous ne pouvez ajouter aucun type de logique dans les méthodes d'invariant, qu'il soit pur ou non. Vous ne pouvez même pas ajouter de logique afin de consigner simplement l'état de la classe. Voici comment définir un contrat d'invariant pour une classe :
public class News {
public String Title {get; set;}
public String Body {get; set;}
[ContractInvariantMethod]
private void ObjectInvariant()
{
Contract.Invariant(!String.IsNullOrEmpty(Title));
Contract.Invariant(!String.IsNullOrEmpty(Body));
}
}
La classe News indique comme conditions d'invariant que le titre et le corps ne seront jamais null ni vides. Notez que pour que ce code fonctionne, vous devez activer un contrôle d'exécution complet dans la configuration du projet pour les différentes versions en fonction des besoins (voir la figure 1).
Figure 1 Les invariants requièrent un contrôle d'exécution complet pour les contrats
Essayez maintenant le code simple suivant :
var n = new News();
Vous aurez la surprise de recevoir une exception d'échec de contrat. Vous avez réussi à créer une nouvelle instance de la classe News, mais pas de chance, son état était non valide. Il est nécessaire d'adopter un autre point de vue sur les invariants.
Dans la conception orientée domaine, les invariants sont associés au concept de fabrique. Une fabrique est simplement une méthode publique chargée de créer des instances d'une classe. Dans la conception orientée domaine, chaque fabrique est chargée de renvoyer des instances des entités de domaine dans un état valide. Le plus important à savoir est que lors de l'utilisation des invariants, vous devez veiller à ce que les conditions soient remplies à tout moment.
Mais à quel moment ? La conception orientée domaine et l'implémentation réelle des contrats de code conviennent tous deux que les invariants doivent être vérifiés à la sortie de toute méthode publique, dont les constructeurs et les setters. La figure 2 illustre une version révisée de la classe News qui ajoute un constructeur. Une fabrique est similaire à un constructeur, mais étant donné qu'il s'agit d'une méthode statique, elle peut avoir un nom personnalisé et sensible au contexte et produire un code plus lisible.
Figure 2 Invariants et constructeurs reconnaissant les invariants
public class News
{
public News(String title, String body)
{
Contract.Requires<ArgumentException>(
!String.IsNullOrEmpty(title));
Contract.Requires<ArgumentException>(
!String.IsNullOrEmpty(body));
Title = title;
Body = body;
}
public String Title { get; set; }
public String Body { get; set; }
[ContractInvariantMethod]
private void ObjectInvariant()
{
Contract.Invariant(!String.IsNullOrEmpty(Title));
Contract.Invariant(!String.IsNullOrEmpty(Body));
}
}
Le code qui utilise la classe News peut être le suivant :
var n = new News("Title", "This is the news");
Ce code ne lèvera aucune exception parce que l'instance est créée et renvoyée dans un état qui prend en charge les invariants. Que se passe-t-il si vous ajoutez la ligne suivante :
var n = new News("Title", "This is the news");
n.Title = "";
La définition de la propriété Title sur la chaîne vide met l'objet dans un état non valide. Dans la mesure où les invariants sont contrôlés à la sortie des méthodes publiques, et que les setters de propriété sont des méthodes publiques, vous obtenez à nouveau une exception. Il est intéressant de noter que si vous utilisez des champs publics au lieu des propriétés publiques, les invariants ne sont pas contrôlés et le code s'exécute sans problème. Votre objet se trouve cependant dans un état non valide.
Notez que les objets dans un état non valide ne sont pas nécessairement source de problème. Dans les systèmes de grande taille, toutefois, par sécurité, vous pouvez gérer les choses de façon à obtenir automatiquement des exceptions si un état non valide est détecté. Cela vous aide à gérer le développement et à mener vos tests. Dans les petites applications, les invariants ne sont pas toujours une nécessité, même lorsque certains apparaissent à la suite de l'analyse.
Bien que les invariants doivent être vérifiés à la sortie d'une méthode publique, l'état peut être temporairement non valide dans le corps d'une méthode publique. Ce qui compte, c'est que les invariants soient vrais avant et après l'exécution des méthodes publiques.
Comment empêchez-vous l'objet d'entrer dans un état non valide ? Un outil d'analyse statique tel que Microsoft Static Code Checker serait capable de détecter qu'une tâche donnée va enfreindre un invariant. Les invariants vous protègent contre les comportements rompus, mais peuvent également vous aider à identifier les entrées mal spécifiées. En spécifiant ces dernières correctement, vous pouvez détecter plus aisément les bogues dans le code à l'aide d'une classe donnée.
Héritage de contrat
La figure 3 illustre une autre classe avec une méthode d'invariant définie. Cette classe peut servir de racine d'un modèle de domaine.
Figure 3 Classe racine basée sur un invariant pour un modèle de domaine
public abstract class DomainObject
{
public abstract Boolean IsValid();
[Pure]
private Boolean IsValidState()
{
return IsValid();
}
[ContractInvariantMethod]
private void ObjectInvariant()
{
Contract.Invariant(IsValidState());
}
}
Dans la classe DomainObject, la condition d'invariant est exprimée via une méthode privée déclarée comme méthode pure (c'est-à-dire qui ne modifie pas l'état). En interne, la méthode privée appelle une méthode abstraite qui sera utilisée par les classes dérivées pour indiquer leurs propres invariants. La figure 4 illustre une classe possible dérivée de DomainObject qui remplace la méthode IsValid.
Figure 4 Remplacement d'une méthode utilisée par les invariants
public class Customer : DomainObject
{
private Int32 _id;
private String _companyName, _contact;
public Customer(Int32 id, String company)
{
Contract.Requires(id > 0);
Contract.Requires(company.Length > 5);
Contract.Requires(!String.IsNullOrWhiteSpace(company));
Id = id;
CompanyName = company;
}
...
public override bool IsValid()
{
return (Id > 0 && !String.IsNullOrWhiteSpace(CompanyName));
}
}
Cette solution semble élégante et efficace. Tentons maintenant d'obtenir une nouvelle instance de la classe Customer qui passe des données valides :
var c = new Customer(1, "DinoEs");
Si nous cessons d'observer le constructeur Customer, tout semble parfait. Toutefois, dans la mesure où Customer hérite de DomainObject, le constructeur DomainObject est appelé et l'invariant est vérifié. Étant donné que IsValid sur DomainObject est virtuel (en fait, il est abstrait), l'appel est redirigé vers IsValid tel que défini sur Customer. Malheureusement, le contrôle est effectué sur une instance qui n'a pas été totalement initialisée. Vous obtenez une exception, mais ce n'est pas de votre faute. (Dans la dernière version de l'API des contrats de code, ce problème a été résolu et le contrôle d'invariant sur les constructeurs est repoussé jusqu'à ce que le constructeur le plus externe soit appelé.)
Ce scénario correspond à un problème connu : n'appelez pas de membres virtuels à partir d'un constructeur. Dans ce cas, cela ne se produit pas parce que vous avez codé directement de cette façon, mais en raison d'un effet secondaire de l'héritage de contrat. Vous avez deux solutions : supprimer la partie IsValid de la classe de base ou avoir recours au code de la figure 5.
Figure 5 Remplacement d'une méthode utilisée par les invariants
public abstract class DomainObject
{
protected Boolean Initialized;
public abstract Boolean IsValid();
[Pure]
private Boolean IsInValidState()
{
return !Initialized || IsValid();
}
[ContractInvariantMethod]
private void ObjectInvariant()
{
Contract.Invariant(IsInValidState());
}
}
public class Customer : DomainObject
{
public Customer(Int32 id, String company)
{
...
Id = id;
CompanyName = company;
Initialized = true;
}
...
}
Le membre protégé Initialized joue un rôle de protection et n'appellera pas dans IsValid remplacé tant que l'objet réel n'aura pas été initialisé. Le membre Initialized peut être un champ ou une propriété. Cependant, s'il s'agit d'une propriété, vous obtenez un deuxième passage des invariants, ce qui n'est pas strictement nécessaire puisque tout a déjà été contrôlé une fois. À cet égard, l'utilisation d'un champ permet d'obtenir un code légèrement plus rapide.
L'héritage de contrat est automatique dans le sens où une classe dérivée reçoit automatiquement les contrats définis sur sa classe de base. De cette façon, une classe dérivée ajoute ses propres conditions préalables et pré-conditions de la classe de base. Il en va de même pour les post-conditions et les invariants. Lorsque vous avez affaire à une chaîne d'héritage, l'outil de réécriture en langage intermédiaire résume les contrats et les appelle dans le bon ordre, au moment et à l'endroit appropriés.
Soyez attentif
Les invariants ne sont pas infaillibles. Les invariants peuvent parfois vous aider d'une certaine façon et entraîner des problèmes d'une autre, particulièrement s'ils sont utilisés dans chaque classe et dans le contexte d'une hiérarchie de classes. Bien qu'il soit recommandé de toujours chercher à identifier la logique d'invariant dans vos classes, si l'implémentation des invariants touche des cas limites, je dirais qu'il vaut mieux les supprimer de l'implémentation. Toutefois, n'oubliez pas que si vous devez gérer des cas limites, ils sont probablement dus à la complexité du modèle. Les invariants sont les outils dont vous avez besoin pour gérer les problèmes d'implémentation, ils ne sont pas le problème.
Il y a au moins une raison pour laquelle le résumé des contrats dans une hiérarchie de classes est une opération délicate. Ce serait trop long d'en discuter ici, mais ce sera un bon sujet pour l'article du mois prochain.
Dino Esposito est l'auteur de « Programming Microsoft ASP.NET 4 » (Microsoft Press, 2011) et co-auteur de « Microsoft .NET: Architecting Applications for the Enterprise » (Microsoft Press, 2008). Basé en Italie, Dino Esposito participe régulièrement aux différentes manifestations du domaine organisées aux quatre coins du monde. Vous pouvez le suivre sur Twitter à l'adresse twitter.com/despos.
Je remercie nos experts techniques d'avoir relu cet article : Manuel Fahndrich et Brian Grunkemeyer