Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

Contourner le problème de l'appel d'une méthode virtuelle dans un constructeur

Ce problème est source de bogues bien sournois. J'ai déjà eu l'occasion de vous en parler dans un billet cet été (Appel d'un membre virtuel dans le constructeur ou "quand C# devient vicieux". A lire absolument...), la conclusion était qu'il ne faut tout simplement pas utiliser cette construction dangereuse. Je proposais alors de déplacer toutes les initialisations dans le constructeur de la classe parent, mais bien entendu cela n'est pas applicable tout le temps (sinon à quoi servirait l'héritage).

Dès lors comment proposer une méthode fiable et systématique pour contourner le problème proprement ?

Rappel

Je renvoie le lecteur au billet que j'évoque en introduction pour éviter de me répéter, le problème posé y est clairement démontré. Pour les paresseux du clic, voici en gros le résumé de la situation : L'appel d'une méthode virtuelle dans le constructeur d'une classe est fortement déconseillé. La raison : lorsque qu'une instance d'une classe dérivée est créée elle commence par appeler le constructeur de son parent (et ainsi de suite en cascade remontante). Si ce constructeur parent appelle une méthode virtuelle overridée dans la classe enfant, le problème est que l'instance enfant elle-même n'est pas encore initialisée, l'initialisation se trouvant encore dans le code du constructeur parent. Et cela, comme vous pouvez l'imaginer, ça sent le bug !

Une première solution

La seule et unique solution propre est donc de s'interdire d'appeler des méthodes virtuelles dans un constructeur. Et je serai même plus extrémiste : il faut s'interdire d'appeler toute méthode dans le constructeur d'une classe, tout simplement parce qu'une méthode non virtuelle, allez savoir, peut, au gréé des changements d'un code, devenir virtuelle un jour. Ce n'est pas quelque chose de souhaitable d'un pur point de vue méthodologique, mais nous savons tous qu'entre la théorie et la pratique il y a un monde...

Tout cela est bien joli mais s'il y a des appels à des méthodes c'est qu'elles servent à quelque chose, s'en passer totalement semble pour le coup tellement extrême qu'on se demande si ça vaut encore le coup de développper ! Bien entendu il existe une façon de contourner le problème : il suffit de créer une méthode publique "Init()" qui elle peut faire ce qu'elle veut. Charge à celui qui créé l'instance d'appeler dans la foulée cette dernière pour compléter l'initialisation de l'objet.

Le code suivant montre une telle construction :

 // Classe Parent
    public class Parent2
    {
        public Parent2(int valeur)
        {
            // MethodeVirtuelle();
        }
 
        public virtual void Init()
        {
            MethodeVirtuelle();
        }
 
        public virtual void MethodeVirtuelle()
        { }
    }
 
    // Classe dérivée
    public class Enfant2 : Parent2
    {
        private int val;
 
        public Enfant2(int valeur)
            : base(valeur)
        {
            val = valeur;
        }
 
        public override void MethodeVirtuelle()
        {
            Console.WriteLine("Classe Enfant2. champ val = " + val);
        }
    }

La méthode virtuelle est appelée dans Init() et le constructeur de la classe de base n'appelle plus aucune méthode.

C'est bien. Mais cela complique un peu l'utilisation des classes. En effet, désormais, pour créer une instance de la classe Enfant2 il faut procéder comme suit :

 // Méthode 2 : avec init séparé
 var enfant2 = new Enfant2(10);
 enfant2.Init(); // affichera 10

Et là, même si nous avons réglé un problème de conception essentiel, côté pratique nous sommes loin du compte ! Le pire c'est bien entendu que nous obligeons les utilisateurs de la classe Enfant2 à "penser à appeler Init()". Ce n'est pas tant l'appel à Init() qui est gênant que le fait qu'il faut penser à le faire ... Et nous savons tous que plus il y a de détails de ce genre à se souvenir pour faire marcher un code, plus le risque de bug augmente.

Conceptuellement, c'est propre, au niveau design c'est à fuir...

Faut-il donc choisir entre peste et choléra sans aucun espoir de se sortir de cette triste alternative ? Non. Nous pouvons faire un peu mieux et rendre tout cela transparent notamment en transférant à la classe enfant la responsabilité de s'initialiser correctement sans que l'utilisateur de cette classe ne soit obligé de penser à quoi que ce soit.

