Dot.Blog

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

Visual NDepend un outil d'analyse de code original et puissant

A partir d'une certaine taille le code d'une application devient difficile à maintenir même s'il est bien conçu. De même, la maintenance évolutive oblige souvent à mettre les mains dans un code écrit par d'autres (si ce code est assez ancien, on peut s'y perdre même en en étant l'auteur d'ailleurs !). Auditer un code est ainsi un besoin fréquent dans le travail de tous les jours d'un développeur. Si on effectue des missions d'audit ce besoin devient alors impérieux car il est nécessaire d'entrer rapidement dans un code "étranger" et toujours complexe (je n'ai jamais été appelé en audit pour des logiciels de 20 lignes...). Les outils d'analyse de code sont ainsi des compagnons indispensables. Hélas ils ne sont pas si nombreux que ça et comme tout ce qui touche la qualité, ils sont un peu "oubliés". Je voulais ainsi vous présenter un excellent outil : Visual NDepend. NDepend est puissant, il permet de construire et visualiser des comparaisons, d'auditer la qualité du code, et surtout de mieux visualiser la structure d'une application. Le tout quantitavement (métriques), qualitativement (l'information est pertinente) et graphiquement (le logiciel propose de nombreux diagrammes et schémas permettant de visualiser ce qui est invisible en lisant le code source : structures, dépendances, ...). NDepend ne se limite pas seulement à l'audit de code, c'est aussi un excellent outil de refactoring. On notera aussi, côté puissance, l'intégration d'un langage d'interrogation, le CQL (Code Query Language), permettant à la façon de SQL de "questionner" un projet et son code comme s'il s'agissait d'une base de données. La comparaison s'arrête là avec SQL car CQL "comprend" le code alors que SQL ne comprend pas le sens des données qu'il manipule. Cette grande différence confère à NDepend une très grande souplesse et beaucoup d'intelligence. Par exemple il est très simple de faire des requêtes du type "montre moi toutes les méthodes privées dont le nombre de lignes de code est supérieur à 20", très utile pour repérer rapidement dans un projet les lourdeurs ou la nécessité de refactoring. Une telle interrogation se formule en la requête CQL suivante "SELECT METHODS WHERE NbLinesOfCode > 20 AND IsPrivate". On peut bien entendu créer des requêtes plus sophistiquées comme rechercher toutes les types qui sont une définition de classe et qui implémentent telle ou telle autre interface ! NDepend est ainsi un outil de grande qualité dont on ne peut que conseiller l'utilisation. Réalisé par Patrick Smacchia, un acteur connu de la scène .NET et Microsoft MVP C#, NDepend est vendu 299 euros en licence 1 utilisateur, c'est à dire pas grand chose au regard des immenses services qu'il peut rendre. Il y a beaucoup de choses à dire sur cet outil et surtout à voir. Le mieux si vous êtes intéressés est de vous rendre sur le site du logiciel www.ndepend.com où vous pourrez visionner de nombreuses petites vidéos qui vaudront mieux que de longs discours. Bon refactoring !  

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)