20 juin 2012

Valeurs et entités : les deux grandes classes d'objets

Mise à jour : les commentaires de ce billet ont mis en évidence un problème plus que certain dans la relation illogique que j'ai établi entre sémantique de mouvement (notamment telle qu'elle est implémentée en C++11) et entité. Du coup, ce billet a été corrigé. Je le publie de nouveau en tête de page afin de permettre aux personnes l'ayant lu au moment de sa sortie de voir ces corrections.

En programmation objet, on manipule différents types d'objets. Certains obtiennent naturellement un constructeur par copie, d'autres non. Certains sont utilisés via des pointeurs, tandis que d'autres sont simplement copiés ici et là au gré des besoins. En bref, et même si vous ne vous en êtes pas encore rendu compte, certains de vos objets ont une sémantique de valeur, tandis que d'autres ont une sémantique d'entité.

Le but de cette article n'est pas de vous apprendre aujourd'hui à différencier une valeur d'une entité, mais de vous faire toucher du doigt ces deux grandes classes d'objets que tout oppose.

Caractéristiques des entités

Une entité est, selon l'une des définitions du Wikitionnaire, une chose considérée comme un être ayant son individualité. Pour parler plus simplement, quel que soit l'ensemble considéré (les hommes, les tables en bois, les personnages nons joueurs dans un jeu vidéo, ...), chaque individualité de l'ensemble est différente de toute les autres parties du même ensemble (je ne suis pas mon frère ; la table à ma gauche a beau être semblable à celle qui est située à ma droite, elle sont disjointes et donc différentes ; le PNJ situé près de l'étang n'est pas celui situé dans la barque du Grand Lac Gelé ; etc).

Entité et unicité

En considérant que les entités sont différentes, et ce quel que soit par ailleurs leur degré de similitude (deux jumeaux ont beau être jumeaux, il n'en sont pas moins des personnes différentes), on arrive trivialement à la conclusion que chaque entité est unique. Cette unicité est un critère important en architecture objet, car cela signifie que dès lors qu'on doit travailler en différents endroits sur la même entité, on ne peut que travailler sur une référence (via un pointeur ou une référence C++) sur cette entité, et non pas sur une copie. Car réaliser une copie d'une entité signifierait qu'on crée une entité identique à la première, ce qui contredit le fait que les entités sont toutes différentes l'une de l'autre.

On référence des entités, on ne les copie pas. Copier une entité crée une entité différente de l'entité originale.

Comparaison d'entités

Les entités étant toutes différentes, peuvent-elle être comparées ? Peuvent-elle être ordonnées ?

Par définition, deux entités ne peuvent pas être égales, puisqu'elles sont nécessairement différentes. Cependant, elles peuvent quand même avoir des critères de similitude, et ces critères de similitude peuvent tout à fait être organisés pour créer des relations d’ordonnancement ou d'équivalence. De fait, il est tout a fait possible de comparer des entités et de les ordonner.

Il faut comprendre toutefois que cette comparaison ne compare pas les entités, mais bien (tel que je viens de le dire) des caractéristiques de ces entités. Une entité ne peut pas être supérieure à une autre, mais elle peut être plus grande. La différence étant d'ordre sémantique, l'exprimer au niveau du code facilitera la lecture de celui-ci. On préférera donc utiliser des fonctions (libres ou non) pour comparer des entités plutôt que opérateurs surchargés.

entite e1, e2;
if (e1 == e2) // aucun sens au niveau sémantique : 
               // e1 et e2 sont par définition différents
{ ... }
if (e1.older_than(e2)) // ok : on compare une caractéristique de e1 et e2
{ ... }

Dans la pratique, c'est ce qui est fait dans la grande majorité des cas. Les limitations de C++98 ne permettant pas d'implémenter aisément la sémantique de mouvement, les entités étaient le plus souvent utilisées via un pointeur - et les opérateurs de comparaisons ne pouvant s'utiliser sur des pointeurs, il devenait logique de créer des fonctions de comparaison spécifiques.

C++11 introduit les rvalue-reference qui permettent - entre autre - d'implémenter une sémantique de mouvement pour les objets. Les pointeurs deviennent donc moins nécessaires. Le contre-coup à prévoir est l'erreur signalée ci-dessus, à savoir que les programmeurs risquent fort d'implémenter les comparaisons entre des entités non pas sous forme de fonctions nommées mais sous forme d'opérateurs surchargés[1].

Seules les caractéristiques des entités peuvent être comparées, pas les entités elles-mêmes

Validité d'une entité

Une entité doit vérifier que son invariant n'est jamais violé (ce qui se fait via l'encapsulation nécessaire). Dès lors, la question "peut on avoir une entité invalide" ne signifie pas qu'on peut, à force de manipulation, obtenir une entité invalide, mais qu'on crée à dessin une entité qui n'est pas valide (par exemple en utilisant un constructeur spécifique).