La méthode de la Factory

Il faut absolument utiliser la méthode de l'init séparé, cela est incontournable. Mais il faut tout aussi fermement éviter de rendre l'utilisation de la classe source de bugs. Voici nos contraintes, il va falloir faire avec.

La solution consiste à modifier légèrement l'approche. Nous allons fournir une méthode de classe (méthode statique) permettant de créer des instances de la classe Enfant2, charge à cette méthode appartenant à Enfant2 de faire l'intialisation correctement. Et pour éviter toute "bavure" nous allons cacher le constructeur de Enfant2. Dès lors nous aurons mis en place une Factory (très simple) capable de fabriquer des instances de Enfant2 correctement initialisées, en une seule opération et dans le respect du non appel des méthodes virtuelles dans les constructeurs... ouf !

C'est cette solution que montre le code qui suit (Parent3 et Enfant3 étant les nouvelles classes) :

    // Classe Parent
    public class Parent3
    {
        public Parent3(int valeur)
        {
            // MethodeVirtuelle();
        }
 
        public virtual void Init()
        {
            MethodeVirtuelle();
        }
 
        public virtual void MethodeVirtuelle()
        { }
    }
 
    // Classe dérivée
    public class Enfant3 : Parent3
    {
        private int val;
 
        public static Enfant3 CreerInstance(int valeur)
        {
            var enfant3 = new Enfant3(valeur);
            enfant3.Init();
            return enfant3;
        }
 
        protected Enfant3(int valeur)
            : base(valeur)
        {
            val = valeur;
        }
 
        public override void MethodeVirtuelle()
        {
            Console.WriteLine("Classe Enfant3. champ val = " + val);
        }
    }

La création d'une instance de Enfant3 s'effectue dès lors comme suit :

 var enfant3 = Enfant3.CreerInstance(10);

C'est simple, sans risque d'erreur (impossible de créer une instance autrement), et nous respectons l'interdiction des appels virtuels dans le constructeur sans nous priver des méthodes virtuelles lors de l'initialisation d'un objet. De plus la responsabilité de la totalité de l'action est transférée à la classe enfant ce qui centralise toute la connaissance de cette dernière en un seul point.

Dans une grosse librairie de code on peut se permettre de déconnecter la Factory des classes en proposant directement une ou plusieurs abstraites qui sont les seuls points d'accès pour créer des instances. Toutefois je vous conseille de laisser malgré tout les Factory "locales" dans chaque classe. Cela évite d'éparpiller le code et si un jour une classe enfant est modifiée au point que son initialisation le soit aussi, il n'y aura pas à penser à faire des vérifications dans le code de la Factory séparée. De fait une Factory centrale ne peut être vue que comme un moyen de regrouper les Factories locales, sans pour autant se passer de ces dernières ou en modifier le rôle.

Conclusion

Peut-on aller encore plus loin ? Peut-on procéder d'une autre façon pour satisfaire toutes les exigences de la situation ? Je ne doute pas qu'une autre voie puisse exister, pourquoi pas plus élégante. Encore faut-il la découvrir. C'est comme en montagne, une fois qu'une voie a été découverte pour atteindre le sommet plus facilement ça semble une évidence, mais combien ont du renoncer au sommet avant que le découvreur de la fameuse voie ne trace le chemin ?

Saurez-vous être ce premier de cordée génial et découvrir une voie alternative à la solution de la Factory ? Si tel est le cas, n'oubliez pas de laisser un commentaire !

Dans tous les cas : Stay Tuned !

Et pour jouer avec le code, le projet de test VS 2008 : VirtualInCtor.zip (5,99 kb)

Le Mythe du StringBuilder

Sur l'excellent blog du non moins excellent Mitsu, dans l'une de mes interventions sur l'un de ses (excellents aussi) petits quizz LINQ, j'aurai parlé du "Mythe du StringBuilder". Cela semble avoir choqué certains lecteurs qui m'en ont fait part directement. Le sujet est très intéressant et loin de toute polémique j'en profiterais donc ici pour préciser le fond de cet avis sur le StringBuilder et aussi corriger ce qui semble être une erreur de lecture (trop rapide?) de la part de ces lecteurs. J'ai même reçu un petit topo sur les avantages de StringBuilder (un bench intéressant par ailleurs).

D'abord, plantons le décor

