Accueil Nos publications Blog Programmer par contrats : promesse tenue ?

Programmer par contrats : promesse tenue ?

codecontracts_cover

La programmation par contrats est une bonne idée du langage Eiffel. Le C# inclut lui aussi le paradigme. Java avait tenté avec les assertions. Qu’en attendre ? Peut on faire de la qualité avec les contrats ?

Qu’est ce donc que la programmation par contrats ?

C’est préciser dans une méthode un contrat d’utilisation. A savoir, notamment, la définition de :

  • pré conditions : quelles conditions doit on vérifier impérativement avant l’exécution du code ? Par exemple, pour tester la connexion à une base de données, il faut connaître au moins son adresse et ses informations de connexion
  • post conditions : une fois la méthode exécutée, que garantit-elle ? Par exemple, pour le calcul de x –> x², on attend (évidemment) que le résultat soit le carré du paramètre…
  • invariants : par exemple, dans l’algorithme de Dijkstra, on veut qu’à tout moment, pour chaque noeud déjà marqué, on connaisse le plus court chemin et sa longueur
  • tests locaux : garantir à tel moment de l’exécution qu’une propriété est vérifiée

A quoi ça sert ?

La question est très bonne et mérite que l’on s’y attarde un peu.
Commençons par ce à quoi ça ne sert pas. Cela ne sert pas à remplacer une exception. D’ailleurs, une violation de contrat peut lever une exception : c’est le cas en Java avec assert. En C#, ça arrête net l’exécution. Est ce bien ? C’est l’éternel débat. Personnellement, je préfère un arrêt sauvage de l’exécution plutôt qu’un résultat faux qui passe inaperçu et qui se propage. La programmation par contrats est avant tout un moyen fort de préciser dans le code les prérequis et les garanties de chaque méthode. Alors, la programmation par contrats, c’est intrusif. Oui, certes. Les conséquences sont notoires, notamment sur le code produit. Sauf que… Sauf que nos amis de feu Sun ont pensé à tout et ont inclus la possibilité de compiler du code sans assertion.

Ce qui nous amène à répondre à la question suivante : les contrats, c’est bien, mais quelle différence avec les tests ? En Java, le gain n’est pas exceptionnel. Doux euphémisme pour sous entendre “presque rien”. Entre écrire un cas de test ou une assertion, l’avantage du test reste de ne pas être intrusif et que, de toute façon, on y a pensé avant de livrer. En Eiffel, en revanche, le gain est colossal, parce que l’outil génère lui même les tests ! Oui, mesdames messieurs, je l’ai appris de la bouche même de monsieur Meyer (le concepteur d’Eiffel), je vous le jure. Voilà en fait le coeur du problème : la programmation par contrats se différencie des tests seulement si l’on n’a plus à les écrire soi même. Donc, il faut que l’outil les génère.

Notre prochaine question de savoir comment programmer en utilisant les contrats. Très simplement en Eiffel, puisque le paradigme est prévu et inclus dans le langage. En Java, non moins simplement avec l’utilisation d’assertions. La faiblesse la plus évidente du système d’assertions en Java reste que le code est dans une méthode, et ne peut donc pas être inclus dans une interface. Charge aux développeurs de préciser, chaque fois, quelles assertions doit vérifier chaque implémentation.

Comment utiliser les contrats en C# ?

En C#, l’idée a été poussée plus loin que chez Oracle, avec un namespace dédié : System.Diagnostics.Contracts. 

Attention : code contracts n’est pas installé par défaut sur Visual Studio ! Avant d’expliquer son utilisation, il va falloir l’installer. Ce n’est, hélas, pas possible avec la version express de Visual Studio. Pour les autres, il suffit d’aller sur https://research.microsoft.com/en-us/projects/contracts/ pour le téléchargement. Installé en quelques minutes, rien d’autre que d’exécuter le .msi ne vous sera demandé.

Une fois l’installation terminée, vous avez tout ce qu’il faut pour découvrir la programmation par contrats en C#. La classe Contracts permet de définir (notamment) les contrats :

  • Contract.Requires(bool condition) définit les préconditions de la méthode (ce que la méthode garantit pour que tout se passe bien)
  • Contract.Ensures(bool condition) définit les post conditions de la méthode (ce qu’elle garantit à ses appelants)

Le plus impressionnant reste la possibilité de définir des contrats pour les interfaces ! Afin de présenter le concept, nous allons prendre un exemple simpliste :



public class User
{
  public User(string login)
  {
    Login = login;
  }

  public string Login { get; private set; }

}

Et on veut définir un  DAO pour sauver nos utilisateurs. On définit pour cela une interface IDao dont le contrat est le suivant :



public interface IDao
{
  // return true if login is the login of a "stored" user
  // login cannot be null
  bool Contains(string login);
  // save an user and return true whether the user has been updated
  // user cannot be null.
  // at the end of this method, user should be stored,
  // whether it was previously stored or not
  bool Save(User user);
}