Le seul intérêt d'une entité invalide est de jouer un rôle de substitut. Au niveau du code, il n'est à priori pas inintéressant d'avoir un tel substitut (cela permet d'éviter les cas particuliers dans la gestion de l'entité, et notamment de supprimer les vérifications du type "est-ce que l'entité considérée existe"), mais on peut quand même se poser de cet utilité au niveau architectural. D'un point de vue sémantique, une entité invalide est un non-sens : la définition de wikipedia, rappelée ci-dessus, pré-suppose que l'entité existe avant de la qualifier de ce nom. En fait, soit une entité existe, soit elle n'existe pas - et elle n'est pas traitée[2]. On peut aller plus loin dans ce raisonnement car si une entité n'existe pas alors elle n'est même pas représentée par un pointeur NULL (c'est une chose commune dans le code).

Une entité existe ou n'existe pas ; si elle existe, elle est toujours valide.

Modification des entités

Modifier les caractéristiques d'une entité ne modifie pas l'entité elle-même. On peut le comprendre aisément en prenant un exemple simple : vous-même. Vous êtes une entité - un être unique. Et pourtant, vous changez tous les jours (ne serait-ce que parce que vous vieillissez au rythme impressionnant de 1 seconde par seconde). Et pourtant, vous restez vous-même.

Modifier les caractéristiques d'une entité ne crée par une entité différente.

C++11 et entités

Mise à jour : ce paragraphe, dont je ne me rappelle plus les conditions d'écriture, semble avoir échappé à la logique - en tout cas, il n'est pas seulement problématique, il est tout en grande partie faux. Car mêler sémantique de mouvement en entités est un non-sens, comme les commentaires ci-dessous me l'ont bien fait comprendre. Du coup, je me vois dans l'obligation d'en invalider une partie - que je laisse quand même pour des raisons historiques (ne pas cacher ses erreur, c'est important ; il faut apprendre d'elles, et se les remémorer de temps en temps).

Comme je l'ai dit un peu plus haut, le standard C++11[3] introduit les rvalue-reference, permettant de définir une sémantique de mouvement pour les objets, ce qui en retour autorise le programmeur à se passer de pointeurs pour implémenter des entités. Les caractéristiques d'une classe représentant des entités sont les suivantes :

  • pas de constructeur par copie public
  • pas d'opérateur de copie privé
  • la classe défini un move-constructor
  • la classe défini un opérateur de déplacement

La section [dcl.fct.def.delete] du standard C++11 propose un exemple d'implémentation pour atteindre ce résultat, en voici un autre :

class my_entity
{
public:
  // fonctions membres spéciales supprimées
  my_entity(const my_entity&) = delete;
  my_entity& operator=(const my_entity&) = delete;
// fonctions membre spéciales implémentées my_entity(my_entity&&); my_entity& operator=(my_entity&&); };

A ces fonctions membres spéciales, il convient d'ajouter un destructeur ainsi que, possiblement, un constructeur par défaut (on peut toutefois se demander quel est l'intérêt d'un constructeur par défaut dans ce cas précis ; mais ceci est une autre histoire).

Caractéristiques des valeurs

Valeur et unicité

Une valeur est nécessairement partie d'un ensemble comprenant au moins elle-même. Dans la grande majorité des cas, cet ensemble contiendra d'autres valeurs, mais ce n'est pas nécessaire (on peut tout à fait imaginer un ensemble ne comprenant qu'une seule valeur, même si un tel ensemble aura un intérêt réduit en termes de programmation). Puisqu'elle fait partie d'un ensemble, son unicité est définie dans cet ensemble : aucune autre valeur ne lui est identique, car toute autre valeur qui lui serait identique serait elle-même, et non une autre valeur.

Les valeurs peuvent être copiées. La copie d'une valeur est la valeur elle-même.

Comparaison des valeurs