Le billet de Mitsu dans lequel je suis intervenu se trouve ici. Dans cet échange nous parlions en fait de la gestion des Exceptions que l'un des intervenants disait ne pas apprécier en raison de leur impact négatif en terme de performance. Et c'est là que j'ai répondu :

"...Dans la réalité je n'ai jamais vu une application (java, delphi ou C#) "ramer" à cause d'une gestion d'exception, c'est un mythe à mon sens du même ordre que celui qui veut que sous .NET il faut systématiquement utiliser un StringBuilder pour faire une concaténation de chaînes."

Comme on le voit ici, seule une lecture un peu trop rapide a pu faire croire à certains lecteurs que je taxe les bénéfices du StringBuilder de "mythe". Je pensais que la phrase était assez claire et qu'il était évident que si mythe il y a, c'est dans le bénéfice systématique du StringBuilder... Nuance. Grosse nuance.

Mythe ou pas ?

La gestion des exceptions est un outil fantastique "légèrement" plus sophistiqué que le "ON ERROR GOTO" de mon premier Basic Microsoft interprété sous CP/M il y a bien longtemps, mais le principe est le même. Et si en 25 ans d'évolution, la sélection darwinienne a conservé le principe malgré le bond technologique c'est que c'est utile...

Est-ce coûteux ?

Je répondrais que la question n'a pas de sens... Car quel est le référenciel ?

Les développeurs ont toujours tendance à se focaliser sur le code, voire à s'enfermer dans les comparaisons en millisecondes et en octets oubliant que leur code s'exprime dans un ensemble plus vaste qui est une application complète et que celle-ci, pour être professionnelle a des impératifs très éloignés de ce genre de débat très techniques. Notamment, une application professionnelle se doit avant tout de répondre à 3 critères : Répondre au cachier des charges, avoir un code lisible, être maintenable. De fait, si les performances ne doivent pas être négligées pour autant, le "coût" d'une syntaxe, de l'emploi d'un code ou d'un autre, doit être "pesé" à l'aulne d'un ensemble de critères où celui de la pure performance ne joue qu'un rôle accessoire dans la grande majorité des applications. Non pas que les performances ne comptent pas, mais plutôt que face à certaines "optimisations" plus ou moins obscures, on préfèrera toujours un code moins optimisé s'il est plus maintenable et plus lisible.

Dès lors, le "coût" de l'utilisation des exceptions doit être évalué au regard de l'ensemble de ces critères où les millisecondes ne sont pas l'argument essentiel.

De la bonne mesure

Tout est affaire de bonne mesure dans la vie et cela reste vrai en informatique.

Si une portion de code effectue une boucle de plusieurs milliers de passages, et si ce traitement doit absolument être optimisé à la milliseconde près, alors, en effet, il sera préférable d'éviter une gestion d'exception au profit d'un ou deux tests supplémentaires (tester une valeur à zéro avant de s'en servir pour diviser au lieu de mettre la division dans un bloc Try/Catch par exemple).

Mais comme on le voit ici, il y plusieurs "si" à tout cela. Et comment juger dans l'absolu si on se trouve dans le "bon cas" ou le "mauvais cas" ? Loin de la zone frontière, aux extrèmes, il est toujours facile de décider : une boucle de dix millions de passages avec une division ira plus vite avec un test sur le diviseur qu'avec un Try/Catch (si l'exception est souvent lancée, encore un gros "si" !). De même, un Try/Catch pontuel sur un chargement de document sur lequel l'utilisateur va ensuite travailler de longues minutes n'aura absolument aucun impact sur les performances globales du logiciel.

Mais lorsqu'on approche de la "frontière", c'est là que commence la polémique, chacun pouvant argumenter en se plaçant du point de vue performance ou du point de vue qualité globale du logiciel. Et souvent chacun aura raison sans arriver à le faire admettre à l'autre, bien entendu (un informaticien ne change pas d'avis comme ça :-) ) !

Où est alors la "bonne mesure" ? ... La meilleure mesure dans l'existence c'est vous et votre intelligence. Votre libre arbitre. Il n'y a donc aucun "dogme" qui puisse tenir ni aucune "pensée unique" à laquelle vous devez vous plier. A vous de juger, selon chaque cas particulier si une gestion d'exception est "coûteuse" ou non. N'écoutez pas ceux qui voudraient que vous en mettiez partout, mais n'écoutez pas non plus ceux qui les diabolisent !

