Pour éviter cela, la solution est très simple:
J'écris mon programme en Fortran 77. J'introduis des lignes de commentaires. Les unes sont les "titres" des différentes parties du programmes; d'autres indiquent la signification de chaque variable. Dix ans plus tard mon programme est toujours compréhensible et utilisable.
Un exemple: les routines de la bibliothèque LINPACK, qui sont vieilles d'environ 35 ans.
Jean-Marc Blanc
Bonjour,
Je pense que l'on devrait aussi se demander pourquoi notre code n'est-il pas compréhensible aux autres. Je propose quelques réflexions:
- Rien (ou très peu) ne nous pousse à écrire du code clair : il n'y a généralement pas de révision de notre code par des collègues avant livraison au client, le client n'est pas prêt à payer un peu plus pour le développement initial dans l'espoir d'économiser des frais de maintenance, les délais d'exécutions sont généralement très court.
- Faire simple demande des efforts particuliers car notre esprit nous propose naturellement d'abord des solutions complexes.
- La complexité de nos réalisations peut s'avérer grisante et nous donner des l'importance : si les collègues ne comprennent rien à mon code, c'est bien la preuve que je leur suis supérieur!
- Nous voulons parfois trop bien faire : réaliser des indirections pour des futurs changements hypothétiques diminue grandement la compréhension immédiate de notre code, l'usage des "design patterns" ou autres "bonnes pratiques" va très souvent dans ce sens.
- Etant donné les changements incessants de technologie, nous sommes amenés à developper avec des languages, outils et techniques que nous ne maîtrisons pas complètement.
- L'expérience moyenne dans le développement est souvent trop courte pour acquérir suffisamment de maturité pour bien programmer. Le statut de programmeur n'est pas suffisamment valorisé pour que cela soit intéressant de faire une longue carrière dans cette fonction; il est bien plus valorisant de se dirriger vers une progression hiérarchique ou vers des tâches mieux appréciées comme l'analyse-métier ou le design technique. Quand on voit des personnes qualifiées de "sénior" après 5 ans d'expérience, cela laisse à réfléchir.
- Le pragmatisme et la modestie sont des qualités rares dans le métier.
- Les personnes qui vont devoir lire notre code vont très souvent devoir le faire pour répondre à des urgences (investigations ou "fixing" de disfonctionnements), évolutions à faire en urgence, ré-utilisation, ...
Merci de cette discussion bien intéressante,
André
grinchisator a commencé, mais reste un peu avare et lapidaire. Je vais donc essayer d'apporter ma pierre à l'édifice.
Je rajouterai qu'il ne faut pas que le modèle du problème et celui de la solution, mais aussi celui du processus qui permet de passer de l'un à l'autre, car c'est celui-ci qui montre que la solution en est effectivement une pour le problème considéré. Comme grinchisator l'a mentionné, c'est une question d'analyse : d'abord on comprend, ensuite on met en pratique, sinon on est quasiment sûr de faire des bétises. Là où est la difficulté (ça ne surprendra personne) est que ce n'est pas toujours évident : on peut faire face au problème pour la première fois, ne pas connaître de solution efficace, ou tout simplement avoir du mal à saisir les différents composant qui entrent en jeu et leurs interactions. Tout cela rend la modélisation difficile, et il peut être nécessaire dans ces cas là d'y aller par itération, en modelant une partie, puis tenter de résoudre le problème à partir de là, voir ce qui manque, modifier le modèle, etc.
Une erreur fondamentale en revanche, c'est que beaucoup pronent l'impossibilité de faire tout bon d'un coup parce que personne n'a la science infuse, ce qui est vrai, mais plutôt que de partir sur un processus itératif qui consiste à admettre que les modèles peuvent être revus à n'importe quel moment (tant que la solution n'est pas clairement identifiée), ils partent sur un processus "jusqu'à rentrer dans le mur" où on décide des modèles au début et on ne les remet en cause que quand on n'a plus le choix. Le problème étant que quand on n'a plus le choix, c'est souvent trop tard et on est bon pour tout refaire, ce qui n'est jamais motivant (donc attention au YAGNI). En bref, on a beau connaître nos limites, l'égo est encore là pour les minimiser.
souviron34 l'a mentionné : on ne peut jamais garantir que notre logique soit la même que celle de notre voisin. Les modèles mentaux de celui qui reprend un code n'ont donc pas de raison d'être les même que ceux de celui qui l'a créé. Par contre, en sachant qu'on part d'une base commune (le langage de programmation) il est tout à fait possible de rendre un modèle, disont une classe, suffisamment claire pour qu'on n'ait aucun doute sur les interactions qu'elle a avec telle ou telle autre classe. Pour celui qui reprend le code, la question n'est pas de savoir s'il est d'accord avec le modèle utilisé, ou si celui-ci lui semble plus naturel qu'un autre, mais de savoir s'il est capable de comprendre ce qui est formellement écrit, et donc de lui donner du sens dès sa première lecture. Si ce n'est pas le cas, c'est soit que des parties nécessaires à la compréhension sont éparpillées ailleurs, auquel cas le code est mal organisé, soit que les noms ne sont pas clairement reliés au contexte d'utilisation, auquels cas soit on renome (si c'est dédié au cas qui nous intéresse), soit on encapsule (si ça vient d'ailleurs, de façon à leur donner un nom plus en phase avec le contexte, exemple en fin de post).
On peut effectivement concevoir une chose de 1001 façons, le tout étant de ne pas mélanger les valeurs personnelles (habitudes et conventions) et pros (quels problèmes sont résolus et quels sont ceux qu'il reste à résoudre).
Un peu fourre-tout : d'un côté on a le côté factorisation avec les fonctions standards et bibliothèques, où le principe est effectivement de réutiliser sans refaire, et de l'autre les design patterns, qui eux visent à refaire pareil, on n'est donc pas sur le même plan. Si je devais généraliser en partant de ces deux points, je comprends qu'il parle ici d'être répétitif, tout simplement, de savoir "réutiliser" au sens large (pas seulement des briques logicielles, mais ausi des idées et habitudes) de façon à garder une logique de codage homogène. Et homogène de manière large en considérant que la réutilisation de fonctions existantes permet de rester homogène entre différents programmes. Le fait d'être répétitif permet de comprendre plus facilement le code car on s'appuie sur des modèles déjà vus.
Là je suis plus sceptique. S'il parle vraiment "d’un ensemble complet de cas de tests", on est tous d'accord que dès lors qu'on sort du Hello World ce n'est généralement pas faisable. En particulier, quand on pense à une lib générique, on n'a pas vocation à imaginer et tester tous les cas possibles. Un ensemble de test vérifiant quelques exemples triviaux + les cas limites est un minimum. Si on est rigoureux dans la conception et l'implémentation (aussi difficile que ça puisse être), on aura rarement besoin de plus. Du reste, on attendra soigneusement d'avoir des cas concrets où ça ne marche pas pour enrichir les tests en conséquence, de façon à ne pas perdre l'information. Mais si cela est dû à une utilisation obscure, il ne faudra pas uniquement se pencher sur la validité du code utilisé, mais aussi sur la validité de son utilisation : est-ce que c'est bien ce bout de code qu'on est censé utiliser là ?
C'est discutable, de par la notion d'encapsulation que je mentionne au dessus. Certaines libs sont faites pour appliquer des algos particulier de manière générique (sans considérer des contextes d'applications précis). Ces algos/méthodes n'ont pas besoin des concepts venant du contexte d'utilisation, ils définissent les leurs et c'est à la charge de l'utilisateur de faire le mapping. On ne peut pas exiger des ces implémentations qu'elles affichent clairement les concepts qui nous intéressent (sinon on contredit la notion de réutilisation précédente). Par contre, il est important d'intégrer la lib de manière à pouvoir l'utiliser en manipulant des concepts qui nous sont familiers, d'où le besoin d'encapsulation qui revient à cacher les méthodes génériques dans une classe dédiée à notre contexte, et qui utilise donc des concepts qu'on connait (exemple en fin de post). À ce titre, et pour faire le lien avec le point précédent, même si la lib est très bien testé, on s'assurera de tester notre propre classe, parce que celle-ci dispose de ses propres spécifications et rien n'assure que la manière dont on utilise la lib corresponde bel et bien à nos besoins. Et si les tests de la lib sont verts mais ceux de notre classe dédiée sont rouges, c'est probablemnent parce que le mapping n'est pas correcte (mauvaise utilisation de la lib ou carrément mauvaise lib).
Au passage, et pour répondre à souviron34, c'est grace à ce genre d'encapsulation qu'on peut comprendre/modéliser/maintenir un module sans avoir besoin de comprendre comment il est utilisé (et donc sans avoir besoin d'avoir une compréhension globale). C'est une question de responsabilité : ce qu'est censé faire le module est défini dans le scope du module. Si ça ne colle pas à notre utilisation, c'est qu'on l'utilise mal ou qu'on n'utilise pas le bon, et non pas que le module est foireux. C'est un choix de gestion des responsabilités. Si on s'autorise à dire que tel module est foireux parce que l'utiliser dans tel contexte ne marche pas, alors les responsabilités sont partagées et on ne sait plus qui définit quoi. Dans ce genre de situation, il ne faut pas s'étonner d'arriver à des cas incohérent où on ne sait plus qui est censé faire quoi.
Un algorithme n'est pas un code source, c'est une description d'un processus qui peut rester plus ou moins abstrait. Implémenter un algorithme dans différentes situations peut se traduire par différents codes sources, mais cela reste néanmoins le même algorithme. Et en allant plus loin, même si on implémente une fonction complexe pour laquelle on n'a jamais appris d'algorithme particulier, on peut la découper en différentes étapes simples, étapes pour lesquelles on utilisera des algorithmes simples et connus, rejoignant l'idée de "choisir des algorithmes existants dans la bonne combinaison pour résoudre un problème". Si on prend cette perspective, je suis plutôt d'accord avec le dernier point, dans le sens où quand on implémente quelque chose, on réutilise des algos qu'on connait déjà ou qui nous paraissent naturels, on ne cherche pas à faire quelque chose de nécessairement original. En revanche, ceux qui cherchent à faire une fonction optimisée à fond, ceux-là vont devoir réfléchir longuement aux différentes propriétés de leur contexte pour voir là où ils peuvent gratter, et donc inventer un algorithme hautement spécialisé mais difficile à comprendre et à analyser. Je vois donc ce dernier point comme un rasoir d'Ockham : ne cherchez pas à faire différent/complexe tant que vous n'avez pas de bonnes raisons de le faire.
Encore une fois je suis sceptique. Je ne pense pas que ce soit une bonne idée de maximiser ces indices, dans le sens où il faut les maintenir après derrière... et on sait qu'on n'est pas bon là dedans non plus. Par contre, il faut maximiser la cohérence du modèle, et donc réutiliser là où on s'attend à avoir la même chose. Pour minimiser la complexité, il est alors important de factoriser, de façon à ne pas avoir besoin de changer 36000 trucs dès qu'on en change 1 dans le modèle. Perso, j'essaye de voir chaque classe et chaque méthode comme un modèle indépendant à part entière : une méthode a un objectif particulier, et les concepts (noms de variables) utilisés dans cette méthode prennent leur sens de cette méthode. De même, les noms de méthodes implémentées dans une classe prennent leur sens de cette classe.
Un exemple actuel que je peux citer est le suivant :
J'ai implémenté une lib générique de parsing où j'ai une classe Csv qui me permet de parser un fichier CSV. Cette classe définit le concept (inner class) Record pour qualifier les lignes de données, donc toutes les lignes sauf l'entête qui donne les noms de colonnes. Cette implémentation n'a aucune notion de ce que contiennent les champs, et n'a pas vocation à le savoir.
Dans un autre projet, je génère des données de benchmarking pour évaluer un algo, que je sauvegarde au format CSV. J'utilise donc ma classe pour parser le fichier, mais comme ça ne me donne pas beaucoup de lien avec la sémantique du projet (quelles données sont contenues dans le CSV), ça ne m'aide pas beaucoup, on est donc face à une "mauvaise traduction du modèle mental" selon Young, et cela se traduit en ayant du code additionnel un peu partout ou j'utilise ce Csv pour récupérer et traduire les données textes correctement avant de les utiliser. Pour régler ce problème, au sein de mon projet j'ai créé une classe BenchmarkCsv qui encapsule une instance de Csv (plutôt que d'en étendre la classe). Cela me permet de définir uniquement les fonctions dont j'ai besoin, avec les noms qui correspondent à mon contexte (e.g. getRanking(), getStakeholders(), ...). BenchmarkCsv définit par ailleurs son propre concept Record qui utilise une représentation différente de Csv.Record. C'est au sein de BenchmarkCsv que je décide dans quel cas j'utilise les concepts venant de Csv ou de BenchmarkCsv, mais pour toute utilisation de BenchmarkCsv on se contente de traiter des concepts définis par celui-ci. Csv est donc complètement caché et sa généricité ne pose plus de problèmes.
De mon point de vue, tous ces points se résument à 2 notions : rigueur et simplicité. Et pour être rigoureux, il faut savoir simplifier, tout comme pour savoir simplifier (correctement) il faut savoir être rigoureux. Le truc, c'est qu'il ne s'agit pas de le savoir pour le faire. Si on n'en fait pas un principe à suivre, on ne le fera jamais, pour la simple raison qu'il y a, comme vient de le dire anweber, trop d'incitation à ne pas le faire pour favoriser la rapidité et les diminutions de coûts (quand la motivation et les critères sociaux ne viennent pas s'y ajouter).
Attention à ne pas tout mélanger :
- un test valide une spécification, il dit si quelque chose d'attendu arrive ou non
- la documentation du code explique différents aspects liés à celui-ci, ce qui peut aller au delà de la spécification, comme des exemples d'utilisation et des considérations abstraites permettant de clarifier le point de vue des concepteurs
- les commentaires, s'ils disent ce que le code fait, font doublon (mais d'autres diront que ça donne une autre représentation et donc aide à la compréhension du code, question de point de vue), mais s'ils disent pourquoi le code est implémenté de telle manière, ce n'est pas quelque chose que tu retrouveras dans tes tests : le fait que mon test passe (pas) ne me dit pas pourquoi ça passe (pas).
Site perso
Recommandations pour débattre sainement
Références récurrentes :
The Cambridge Handbook of Expertise and Expert Performance
L’Art d’avoir toujours raison (ou ce qu'il faut éviter pour pas que je vous saute à la gorge {^_^})
Je voulais te répondre sur ce point spécifique ;
tu parles de "maintenir un module"...
Je parlais (et je pense le posteur en référence aussi) de "maintenir une application" (sinon pourquoi poser la question du "global" ?), ou "maintenir un module dans le contexte d'une application"...
Ayant travaillé sur des applis de quelques millions de lignes, de centaines de modules, et de dizaines de milliers de fonctions/méthodes, il me semble qu'il est strictement impossible de maintenir un des "modules" (et même une des méthodes ou fonctions) sans avoir la vue globale de ce à quoi ça sert... quels sont les paramètres entrés ou sortis, dans quel contexte ils servent, etc etc etc... Car (et on revient sur la "documentation") des fois une certaine utilisation a défini un "CdC" qui est omis dans la référence de la "fonction" ou "module" (peut-être parce que le document a 10 ou 15 ans, figure parmi les 45 volumes de doc dans la pièce dédiée, etc etc, qu'il y a eu un crash et qu'on a perdu la sauvegarde, que l'auteur est parti à la retraite, est mort, que la personne-ressource est partie, que la loi ou le réglement sur lequel c'était basé a changé, etc)...
C'est pour ça que je dis que AU CONTRAIRE, il est essentiel d'avoir la vue d'ensemble si on veut maintenir correctement une petite fonction ou un petit module...
Je connais bien entendu l'encapsulation et son utilité... Ce n'est pas de ça qu'il s'agissait, mais de maintenance.... Sur des logiciels qui par exemple ont 15 ou 20 ans, ou même 10 ou 5 mais ont des conséquences "critiques" (vie ou mort/blessures d'hommes, conséquences économiques ou industrielles fortes (fermetures d'usines, évacuation de zones, etc), qui sont adaptés à une variété de clients (au vrai sens commercial), les particularités ou dépendances ou conséquences doivent être vues globalement avant de faire la moindre modification ponctuelle et locale...
D'un côté, je vois les applications qui nécessitent d'être optimisées, comme les applications critiques, et de l'autre celles qui ne le nécessitent pas mais qui peuvent être néanmoins complexes de part le grand nombre de choses qu'elles font. Dans le premier cas, une expertise très avancée est nécessaire, on ne peut pas caser un nouveau dessus juste parce qu'on a besoin de quelqu'un. Cela dit, dans de tels systèmes il devient de plus en plus intéressant de faire de la génération de code, où on définit des contraintes à respecter et ces contraintes sont soit traduites en code source directement (méthodes formelles), soit du code source est généré et muté jusqu'à satisfaire les contraintes (genetic programming). Je met donc ces cas là de côté, car c'est très particulier et l'importance de la clarté du code est discutable. C'est un compromis entre temps de dév, expertise, coûts, outils à disposition, criticité, etc.
Pour les autres cas, où l'expert codeur est censé être humain, je pense qu'il est plus intelligent de faire une application complexe sur la base d'une combinaison de choses génériques (au sens "faites pour fonctionner dans un domaine plus large que ce que le projet nécessite") : quand tu as tellement de choses qui dépendent les unes des autres que tu es obligé d'avoir une vision globale pour ne pas faire n'importe quoi dans un scope local, pour moi c'est aller trop loin dans l'optimisation (si on considère que la maintenance doit rester gérable). Le fait d'utiliser des parties génériques, qui ont leur propre objectif indépendamment du projet, permet d'avoir un découpage propre de l'application avec une délégation de responsabilités qui permet d'avoir moins de questions à se poser. Si telle chose ne marche pas, c'est là que ça doit être géré, point final, et on ne commence pas à imaginer des solutions tordues pour que quand ça marche pas là ce soit corriger ici, puis voir que faut rajouter aussi un truc là pour quand c'est utilisé comme ça, etc. Ceux qui travaillent sur une partie générique ont leur cahier des charges, ceux qui font l'appli ont le leur, et changer quelque chose dans l'un ne change rien dans l'autre, parce que l'objectif final est toujours le même et c'est tout ce que le voisin a besoin de savoir.
Mère Nature est un processus hautement parallèle, où tout interagit avec tout. Mais personne n'est capable, pour autant que je le sache, de comprendre/analyser un tel processus de manière à la fois holistique et sereine. Il nous faut, nous être humains, se focaliser sur des propriétés spécifiques, établir des procédures d'analyse, tirer des conclusions, revoir nos jugements, etc. Si on fait des applis qui, comme mère Nature, ont des dépendances de partout, il ne faut pas s'étonner que ça devienne ingérable. On peut dire X bosse là dessus, Y là, Z ici, etc. mais derrière il faut ensuite avoir A pour relier tout le monde et faire la synthèse, la synchro entre chacun, etc. Et plus on en rajoute pour faire telle ou telle tâche, plus il faut en rajouter pour les manager. Et si une personne à une probabilité p de ne pas faire d'erreur, N personnes ont une probabilité p^N bien inférieure. Au bout d'un certain nombre, on est sûr d'avoir une bourde quelque part, et pour éviter ça il nous faut encore rajouter des gens qui s'assureront qu'on ait des moyens de corriger quand ça arrive. À la fin on arrive à un bric à brac ingérable, et avoir une vision globale et cohérente pour 1 personne est juste infaisable.
Dans les systèmes critiques, plutôt que de s'assurer d'avoir une vision globale et cohérente, on préfère y rajouter des filets de sécurité (spare, duplication et vote, etc.) au cas où justement il y aurait un truc qui n'irait pas. Mais si l'objectif est de maîtriser l'appli pour la faire correctement, passer par un découpage qui réutilise des parties indépendantes sur lesquelles on n'a pas besoin de savoir grand chose, et dans lesquelles il n'y a pas besoin de savoir quoi que ce soit à propos de l'appli qu'on implémente, me semble incontournable.
Site perso
Recommandations pour débattre sainement
Références récurrentes :
The Cambridge Handbook of Expertise and Expert Performance
L’Art d’avoir toujours raison (ou ce qu'il faut éviter pour pas que je vous saute à la gorge {^_^})
Je ne parlais pas d'optimisation..
Simplement il me semble que ta vision "je pense qu'il est plus intelligent de faire une application complexe sur la base d'une combinaison de choses génériques " est justement une vision de "doctorant", comme tu l'indiques....
Ce n'est que mon expérience, mais il est bien entendu que l'on utilise des biblothèques à tour de bras... Ce que je dis, c'est que même si cette biblothèque a l'air "étanche", dans un cadre professionnel il est absolument vital d'avoir regardé tous les appels, usages, etc, avant de modifier quelque chose (sauf si bien entendu elle est vraiment vraiment vraiment étanche, mais il est très très rare que ce soit le cas : ne serait-ce que les structures de données, qui peuvent éventuellement être passées en binaire pour communiquer entre applis, ou des problèmes d'alignement, etc etc).....
C'est comme pour les versions des OS... "Normalement" une appli ne devrait pas dépendre des versions.... Le problème (comme le dit l'adage, la différence entre la théorie et la pratique c'est qu'en théorie il n'y en a pas) c'est que la réalité dépasse souvent la fiction... Et qu'une petite modif due à un update de maintenance d'un OS peut tout à fait foutre le bordel dans une appli tout à fait stable (j'ai eu le cas, avec plus de 3 mois de recherche, où l'appli fonctionnait très bien sur N plateformes opérationnelles, et pas sur 1 en particulier, qui a priori n'avait ien de différent d'une bonne partie des autres... )
Ce que je veux dire dans le fond c'est que justement, il y a d'un côté la théorie, et de l'autre côté la pratique... Quand on discute de sujets comme celui dont il est question ici, on est orienté "pratique", il me semble....
Je suis d'accord sur les points qui nous fait oublier ce que notre code fait. Par contre, pour moi, les causes sous-jacentes sont principalement: le manque de maitrise du problème à résoudre et des solutions existantes pouvant s'y rapporter. Les spécifications à atteindre sont souvent imprécises, et même changent en cours de projet. Les fonctionnalités provenant de librairie sont mal utilisés car nous ne connaissons mal ce que fait réellement ces algorithmes. Ajoutons à ce contexte le stress des échéanciers trop court et nous venons de perdre le contrôle sur notre code et de notre projet. Beaucoup trop d'informaticiens se targuent d'être d'excellent programmeurs parce qu'ils connaissent à fond tel langage ou librairie. À cela je leur répond un diction culinaire: Connaître les ingrédients, ne fais pas de vous un grand chef!
Personne maitrise réellement l'ART de la programmation (qui nous éviterait les problèmes mentionnés ci-haut!). À ceux qui pensent que ces propos sont pessimistes, je leur réponds qu'il y a toujours place à l'amélioration. Je souhaite à tous de rester humbles devant les problèmes et les autres. L'humilité nous permet de progresser et c'est le point le plus important pour des informaticiens qui ont à s'adapter à un monde en perpétuelle changement.
Merci d'avoir traduit l'article de Stephen Young! Il a écrit quelques autres articles sur le sujet, outre medium.com où son article est repris, voici son site web: http://aestheticio.com/
Il m'arrive souvent de reprendre un code après quelques années et de n'y rien comprendre au premier abord. La raison est plus simple que tout ce discours : quand on vient d'écrire un code ou de le comprendre de nouveau, il apparait tout simplement "évident", on ne sait plus "où est le problème". Et les commentaires qu'on peut rajouter sont redondants. La solution, qui permet de coder plus facilement et de comprendre même longtemps après est de commenter AVANT de coder.
Partager