La copie de valeur ne créant pas de nouvelle valeur, il est évident qu'on peut tester l'égalité entre deux valeurs. Cette opération a une sémantique forte : on vérifié que deux valeurs sont identiques (ou différentes, selon l'opérateur choisi).

Les valeurs peuvent être comparées entre elles..

Les conditions suivantes doivent cependant être vérifiées :

  • si v1 == v2 est vrai, alors v1 != v2 est faux.
  • si v1 == v2 est faux, alors v1 != v2 est vrai.

On peut écrire ces conditions sous une forme abrégées :

(v1 != v2) == !(v1 == v2)

L'étendue de ces comparaison diffère selon l'ensemble considéré, certains opérateurs de comparaison pouvant ne pas exister. Seul l'opérateur de test d'égalité est assuré d'exister[4]. Si les valeurs peuvent être ordonnées, alors les opérateurs <, <= (et bien entendu leurs pendant >, >=) existent aussi.

Modification des valeurs

Modifier une valeur change la valeur elle-même. Trivialement, si on ajoute 1 à un entier, je n'ai plus le même entier. Quelle que soit la modification apportée à la valeur, cette modification entraine la création d'une autre valeur.

Les valeurs sont immuables.

Validité d'une valeur

Puisque toute modification crée une nouvelle valeur, alors par définition il n'existe aucune modification qui puisse faire passer cette valeur d'un état "existant" à un état "non-existant" puisque dans le cas ou une telle opération existerait, son résultat serait à la fois une nouvelle valeur et pas de valeur du tout. Par contre, cette nouvelle valeur peut être invalide - c'est à dire qu'elle n'appartient pas au domaine de validité de l'ensemble défini par toutes les valeurs acceptés. Une telle valeur invalide peut-être construite à dessin (exemple : valeur non initialisée, ou initialisée avec des caractéristiques qui sont elle-même invalide) ou modifiée de telle manière qu'elle en devient invalide (exemple : un objet logarithme(x) dont on passerait la propriété x à 0).

Une valeur invalide peut être très utile dans certains cas. Bien souvent, on s'en sert dans un but de signalement - par exemple, un code d'erreur numérique négatif signifie qu'une opération a échoué.

Une valeur existe toujours. Elle peut être valide ou invalide.

C++11 et valeurs

C++a permis, dès son origine, la création de classes représentant des valeurs. Une telle classe a les caractéristiques suivantes :

  • elle possède un constructeur par copie
  • elle possède un constructeur par défaut
  • elle possède un destructeur[5]
  • elle défini au moins l'opérateur d'égalité

L'opérateur d'inégalité est ce qu'on appelle du sucre syntaxique, dans le sens où étant l'exact opposé de l'opérateur d'égalité (vois ci-dessus).

Si besoin, on peut aussi définir les opérateurs <, <=, >, >= - s'ils ont du sens relativement à l'ensemble de valeurs considéré.

C++11 introduit un nouveau moyen de construire les valeurs - les user defined litterals. En créant un nouveau suffixe (et le traitement nécessaire pour convertir le littéral définie avec ce suffixe en une valeur), on peut donner encore davantage de sens à son code[6].

Composition de valeurs et d'entités

Trivialement, un objet composé de une ou plusieurs entités ne peut pas être une valeur - Il s'agit donc d'une entité[7].

Lorsqu'un objet est composé de plusieurs valeurs, ça ne fait pas nécessairement de lui une valeur. Prenons deux exemples différents

  • un objet weight, composé d'une valeur quantity et d'une valeur unit. Un tel objet est lui aussi une valeur (on a bien l'identité 100 g == 0,1 kg ; modifier la quantité ou l'unité modifie la valeur de l'objet).
  • un objet contact_sheet_template, composé des valeurs suivants : photo_count, col_size et line_size. Cet objet est bel et bien une entité : la modification d'une des valeurs qui la compose ne change pas son identité - cela reste le même patron de planche contact[8].

En fait, on se rends compte qu'un grand nombre de classes d'entités ne sont composées exclusivement que de valeurs.

Conclusions

Classes de valeur et classes d'entité sont deux types d'objets qui sont fondamentalement différents. Si une entité peut être composée de plusieurs valeurs, l'inverse n'est pas vrai. Les caractéristiques des deux classes d'objets sont incompatibles, et leur fonctionnement intrinsèque diffère fortement.

En architecture orientée objet, il est nécessaire de bien faire la dichotomie entre ces deux types de classes. Une bonne dichotomie est la base d'une bonne approche du problème. Elle nous donnera des pistes sur ce qu'il est possible et raisonnable de faire avec les objets de l'application, et nous dira comment ceux-ci doivent être traités par le programmeur.

Je termine cet article en vous donnant deux lieux, qui résument tout ce qui a été dit ci-dessus. Ces deux liens sont extraits de la FAQ du site developpez.com :

Merci de votre attention, et à bientôt !

Notes

[1] Bien évidemment, s'il n'existe qu'un unique critère sur lequel on peut faire des comparaisons, on peut être tenté de surcharger les opérateurs nécessaires. Sémantiquement, ça n'a aucun sens (et cela complique donc la compréhension du code), et il est quand même préférable de l'éviter. Dans les cas où un opérateur de comparaison est nécessaire (par exemple pour utiliser les algorithmes de la C++SL), on préférera passer par un prédicat qui sera souvent (mais pas nécessairement) présenté sous la forme d'une fonction lambda.

[2] l'intérêt de traiter quelque chose qui n'existe pas reste effectivement assez faible

[3] l'un des tous derniers draft, nommé N3242 et très proche du standard, est disponible en ligne (pdf)

[4] si le test d'égalité existe, il est évident que le test de non égalité existe aussi

[5] La présence de ces deux membres spéciaux entraîne aussi la présence d'un destructeur - ces trois membres forment ce qu'on appelle communément la règles des trois - et assez communément d'un constructeur par défaut, de manière à pouvoir stocker les valeurs dans les conteneurs de la librairie standard.

[6] attention : à l'heure actuelle, peu de compilateurs supportent cette fonctionnalité. Pour plus d'info à ce sujet, cf. cette matrice sur le wiki Apache/Stdcxx

[7] une collection est, par essence, une entité. On écarte donc ce cas de la discussion.

[8] une planche-contact est un ensemble de photos (par exemple toutes les photos d'une même pellicule) imprimées de manière à obtenir un aperçu de toutes les photos sur une seule page. Cf. cette définition de wikipedia.

Commentaires

1. Le mercredi, mai 2 2012, 10:59 par alb

Salut,

Je me pose la question de savoir si la sémantique de mouvement est réellement adaptée à la sémantique d'entité. Le problème se pose frontalement dès lors qu'on remarque que la sémantique d'entité est propice à l'héritage (ce qui n'est pas le cas de celle de valeur au passage). Les opérateurs de déplacement vont poser les mêmes problèmes que les opérateurs de copie dans le cadre d'un héritage : slicing et pas de multidispatch. Si la sémantique de mouvement se marie mal avec l'héritage, n'est-ce pas un indicateur qu'à priori la sémantique de mouvement se marie mal avec la sémantique d'entité ? Qui plus est le déplacement peut laisser dans un état 'invalide' l'entité de départ (puisque vidée au profit de la nouvelle entité) même si celle ci a vocation à être rapidement détruite. Ce qui est contradictoire avec ta remarque pertinente qu'une entité est valide ou n'est pas. Peut être vais-je dire une bêtise, mais la sémantique de mouvement n'est elle pas destinée aux sémantiques de valeurs pour lesquelles le coût de la copie est trop important ou pour les enveloppes RAII (efficience par rapport à la copie mais une utilisation plus facile (expl : fabrique de std::unique_ptr)) ?

2. Le mercredi, mai 2 2012, 16:58 par Emmanuel Deloget

Le problème se pose frontalement dès lors qu'on remarque que la sémantique d'entité est propice à l'héritage (ce qui n'est pas le cas de celle de valeur au passage)

Bonne remarque. Je n'ai pas pensé à traiter ce point dans l'article.

Les opérateurs de déplacement vont poser les mêmes problèmes que les opérateurs de copie dans le cadre d'un héritage : slicing et pas de multidispatch. Si la sémantique de mouvement se marie mal avec l'héritage, n'est-ce pas un indicateur qu'à priori la sémantique de mouvement se marie mal avec la sémantique d'entité ?

Hmmm. Vu que j'ai "oublié" de parler d'héritage, je n'ai même pas considéré ce point. C'est vrai que la sémantique de mouvements pose des problèmes à ce niveau.

Peut être vais-je dire une bêtise, mais la sémantique de mouvement n'est elle pas destinée aux sémantiques de valeurs pour lesquelles le coût de la copie est trop important ou pour les enveloppes RAII ?

Et dix de der. J'ai peut-être mis trop de chose dans ma vision du déplacement, et ce "trop de chose" semble se baser sur une réflexion incorrecte.

Le point mérite d'être reconsidéré à tête reposée. Je vais tenter de trouver du temps pour réfléchir à ce que ça implique.

3. Le vendredi, mai 11 2012, 19:05 par alb

(Re)Salut,

Je pense que cela se situe autour de la façon dont on peut caractériser l'unicité (la singularité) d'un objet ayant une sémantique d'entité dans le langage.

Tout ton premier paragraphe Entité et unicité est à mon sens un peu ambigu sur la définition de l'unicité car il ne définit pas comment on 'identifie' un objet et ainsi comment on peut le distinguer d'un autre. Typiquement dans cette phrase :

Car réaliser une copie d'une entité signifierait qu'on crée une entité identique à la première, ce qui contredit le fait que les entités sont toutes différentes l'une de l'autre.

Cette phrase pourrait être comprise comme quoi deux entités ne peuvent avoir le même état interne. Ce qui est faux. Les attributs de l'objet peuvent être identiques sans pour autant qu'on désigne le même objet.

Ce qui pose la question :
Qu'est ce qui caractérise l'unicité d'un objet entité le long de l'exécution du programme et qui n'est pas dans son état interne ?

En C++, je ne vois pas autre chose que l'adresse. C'est donc celle ci qui va me permettre de dire si deux variables (références ou pointeurs) désignent la même entité : uniquement si elles pointent sur la même adresse.

La sémantique de mouvement (au delà du simple problème d'implémentation lié au slicing par ex) pose la question de savoir si ce qui caractérise dans le langage l'unicité d'un objet peut varier au cours de l'exécution (tout en garantissant son unicité). J'ai pas de réponse 'formelle' mais j'ai l'intuition que non. La caractéristique 'identité' d'un objet 'entité' est invariante vu du langage le long de l'exécution du programme. Ou pour être plus précis, je pense que la variation de la propriété d'identité ne peut se faire que 'au delà' du langage. Par exemple, rien n'empêcherait un environnement d'exécution de déplacer un objet et de changer toutes les références existantes vers cette nouvelle adresse (je ne sais pas si la JVM ou .Net le font). Mais je pense que ce relogeabilité ne doit pas pouvoir s'exprimer ni être perçue dans le langage. Ce qui aboutirait logiquement à dire qu'une sémantique de mouvement portée par le langage est incompatible avec une sémantique d'entité.

Qu'en penses-tu ?

++

4. Le mercredi, juin 20 2012, 11:54 par Emmanuel Deloget

Cette phrase pourrait être comprise comme quoi deux entités ne peuvent avoir le même état interne. Ce qui est faux. Les attributs de l'objet peuvent être identiques sans pour autant qu'on désigne le même objet.

La phrase n'est pas très claire, je te l'accorde. Même sans réaliser une copie d'une entité, on peut admettre que deux entités peuvent être en tout point similaire (c'est le "les entités sont différentes, et ce quelque soit par ailleurs leur degré de similitude" plus haut) - sauf leur emplacement dans leur espace d'existence. Une copie, par définition, n'a pas le même emplacement que l'original, si bien que bien qu'elle soit parfaitement similaire à l'original, ce n'est pas l'original quand même (ex: un tableau, dont on ne garderais qu'une partie des caractéristiques physiques ; dans le cadre des caractéristiques prises en compte, un faussaire habile arrivera a créer un tableau exactement similaire au premier - une copie -, mais ce ne sera quand même pas l'original).

Ce qui nous amène naturellement à :

Qu'est ce qui caractérise l'unicité d'un objet entité le long de l'exécution du programme et qui n'est pas dans son état interne ?
En C++, je ne vois pas autre chose que l'adresse. C'est donc celle ci qui va me permettre de dire si deux variables (références ou pointeurs) désignent la même entité : uniquement si elles pointent sur la même adresse.

Très juste.

La sémantique de mouvement (au delà du simple problème d'implémentation lié au slicing par ex) pose la question de savoir si ce qui caractérise dans le langage l'unicité d'un objet peut varier au cours de l'exécution (tout en garantissant son unicité). J'ai pas de réponse 'formelle' mais j'ai l'intuition que non. La caractéristique 'identité' d'un objet 'entité' est invariante vu du langage le long de l'exécution du programme. Ou pour être plus précis, je pense que la variation de la propriété d'identité ne peut se faire que 'au delà' du langage. Par exemple, rien n'empêcherait un environnement d'exécution de déplacer un objet et de changer toutes les références existantes vers cette nouvelle adresse (je ne sais pas si la JVM ou .Net le font). Mais je pense que ce relogeabilité ne doit pas pouvoir s'exprimer ni être perçue dans le langage. Ce qui aboutirait logiquement à dire qu'une sémantique de mouvement portée par le langage est incompatible avec une sémantique d'entité.

En passant, je suis d'accord avec toi : la relogeabilité ne peut faire partie que de l'environnement d'exécution. Jamais, au niveau de l'architecture, on ne doit avoir besoin de dire qu'une entité doit être relogée (des données peuvent l'être, notamment si on travaille soi même sur un projet de garbage collector, mais c'est une toute autre histoire). Au niveau de ce qu'offre le langage choisi, ce qui défini l'unicité d'un objet c'est son emplacement dans son espace d'existence ; il ne peut y avoir deux objets différents au même endroit. En C++, .Net ou Java, on peut raccourcir ça à la référence de l'objet, parce que l'emplacement d'un objet est défini complètement par cette référence et par sa taille (la taille étant fixe pour tous les objets du même type, la référence est donc parfaitement suffisante). Le fait que cette adresse puisse varier (dans le cas de Java ou .Net) ne change pas le raisonnement : ici, l'invariant n'est pas dans le fait que cette référence soit invariante, mais que le couple (objet1,emplacement1) soit à tout moment unique - dans le sens où un autre couple (objet2,emplacement2) identique ne puisse pas exister.

Je vais mettre à jour l'article, en barrant certaines phrases et en renvoyant aux commentaires pour de plus amples informations. C'est ce qui me semble être le plus sage.

5. Le samedi, juin 23 2012, 20:36 par Meseira

Bonjour Emmanuel,

tout d'abord, je souhaite te dire un grand bravo pour ce blog. Les articles sont intéressants et bien écrits. En particulier, je me réfère souvent à ceux relatifs à "Architecture Orientée Objet" dont j'apprécie particulièrement la lecture.

Mon message n'est pas directement relié à un article en particulier. J'aurais aimé savoir si il existe un bon ouvrage de référence traitant des divers principes abordés dans vos articles (OCP, Liskov, ...). Plus précisément, j'aurais souhaité savoir si il existe des exercices pour comprendre la mise en place de ces principes et surtout la façon d'y penser. En effet, pour un petit projet personnel, mettre en oeuvre certains de ces principes est faisable mais j'avoue avoir du mal à voir comment architecturer un plus gros projet "from scratch".

Je me doute que l'expérience est la meilleure source de savoir dans le domaine mais si, par hasard, un bon bouquin pouvait illustrer cela par des exemples et/ou exercices, cela m'intéresserait beaucoup.

Merci et bonne route à ton blog!

6. Le mardi, juin 26 2012, 14:53 par Emmanuel Deloget

Que dire, à part Merci ?

Sinon, sur la question des livres, à part le "Agile software development" de Robert Cecile Martin, je ne vois pas. Le savoir des architectes logiciels est bien disséminé dans un nombre très conséquent de livres :)

Agile Software Development, Principles, Patterns, and Practices

7. Le mercredi, juillet 4 2012, 19:47 par Krypt

Bonjour,

Je poste alors que j'ai à peine commencé l'article, pour vous signaler une faute (je n'ai pas trouvé d'autre endroit) :
Sous-chapitre "Entité et Unicité", première ligne : quelque soit > quel que soit.

(ne pas publier ce message, donc)

8. Le vendredi, juillet 6 2012, 11:10 par Emmanuel Deloget

Si, je publie - et pour une bonne raison : tu as repéré une faute, et il est normal que cela soit dit (en public, vu que c'est un blog public). Pas besoin d'avoir honte d'être meilleur que moi en grammaire ou en orthographe :)

Je m'en vais donc la corriger :) (et je signale qu'il y a la même quelques lignes avant). 

9. Le samedi, juillet 7 2012, 13:16 par Meseira

Bonjour Emmanuel,

merci pour le livre de Robert C. Martin. Je viens de le recevoir (j'ai commandé l'édition internationale de 2011) et, à première vue, ce livre a vraiment l'air super. Beaucoup d'exemples et de nombreux sujets traités. Ce sera ma lecture du week-end, à n'en pas douter!

Merci!

Ajouter un commentaire

Les commentaires peuvent être formatés en utilisant une syntaxe wiki simplifiée.

Fil des commentaires de ce billet