Et le StringBuilder dans tout ça ?

Si dans mon intervention sur le blog de Mitsu j'associais gestion des exceptions et StringBuilder c'est parce qu'on peut en dire exactement les mêmes choses !

Dans les cas extrêmes de grosses boucles il est fort simple de voir que le StringBuilder est bien plus performant qu'une concaténation de chaînes avec "+". Cela ne se discute même pas.

Mais, comme pour la gestion des exceptions, c'est lorsqu'on arrive aux "frontières" que la polémique pointe son nez. Pour concaténer quelques chaînes le "+" est toujours une solution acceptable car le StringBuilder a un coût, celui de son instanciation et du code qu'il faut écrire pour le gérer. Il s'agit là des cas les plus fréquents. On concatène bien plus souvent quelques chaines de caractères dans un soft qu'on écrit des boucles pour en concaténer 1 million le tout dans un traitement time critical... (encore des tas de "si" !).

Même du point de vue de la mémoire les choses ne sont pas si simple. Le StringBuilder utilise un buffer qu'il double quand sa capacité est dépassée. Dans certains cas courants (petites boucles dans lesquelles il y a quelques concaténation), le stress mémoire infligé par le StringBuilder peut être très supérieur à celui de l'utilisation de "+" ou de String.Concat.

C'est dans ces cas les plus fréquents que l'utilisation du StringBuilder comme panacée apparaît n'être qu'un mythe.

Conclusion 

Vous et moi écrivons du code, ce code se destine à un client / utilisateur. La première exigence de ce dernier est que le logiciel réponde au cahier des charges et qu'il fonctionne sans bug gênant. Cela impose de votre côté que vous utilisiez une "stylistique" rendant le code lisible et facilement maintenable car un code sans bug n'existe pas et qu'il faudra tôt ou tard intervenir ici ou là. Dans un tel contexte réaliste, ce ne sont pas les millisecondes qui comptent ni les octets, mais bien la qualité globale de l'application. Et c'est alors que les dogmes techniques tombent. Car ils n'ont d'existence que dans un idéal purement technique qui fait abstraction de la réalité. Un Try/Catch ou un StringBuilder ne peuvent pas être étudiés en tant que tels en oubliant les conditions plus vastes de l'application où ils apparaissent. Leur impact n'a de sens que compris comme l'une des milliers d'autres lignes de code d'une application qui doit répondre à un autre critère, celui de la qualité et de la maintenabilité.

Un seul juge existe pour trancher car chaque cas est particulier : vous.

Pour ceux qui veulent jouer un peu avec les StringBuilder, voici un mini projet que vous n'aurez qu'à modifier pour changer les conditions de tests : ConsoleApplication2.zip (6,10 kb)

Un livre à avoir : "Framework Design Guidelines"

J'achète assez peu de livres en général, en effet dès qu'on s'intéresse à ce qui est nouveau tous les livres potentiellement intéressants sont "à paraître", et quand ils paraissent l'info est déjà réchauffée par rapport à ce qu'on peut trouver des mois avant sur Internet. Bien entendu cela implique d'aller chercher l'information brute, éparpillée et en anglais. C'est pour cela que les ouvrages techniques en français trouvent toute leur utilité en offrant une information structurée, vérifiée et traduite pour tous ceux qui ne lisent pas l'anglais.

Toutefois il m'arrive d'acheter certains livres lorsqu'ils traitent de sujet plus "intemporels" comme la méthodologie ou ici le framework .NET.

Oui, ce livre est en anglais, et à votre prochaine question la réponse est non, je n'en connais pas de traduction en français ce qui est de toute façon rare dans ce domaine. L'exemple récent de la fermeture de l'antenne française de O'Reilly illustre la difficulté globale de vendre du livre technique en France malgré un catalogue attrayant. Quant à traduire un ouvrage et donc réengager des frais importants pour un public très restreint, fort peu s'y sont risqués ou s'y risquent. Dans mes propres "development guidlines", en position 1 je mettrais "faites du C#", en 2 "faites du LINQ", mais en position 0 "faites de l'anglais" !.

Framework Design Guidelines