Et bien entendu, chaque implémentation devrait vérifier ces conditions (user non nul, à la fin de la méthode Save, l’user est effectivement sauvé, etc). Tout l’intérêt est là : définir une fois, et une seule, les contrats de chaque implémentation. Et pas, implémentation par implémentation, répéter les mêmes contrats… On va alors utiliser une (fausse) implémentation de l’interface qui va définir les contrats à respecter. Celle ci sera liée une fois à l’interface, et toutes les conditions seront définies une seule fois, en l’occurrence dans cette classe :



public class ContractDao : IDao
{
  public bool Save(User user)
  {
    // param user cannot be null
    Contract.Requires(user != null);
    // param user cannot be null
    Contract.Requires(user.Login != null);
    // after save execution, we contain the user
    Contract.Ensures(this.Contains(user.Login));
    return default(bool);
  }

  public bool Contains(string login)
  {
    // login cannot be null
    Contract.Requires(login != null);
    return default(bool);
  }
}

Ensuite, il nous faut lier le contrat à l’interface. Il existe deux attributs pour cela. Un pour lier la classe qui définit les contrats à l’interface. Soit, dans notre cas, ContractDao à IDao :



[ContractClassFor(typeof(IDao))]
public class ContractDao : IDao

et l’autre attribut qui lie l’interface à sa classe de contrats. Soit, dans notre cas, IDao à ContractDao:



[ContractClass(typeof(ContractDao))]
public interface IDao

Pour l’implémentation, en guise d’exemple, nous avons fait au plus simple : une classe qui utilise un dictionnaire. Les clés sont les login, les valeurs sont les utilisateurs :



public class InMemoryDao : IDao
{

  private Dictionary<string, User> userTable;

  public InMemoryDao()
  {
    this.userTable = new Dictionary<string, User>();
  }

  public bool Save(User user)
  {
    bool contains = this.userTable.ContainsKey(user.Login);
    this.userTable.Add(user.Login, user);
    return contains;
  }

  public bool Contains(string login)
  {
    return this.userTable.ContainsKey(login);
  }
}

Si nous ne respectons pas un contrat, comme dans le code ci dessous :



class Program
{
  static void Main(string[] args)
  {
    //const string login = "soat";
    // Make it crash!!!
    const string login = null;
    User user = new User(login);
    IDao dao = new InMemoryDao();
    Console.WriteLine("Expected false:" + dao.Contains(login));
    dao.Save(user);
    Console.WriteLine("Expected true:" + dao.Contains(login));
  }
}

Notre debugger préféré nous rattrapera au vol:

CONTRACTS_executionCrash

Enfin, signalons qu’il est possible de configurer l’usage du namespace Contracts, avec notamment la possibilité d’inclure, ou pas, à l’exécution, les tests des conditions des contrats.

Configuration des contrats

Quelles sont les limites de la programmation par contrats ?

J’en relèverai deux :

  1. écrire des conditions fonctionnelles est difficile. L’usage est plutôt local et technique
  2. il n’a pas d’équivalent pour la validation des données. Son rôle est la validation des méthodes

Reprenons ces deux points en détail.
Programmation par contrats et validation fonctionnelle : l’idée est qu’il existe plusieurs sortes de contraintes, qu’on va illustrer sur le cas des actions en finance. On considère par exemple les actions du CAC40. Une action du CAC40 est valide si ses principales caractéristiques sont connues (isin, code marché, etc) et si elle figure dans les codes ISIN du CAC40. Cela signifie très concrètement aller charger des informations externes, les vérifier, et valider (ou pas). Donc, possiblement, un accès base, donc du temps, un risque d’erreur, etc. Dans ce cas là, s’il existe des contraintes externes à vérifier, autant passer par des tests qu’on peut jouer à la demande. Autre souci : la complexité de la validation fonctionnelle. Il faudra probablement développer une méthode particulière de validation, et donc la tester. Retour à la case départ.

Concernant la validation des données, que souhaite t’on faire ? Les parcourir, détecter les anomalies, et les signaler. L’hérésie suprême serait un test (fût-il unitaire, d’intégration, etc). L’autre erreur serait de passer par les contrats. Pourquoi ? Parce qu’on se contenterait de parcourir les données, et lever une exception quand quelque chose ne va pas. C’est techniquement possible, mais ce n’est absolument pas, mais alors pas du tout, ce que les contrats visent à faire. Aussi utile que d’utiliser les exceptions pour gérer les évènements… Alors, que faire ? Une solution reste, désolé de sortir l’artillerie lourde, le moteur de règles pour la vérification statique, c’est à dire lire les données depuis un support fixe (une base de données par exemple),  les valider, relever les exceptions dans un outil dédié. C’est exactement ce que propose Sungard pour la validation des données de référentiel et de back office (suite AAPT). D’emblée, utiliser un tel outil peut faire peur. Or, d’expérience, JBoss Drools est un outil merveilleux, très simple d’utilisation et fort bien adapté.