Un merci bien senti à Simon Laperle, étudiant en informatique de gestion S6 au Collège Lionel-Groulx à l'hiver 2009, qui a expérimenté avec une version antérieure de ce document et qui a amené à moi quelques suggestions de raffinement fort pertinentes.
Bien que j'aie une position tiède face aux propriétés comme celles de Delphi ou des langages .NET, je suis le premier conscient du fait que :
Sachant cela, l'envie m'a prise de mettre au point l'ébauche d'un système de propriétés pour C++ au cas où quelqu'un en aurait envie. Voici ce que cela donne.
L'idée d'une propriété est de faire apparaître un triplet valeur/ méthode d'affectation/ méthode de consultation comme une seule et même variable, dans le but de remplacer du code tel que celui proposé à droite... |
|
...par ceci, sans qu'il n'y ait de perte côté encapsulation. Pour y arriver, il faut que Nom (pour prendre cet exemple) soit un objet encapsulant la véritable valeur et implémentant, dans son opérateur d'affectation, des politiques de validation convenables à la donnée encapsulée. |
|
Une version simple (simpliste!) de la classe Propriete est proposée à droite. L'idée de base est là. Cette version pourrait être raffinée avec des traits sur T. Ceci permettrait entre autres de mettre en place un opérateur -> si T est un pointeur ou ne pas implémenter l'affectation si T est constant. Ces problèmes ne sont pas très difficiles à résoudre, alors permettez-moi de vous les laisser en exercice. Notez que cette version ne fait qu'enrober une donnée de type T sans offrir de validation. En soi, une telle approche est insuffisante pour nos fins. |
|
Une classe Personne ayant une propriété Nom encapsulant une std::string aurait l'air de ceci. Notez l'absence de mutateurs ou d'accesseurs ici, le tout étant déjà en place à travers Nom qui implémente à la fois l'affectation et la conversion implicite dans le type de la donnée encapsulée par la propriété. |
|
Aurait-il, selon vous, été sage d'implémenter la construction par copie dans la classe Propriete? Expliquez votre position. Un programme de test raisonnable pour ceci serait celui proposé à droite. Ce programme, bien que simple, montre une déficience de notre modèle trop simple : l'incapacité d'afficher p.Nom sans passer par une variable temporaire. |
|
Il faut comprendre ici que cout << p.Nom; ne compilerait pas du fait que le compilateur chercherait un opérateur d'écriture sur un flux prenant en paramètre une Propriete<string> et ne chercherait pas à la convertir en string. En retour, l'opération cout << static_cast<string>(p.Nom); aurait fonctionné mais je doute que nous souhaitions exiger cela du code client.
La solution la plus simple (et probablement la meilleure dans ce cas-ci) est de surcharger les opérateurs de projection sur un flux et d'extraction d'un flux pour une Propriete<T>. Dans les deux cas, l'implémentation est plutôt simple. Remarquez qu'il n'y a pas de perte d'encapsulation lors de la lecture puisque l'introduction d'une valeur dans la Propriete<T> se fait par l'opérateur d'affectation, donc à la fois de la même manière et selon les mêmes règles que dans le code client. |
|
Pour réaliser l'encapsulation à l'aide de propriétés, nous voulons être en mesure de choisir des politiques de validation propres à chacune des propriétés prise individuellement.
Ce comportement est décidé à la compilation, de manière statique (dans les langages .NET, pour encapsuler la donnée d'une propriété non-triviale, on rédigerait le code d'une méthode!), et se prête beaucoup mieux à des politiques qu'à du polymorphisme. L'ajout d'une classe politique dans le code à droite montre que l'ajout de règles de validation peut être à la fois simple et efficace. La politique de validation par défaut, ToujoursOk, sera un foncteur Pass-Through, ne réalisant aucune validation et présumant que toute valeur est admissible. Nous l'examinerons plus bas. Notez que ceci vient de transformer Propriete qui vient de passer d'un type générique sur la base d'un seul type à un type générique sur la base de plusieurs types. Remarquez la distinction entre les types Validate et InitValidate. Le premier permet de valider les tentatives de modification (par affectation) aux états de la propriété, alors que le second n'est applicable qu'à son état initial. Par défaut, j'ai utilisé ToujoursOk pour validation initiale, mais dans le respect des invariants de l'objet possédant la propriété, nous aurions pu faire en sorte que les deux correspondent à une seule et même politique (écrire class InitValidate = Validate plutôt que class InitValidate = ToujoursOk). |
|
En particulier, ce changement affecte la signature et l'implémentation des opérations d'accès (en lecture et en écriture) aux flux. Ces changements sont, heureusement, complètement transparents du point de vue du code client. |
|
Une classe Personne utilisant cette nouvelle version de Propriete et faisant en sorte qu'un nom de moins de cinq caractères soit illégal et qu'un voisin nul soit inacceptable serait celle proposée à droite. Pour comprendre le sens des politiques utilisées ici, voir un peu plus bas. |
|
Une autre classe, EntierPositif, n'accepte que des entiers strictement positifs et est telle que ses instances peuvent être construites à la compilation, même si elle contient une propriété. |
|
Un programme correct serait, quant à lui, celui proposé à droite. Des blocs try ... catch ont été placés là où des tentatives de corruption des propriétés (en fonction des politiques mises en place) sont réalisées, de manière à attraper les exceptions levées et à permettre au programme de démonstration de poursuivre normalement ses opérations. Notez qu'avec les politiques de validation mises en place, ce programme affichera :
L'affectation de "Joe" à p0.Nom échouera parce que ce nom est trop court pour notre politique de validation. Les autres échecs devraient être évidents. |
|
Toutes nos politiques de validation seront des prédicats, idéalement constexpr. La liste proposée ici se veut illustrative (l'ensemble des possibles est infini).
Nous avons déjà aperçu, du moins d'un point de vue utilisation, la petite politique ToujoursOk qui correspond conceptuellement à aucune validation appliquée; le paramètre est nécessairement correct. Il s'agit d'une sorte de no-op pour les besoins de la cause. Une propriété ToujoursOk est essentiellement un attribut. |
|
Deux politiques typiques sur des types numériques seraient d'accepter une valeur seulement si elle est positive (ou strictement positive, selon le cas). Des cas plus généraux sont envisageables : plus grand (ou plus grand ou égal) qu'un certain seuil, par exemple. Les exemples de politiques proposés à droite couvrent certains des cas envisageables. On pourrait généraliser encode un peu plus la démarche (je vous laisse vous amuser si vous avez envie de pousser le tout un peu plus loin). |
|
Il est aussi possible d'envisager des règles de validation pour les propriétés qui s'appliquent à celles dont la valeur est un conteneur. Quelques exemples apparaissent à droite. D'autres cas, non couverts ici, sont faciles à déterminer et sujets à être fort utiles (des combinaisons de politiques, par exemple). Amusez-vous à souhait. |
|
Quelques liens pour enrichir le propos.