Ce livre traite des bonnes méthodes à mettre en oeuvre pour développer des applications respectueuses du framework .NET. Ecrit par deux membres de l'équipe de développement de .NET il a aussi été relu et corrigé par d'autres personnes de cette équipe qui, chose originale, font apparaître ici et là des petits encadrés où ils apportent leur point de vue sur ce que les auteurs viennent d'écrire. C'est une approche intéressante qui rend le contenu plus vivant. L'avant propos est d'ailleurs signé par Anders Hejlsberg, difficle de trouver mieux...

Ce n'est pas un livre très récent, il a du paraître en 2006 la première fois, mais son contenu est "intemporel" dans le sens où il dépasse les modes ou les dernières options du framework pour parler de son coeur, ses API, sa structure et la façon de bien coder dans cet environnement.

Le contenu

Le sous titre est finalement clair "Conventions, Idioms and Patterns for Reusable .NET Libraries". Conventions, idiomes et patterns pour des bibliothèque de codes réutilisables.

La première partie traite des "qualités d'un framework bien conçu" et on entre un peu dans les coulisses de .NET, comment et pourquoi certains choix ont été arretés. Une façon agréable, au delà du côté anecdotique, d'aborder les qualités générales que se doit posséder toute bonne bibliothèque de code, comme celles que nous sommes amenées parfois à écrire.

Les fondamentaux du framework sont ensuite abordés puis on trouve les guidelines proprement dites. Stratégie de nommage, le choix entre classe ou structure, entre classe abstraite ou interface, la conception des membres d'une classe (propriétés, méthodes, champs...) tout cela est traité avec intelligence, à propos, et permet d'affiner ses propres vues sur ces questions ou parfois même de s'interroger sur des sujets dont on pensait avoir fait le tour.

Les exceptions sont traitées en détail, toujours sous l'angle de la meilleure mise en oeuvre possible plus que sur le code ou la syntaxe. Ce livre est un moyen terme entre théorie, design pattern, expérience de développeurs ayant écrit l'un des plus gros framework existant, sagesse et doutes que tout informaticien conscient de l'importante de ses choix se doit d'avoir.

Un final sur l'utilisation de FxCop, son évolution, comment il marche et comment en tirer le meilleur est tout à fait appréciable. Je parlais justement dans un dernier billet de la "sous utilisation" de cet outil fanstastique par la majorité des développeurs que je rencontre ou que je forme. Ce chapitre permettra peut-être aux lecteurs de ce livre de faire plus souvent usage de FxCop.

Le CD

Le livre est accompagné d'un CD contenant des exemples de code et plusieurs vidéo de conférences (en anglais aussi, ne rêvez pas :-) ), toutes particulièrement intéressantes et portant sur des sujets aussi variés que le garbage collector (et tout ce qui tourne autour de la libération de la mémoire et des ressources), les performances ou le packaging et le déploiement. Une vraie mine d'information, pas forcément récentes, comme le livre lui-même, mais qui abordent des fondamentaux parfois négligés.

Conclusion

Comme je le disais, ce livre est intemporel, en tout cas tant que vivra le framework .NET sous la forme que nous connaissons depuis plusieurs années maintenant. Il permet d'entrer un peu dans l'équipe de développement de cette plateforme, de comprendre les choix qui ont présidé à sa mise en oeuvre et surtout il permet, par cette vision "de l'intérieur" et les nombreux conseils qu'il contient de mieux concevoir son propre code, de mieux trancher entre certaines options parfois trop nébuleuses (classe ou interface, classe ou structure, par exemple) en sachant avec précision pourquoi on fera ou non tel ou tel choix.

C'est un livre plaisant à lire, qu'on peut "consommer" en piochant les chapitres, pas forcément dans l'ordre du livre. Un "must have" pour qui développe sous .NET, c'est une évidence.

Références

Framework Design Guidelines

Krzysztof Cwalina (au scrabble si vous le placez vous mettez à genou vos adversaires !)
Brad abrams

0-321-24675-6

Edité par Addison-Wesley

Une bonne librarie technique en ligne

Vous pouvez acheter ce livre dans toutes les bonnes librairies, toutefois si vous voulez le payer le moins cher possible, je vous conseille de le commander directement à "The Book Depository Ltd", une librairies anglaise qui livre vite et bien qui affiche des prix en général très bas. Leur site Internet : http://www.bookdepository.co.uk/WEBSITE/WWW/WEBPAGES/homepage.php

C'est un vendeur qu'utilise Amazon, mais si vous passez en direct c'est généralement moins cher...

Bonne lecture et Stay Tuned !

TDD utopie ou géniale avancée ?

Perdu dans mes pensées lors d'une pause amplement méritée je pensais à la méthode TDD et j'ai eu envie de vous livrer le fruit de mes réflexions. Après tout, bloguer c'est ça aussi.
...C'est sûr je vais passer pour un ET, aucun être humain normalement constitué n'irait penser à de telles choses pendant une pause. Mais j'assume !

Le Test-Driven Development ou développement piloté par les tests, n'est pas réellement un sujet récent, cette méthodologie est dérivée du concept "tester en premier" de l'Extreme programming, lui même une forme dérivée de l'Agile Software Development datant de 1996, dans la même veine que Scrum ou RAD en 1991, ce qui ne nous rajeunit pas.
Tout cela pouvant remonter certainement au néolithique de proche en proche. Par exemple taper sur la tête de son voisin avec la massue toute neuve qu'on vient de fabriquer histoire de tester si elle sera assez solide pour aller à la chasse était certainement le grand début de l'Agile ou piquer un silex tout taillé à un copain est finalement le début de la réutilisation voire du RAD. Dieu lui-même en reprenant les plans de fabrication et une côte d'Adam pour faire Eve faisait de la réutilisation et du design pattern. Confondre grand ordonnateur et grand ordinateur est un exercice de style osé, j'en conviens.

Bref le TDD, comme toute méthodologie, n'est que le fruit d'une lente maturation au fil des ans. Et parmi toutes celles proposées ces 20 dernières années c'est celle qui apparaît la plus sage, la mieux adaptée. Elle recentre le développement sur l'essentiel : le besoin réel et donc l'utilisateur. Techniquement elle prône une économie de mouvement évitant les errances : on ne code que ce qui est nécessaire pour que ça marche et surtout pas plus. On s'éloigne ainsi des grandes généralisation, fantasme d'informaticien, donnant lieu à des développements complexes et coûteux qui, une fois terminés, sont déjà obsolètes.

Mais est-ce une nouvelle utopie et subira-t-elle le même sort que toutes les autres méthodes de développement (à savoir se limiter à quelques termes pompeux sur les propositions commerciales et à quelques discussions autour des machines à café) ?

UML est un bon exemple du triste sort réservé aux meilleures idées de nos têtes pensantes. Même si cette mode tend à passer (remplacer par d'autres) il fut un moment ou toute "propal" se devait de faire apparaître ce terme comme toute lessive se doit au minimum d'avoir la mention "deux en un" (voire plus) sur son emballage pour se vendre.

Avant que d'excellentes méthodes comme TDD puissent s'imposer, il sera donc nécessaire de changer les mentalités, celles des dirigeants de SSII et de leurs commerciaux qui se retranchent souvent derrière la loi du marché et de la concurrence pour offrir au client ce qu'il demande au lieu de lui conseiller ce dont il a besoin, et celle des clients qui ne peuvent continuer à se dédouaner de leur ignorance s'ils désirent obtenir des logiciels fiables, maintenables et adaptés à leurs besoins et ce au meilleur prix (mais pas en dessous, la qualité se paye). 

Un long voyage nous attend donc avant qu'un tel bouleversement se produise, et TDD, comme l'XP ou l'Agile development et bien d'autres dont nous avons tous oublié les noms, finira peut-être aussi dans les nymbes brumeuses où s'évanouïssent les idées trop en avance sur les mentalités...

En attendant, essayez de développer selon TDD :  vous y gagnerez en efficace et en qualité.
Visual Studio se plie fort bien au Unit Testing, mais beaucoup moins bien au TDD. Il est fort simple de créer un test pour une méthode existante mais pas l'inverse, base du TDD. Néanmois, avec un peu d'imagination, il est tout à fait possible de créer un squelette de classe, d'y ajouter une méthode ne faisant rien puis de générer le test unitaire pour enfin revenir sur l'implémentation de la méthode. Il n'y a pas de temps perdu, il n'y a qu'à changer un peu l'ordre dans lequel les choses sont faites.

Essayez, vous verrez, c'est étonnant comme le code final d'une classe est assez différent de celui qu'il aurait été créé par la méthode "classique".

Happy coding !