Journal Software architecture considered harmful

Posté par  . Licence CC By‑SA.
Étiquettes : aucune
27
8
juin
2022

Bonjour tout le monde,

Trop souvent je me retrouve à devoir travailler sur des projets dont la complexité globale est supérieure à ce que serait celle d'un bête fichier avec les fonctions versionnée avec un numéro dans le nom (j'exagère à peine).

Concrètement, c'est assez typique je pense : pour un projet, 4 dépôts git avec pour chacun 6 branches quasi vides, une arborescence /lib /app /include /doc /ressource, des submodules dans tous les sens, tous les fichiers README, TODO, INSTALL, une péta chiée de tests (avec une dépendance à la con pour la compilation), tout ça pour une charge utile de 600 lignes de code.

Ajouter de la structure, c'est bien, mais ça reste ajouter quelque chose, ça augmente la complexité. Si cette complexité ajoutée n'apporte pas un énorme plus il ne faut pas la mettre.

J'ai le sentiment qu'au cours de leur formation des programmeurs commencent par tout mettre dans un seul fichier, puis apprennent à faire des fonctions, des modules, des packages, etc. mais qu'au final pour un code utile donné la complexité reste constante, elle ne fait que s'éloigner du code proprement dit. Pire ! Quand tout le code est dans un seul fichier en vrac, au moins, il tient dans un buffer d'éditeur de texte, pas besoin de regarder dans 4 dépôts, 36 répertoires et 170 fichiers.

J'ai l'impression que des dev passent plus de temps à jouer avec les outils qu'à coder, je préfère 1000 fois un programme en vrac dans un seul fichier qu'une coquille vide qui fait trois trucs avec une archi qui n'est pas nécessaire en dessous de 100 000 lignes de code. Il y a un culte du cargo là dedans, reproduire à échelle microscopique des techniques qui sont nécessaire quand on bosse sur un projet énorme, dans l'espoir que… ça rende le projet énorme ? Je sais pas trop ce qui est espéré là dedans.

Bien sûr, il faut un peu penser et garder une visibilité sur la suite, mais certainement pas anticiper. Le besoin de structure dans un code vient des "feuilles", certainement pas de la "base".

https://fr.wikipedia.org/wiki/YAGNI
https://fr.wikipedia.org/wiki/Principe_KISS

  • # tss tss...

    Posté par  (site web personnel) . Évalué à -5.

    Il y a un culte du cargo là dedans, reproduire à échelle microscopique des techniques qui sont nécessaire quand on bosse sur un projet énorme, dans l'espoir que…

    Je ne suis pas certain qu'il faille attribuer au⋅x programmeur⋅s quelque⋅s volonté⋅s qui ne s'explique pas simplement par le copier/coller, le cookiecutter ou autre génération automatisée.

    Adhérer à l'April, ça vous tente ?

    • [^] # Re: tss tss...

      Posté par  (site web personnel, Mastodon) . Évalué à 2.

      C'est un peu ce que je me suis dit en lisant « Quand tout le code est dans un seul fichier en vrac, au moins, il tient dans un buffer d'éditeur de texte, pas besoin de regarder dans 4 dépôts, 36 répertoires et 170 fichiers. » Il y a l'influence des outils (les fameuses IDE qui font tout un tas de trucs y compris afficher la liste des 170 fichiers dans une encadré en faisant croire que ce sont juste des fonctions –non, pas faux quand t'as une fonction par fichier etc.)

      “It is seldom that liberty of any kind is lost all at once.” ― David Hume

      • [^] # Re: tss tss...

        Posté par  . Évalué à 2.

        Il y a l'influence des outils (les fameuses IDE qui font tout un tas de trucs y compris afficher la liste des 170 fichiers dans une encadré en faisant croire que ce sont juste des fonctions –non, pas faux quand t'as une fonction par fichier etc.)

        Comme ici: https://git.skarnet.org/cgi-bin/cgit.cgi/skalibs/tree/src/libstdcrypto

        Vu le projet, je doute qu'il soit fait avec un IDE par contre, ou alors y'a des IDEs qui supportent autotools?
        L'avantage d'une fonction par fichier, c'est le temps de compilation. Et un éditeur de code qui comprenne le langage peut largement permettre aux gens de naviguer entre les fichiers (le mien est pas config pour ça, mais rien ne l'empêcherait je pense)

        • [^] # Re: tss tss...

          Posté par  (site web personnel, Mastodon) . Évalué à 2.

          Dans cet exemple, y a pas assez fichiers pour avoir besoin d'un IDE ;-D

          “It is seldom that liberty of any kind is lost all at once.” ― David Hume

        • [^] # Re: tss tss...

          Posté par  (Mastodon) . Évalué à 2.

          L'avantage d'une fonction par fichier, c'est le temps de compilation.

          C'est aussi que à l'édition de lien avec le .a contenant les objets de tous ces fichiers, seulement les fonctions appelées sont incluses dans le binaire, ce qui en réduit la taille.

          • [^] # Re: tss tss...

            Posté par  . Évalué à 2.

            Oui, j ai oublie le "entres autres".
            Je parie que les conflits git sont moins tordus aussi quand on remplace un gros bloc..
            j ai aussi deja pense a utiliser ce type de morcellement, mais clairement il faut un outillage qui masque l'info de bas niveau, ou une trop grosse discipline pour moi.
            j y avais pense pour, notamment generer automatiquement le header des classes, maintenu a partir d'un graphe.
            Je trouve toujours l idee allechante moi.

  • # Adage

    Posté par  (site web personnel) . Évalué à 10.

    Comme dirait un de mes premiers collègues:

    Au début on fait simple parce qu'on ne sait pas faire autrement ;
    Ensuite on fait compliqué parce qu'on peut ;
    Après on fait simple parce qu'on a compris que ça servait à rien de faire compliqué.

    Matthieu Gautier|irc:starmad

    • [^] # Re: Adage

      Posté par  (site web personnel) . Évalué à 8.

      Oui c'est exactement ça (en tout cas c'est ce que j'observe autour de moi et avec moi même) ; on peut dire que c'est l'exercice de la maturité avec l'expérience dans bcp de domaines.

      Et comme le disait un ancien collègue :

      il est plus simple de faire compliqué mais plus compliqué de faire simple

      • [^] # Re: Adage

        Posté par  . Évalué à 5. Dernière modification le 10 juin 2022 à 11:00.

        Y’a aussi l’influence du monde du travail.

        Va justifier que tu as passé trois jours à réfléchir comment satisfaire un besoin en 10 lignes de code alors que ton collègue aura remplit le même besoin en y passant le même temps, mais avec 300 lignes…

        Le middle management c’est de plus en plus rarement des techniciens/ingénieurs expérimentés…

        Mort aux cons !

        • [^] # Re: Adage

          Posté par  . Évalué à 5.

          Va justifier que tu as passé trois jours à réfléchir comment satisfaire un besoin en 10 lignes de code

          C'est marrant, j'ai la problématique en ce moment : les biostats du service nous sortent des scripts de 1500 lignes de R pour faire ce que je sors en 300 (dplyr et magrittr aggravant le problème). Sauf que quand mon chef me demande une évolution, il l'a dans l'aprem, alors que pour eux, il l'a dans la semaine.

          Ça, ce sont les sources. Le mouton que tu veux est dedans.

    • [^] # Re: Adage

      Posté par  (site web personnel) . Évalué à 4.

      Le Second-system effect, en quelques sortes.
      https://en.wikipedia.org/wiki/Second-system_effect

      pertinent adj. Approprié : qui se rapporte exactement à ce dont il est question.

  • # et les fonctions

    Posté par  . Évalué à 8.

    ton journal me fait penser au https://en.wikipedia.org/wiki/Code_golf

    Il me fait aussi penser à une autre source de complexité, qui est la tendance de certains à écrire plein de petites fonctions, même si le graphe d'appels est extrêmement simple et ne justifie pas de découper en plusieurs fonctions. Du coup au lieu de lire de haut en bas une fonction de 50 ou 100 lignes, on doit se promener dans un plat de spaghettis.

    J'en avais parlé à un collègue qui avait ce travers, et il m'avait dit que c'était une recommandation du livre "Clean code" de faire des fonction très courtes, de quelques lignes seulement. Je ne sais pas si c'est vrai (je n'ai pas lu le livre). La recommandation du Coding Style du noyau Linux me semble faire plus de sens: https://www.kernel.org/doc/html/v4.10/process/coding-style.html#functions

    • [^] # Re: et les fonctions

      Posté par  . Évalué à 10.

      L'avantage de découper en plusieurs fonctions je trouve c'est que ça facilite les tests. Plusieurs fonctions faisant uniquement une seule chose c'est plus simple à tester que d'essayer de couvrir une grosse fonction qui a possiblement beaucoup plus de chemins d’exécution. En continuité, c'est aussi possiblement plus facile à mocker.

      Dans le subjectif, ça me facilite la lecture. J'avoue que quand j'arrive dans la fin d'une fonction de plusieurs dizaines de lignes, j'ai un peu oublié quels sont les paramètres, leurs valeurs possibles etc. Si la fonction est bien découpée et fait appel à plusieurs autres fonctions pour des unités de travail définie, je trouve que c'est moins de charge cognitive.

      Maintenant je suis d'accord que si le graphe est super simple (par exemple, si la fonction est déjà unitaire), c'est rajouter de la complexité possiblement inutile.

      • [^] # Re: et les fonctions

        Posté par  (site web personnel) . Évalué à 6.

        C'est exactement ça, une question d'équilibre. Ensuite perso je préfère un code plus simple à lire qu'un code simple à tester.

        J'écris le test une fois, je le lis des dizaines de fois :D

        Et là encore, il faut un certain équilibre (le test reste du code, qu'il ne faut pas sacrifier).

        En fait dès qu'on me parle sous la forme d'un dogme ("les commentaires ne servent à rien, le code doit se suffire", "une fonction ne doit pas dépasser 5 lignes", …) j'arrête l'échange car ça ne mènera à rien si on ne parle pas de cas concrets (viens sur la couche système/réseaux sans commentaires et on en reparle :D).

        Après ça vient avec l'expérience, c'est facile de dire ça après près de 20ans de code (purée déjà :p), et qu'au début il faut quelques recommandations à suivre.

        Mais il faut bien comprendre que ce sont des recommandations et que tu dois les remettre en questions face à une situation.

        Bon vu que les articles sont désormais ultra polarisés pour attirer le chaland, cette notion n'est pas vendeuse, et on est donc pas sorti de l'auberge :(

    • [^] # Re: et les fonctions

      Posté par  . Évalué à 9.

      C'est un peu plus compliqué que ça, normalement les fonctions devraient avoir un nom explicite, ce qui facilite la compréhension du code (attention j'ai dit compréhension, pas debuggage); perso, une fonction qui fait plus d'un écran de haut, demande de se poser des question, mais ça ne veut pas dire qu'il faut la couper, surtout si c'est pour avoir func1partie1, func1partie2 qui pour le coup n'aide en rien à la compréhension.

      A deux écran de haut ou plus c'est vraiment exceptionnel.

      Il ne faut pas décorner les boeufs avant d'avoir semé le vent

      • [^] # Re: et les fonctions

        Posté par  . Évalué à 5.

        j'aime bien ton métrique ! :D

        • [^] # Re: et les fonctions

          Posté par  (site web personnel) . Évalué à 10.

          Avec les écrans qu'on a aujourd'hui il n'y a pas de raison de se limiter à 400 lignes. Moi avec mon 50 pouces installé en portrait, je mets une police 6 et je peux afficher 73269 lignes sans problème.

        • [^] # Re: et les fonctions

          Posté par  . Évalué à 5.

          j'aime bien ton métrique ! :D

          oui il est très approximatif, et n'importe qui peut le détourner en prenant une police de caractère sur 3 pixels de haut, ou mettre son écran en vertical…

          de même j'ai pas donné de largeur max pour les lignes, j'ai tendance à me limiter à 120 en général; mais l'idée derrière cette métrique (choisie avec un éditeur raisonnable, sur un taille de police raisonnable), c'est de pouvoir avoir la totalité de la fonction en visuel. Tout comme devoir bouger pour aller voir ce que fait trucmuche(machin, bidule)

          La je suis en train de coder un export objet -> format vecteur, avec un paquet de valeurs à faire, rien que la déclaration des vecteurs pour un type d'objet fait 2/3 de mon écran, et découper en morceau serait complètement artificiel…

          comme je l'ai dit c'est un cadre raisonnable, mais il n'est pas strict, cependant cela permet de se poser des question de pourquoi on déroge au cadre. Et c'est cela qui est important: se remettre en question si on déborde.

          Il ne faut pas décorner les boeufs avant d'avoir semé le vent

        • [^] # Re: et les fonctions

          Posté par  . Évalué à 2.

          de memoire, cccc affiche en jaune ce qii depasse 50 lignes de code brut, entres autres.
          c est une metrique quej essaie de suivre, mais avec la gestion des erreurs, les logs et autres asserts, 50 se depasse vite.
          Apres, le code pour (de-)serialiser est une grosse exception, malheureusement.

      • [^] # Re: et les fonctions

        Posté par  (site web personnel) . Évalué à 10. Dernière modification le 09 juin 2022 à 22:36.

        Il y a un second aspect important dans la préconisation de clean code de faire plein de petites fonctions : à l'intérieur d'une fonction, le niveau d'abstraction doit être homogène.

        Par exemple, on évite de mélanger des appels à des fonctions de haut niveau avec du code de très bas niveau dans le même bloc.

        Éviter :

            void readTemperature() {
               connectToSensor();
               readSensorValue();
        
               FILE *fp = fopen("output file.txt", "w");
               fwrite(...);
               fclose(fp);
            }

        Préférer :

            void readTemperature() {
               connectToSensor();
               readSensorValue();
               writeSensorValue("output file.txt");
            }
        
            void writeSensorValue(char *filename) {
               FILE *fp = fopen(filename, "w");
               fwrite(...);
               fclose(fp);
            }

        Ça évite au cerveau de devoir changer de niveau d'abstraction en pleine lecture d'un « paragraphe ».

        • [^] # Re: et les fonctions

          Posté par  (site web personnel) . Évalué à 4.

          Le problème c'est que c'est souvent pas si simple.

          Par exemple, la fonction "à éviter" est parfaitement lisible pour moi.
          En pratique, les fonction sont bien plus longues et donc plus dur à diviser

          bool readTemperature(Sensor * sensor, Field field; const char *filename) {
              SensorHandle *sensor_handle = connectToSensor(sensor, NULL, true);
              Value *v = readSensorValue(sensor_handle, field);
          
              FILE *fp = fopen(filename, "w");
              if (fp) {
                  fwrite(... v ... field ...);
                  fclose(fp)
              }
          
              freeSensorValue(v);
              freeSensorHandle(sensor_handle)
              return fp != NULL;
          }

          Alors bon, tu dois passer plein d'arguments

          bool readTemperature(Sensor * sensor, Field field, const char *filename) {
              SensorHandle *sensor_handle = connectToSensor(sensor, NULL, true);
              Value *v = readSensorValue(sensor_handle, field);
          
              bool ret = writeSensorValue(v, field, filename);
          
              freeSensorValue(v);
              freeSensorHandle(sensor_handle)
              return ret;
          }
          
          void writeSensorValue(Value *v, Field field, const char *filename) {
              FILE *fp = fopen(filename, "w");
              if (fp) {
                  fwrite(... v ... field ...);
                  fclose(fp);
                  return true;
              }
              return false;
          }

          Ok, dans cet exemple c'est encore correcte mais il y a des cas ou séparer en différente fonction demande de passer tellement de paramètre que le boiler plate rends le code plus difficile à lire, surtout si l'ordre des trucs est changer.
          Et il y a plein de question comme quelle fonction dois libérer les ressources.

          Enfin soit.. qui écrit encore en C de toute façon?
          Perso, j'écrirais plutôt le code comme ça:

          bool readTemperature(Sensor &sensor, Field field, sting_view filename) {
            auto sensor_handle = sensor.connectSensor();
            auto v = sensor_handle.readValue(field);
          
            if (auto file = File::create(filename)) {
               file.write(... v .. field);
               return true;
            };
            return false;
          
          }
          • [^] # Re: et les fonctions

            Posté par  (site web personnel) . Évalué à 5.

            tellement de paramètre que le boiler plate rends le code plus difficile à lire, surtout si l'ordre des trucs est changer.

            Suffit de faire des variables globales.

            pertinent adj. Approprié : qui se rapporte exactement à ce dont il est question.

            • [^] # Re: et les fonctions

              Posté par  . Évalué à 2.

              je pense que tu confonds variables globales (accessibles hors TU) et variables de module.
              La seconde est moins pire, et meme ok en mono-thread.

              Mais la solution a trop de variables passees,je crois que c est plutot les "types utilisateur": struct en C. Qui rendent la maintenance de l api plus simple, sans etre la panacee

    • [^] # Re: et les fonctions

      Posté par  . Évalué à 4. Dernière modification le 09 juin 2022 à 23:55.

      La recommandation du Coding Style du noyau Linux me semble faire plus de sens: https://www.kernel.org/doc/html/v4.10/process/coding-style.html#functions

      J'ai le livre, et à la lecture de ton lien je comprends plutôt que les deux recommandations sont similaires. Clean Code recommande une fonction par tâche unitaire (obtenir des fonctions courtes est une conséquence).

      Si le collègue se concentre en premier sur le nombre de lignes en oubliant que c'est l'unitarité qui fait la fonction, oui là c'est un problème.

  • # Pas sûr

    Posté par  (site web personnel) . Évalué à 10.

    Je comprends ta souffrance et pourtant ça ne me semble pas évident. Quand je repars sur des trucs simples, j'en reviens toujours à ressentir les mêmes effets :

    • trop de lignes dans le fichier : « c'est le bordel, ce fichier fait trop de trucs »
    • trop de trucs enfouis dans le bazar : « pas moyen d'écrire un test unitaire pour cette fonction déclarée statique au fond d'un fichier avec dépendances à des variables globales » (ça marche aussi avec trop d'encapsulation)
    • au bout d'un moment j'en ai marre et je range : « et vas-y qu'il faut que je déplace tout le bazar, et vas-y qu'il y a des dépendances implicites et des raccourcis moisis ».

    Alors avec le temps je préfère encore une arborescence plutôt vide qu'un petit tas de trucs empilés en vrac :) La simplicité, je la cherche ailleurs : calmos sur l'OOP, calmos sur la metaprog, calmos sur les dépendances third party (outillage ou libs).

    Pour le multi-dépôts je n'ai pas encore tranché. J'aime que ce soit séparé et isolé, mais d'un autre côté le mono-repo est bien confortable pour les refactos et pour retrouver tout le projet dans un état donné.

    • [^] # Re: Pas sûr

      Posté par  (site web personnel) . Évalué à 2. Dernière modification le 14 juin 2022 à 18:05.

      En toute objectivité le multi-dépôts, ça mérite de rejoindre Multidesk OS au rayon des fantasmes à jamais inassouvis !

  • # Perso

    Posté par  (site web personnel) . Évalué à 7. Dernière modification le 09 juin 2022 à 13:12.

    Personnellement j'ai un squelette de repo par langage que j'ai tendance à réutiliser pour chaque projet dans ce langage. Pour Rust, c'est cargo qui s'en charge. Pour Python c'est juste deux fichiers. Mais pour d'autres langages comme Java ou C++ c'est plus conséquent afin d'avoir un build system bien configuré, des tests et une structure qui va suivre quand le projet va grossir (parce que si je pars sur ces langages, c'est pas pour 100 ou 1000 lignes de codes…).

    Mais si jamais je devais faire un petit truc en C++ avec 100 lignes de code, je partirais quand même là dessus parce que ça simplifie énormément la vie d'avoir un truc tout prêt où y'a juste à coder ce qui t'intéresse sans avoir à réinventer la roue:
    - qu'est-ce que je mets dans mon makefile
    - ah mince j'ai une nouvelle dépendance, comment je l'ajoute
    - une copine veut tester mon code mais ça marche pas chez elle, pourquoi ? Ah oui j'avais supposé que X était présent et l'erreur n'est pas du tout évidente
    - etc …

    Et puis quand tu connais un environnement/framework/… il n'y a pas vraiment de surcoût à payer pour l'utiliser sur quelque chose de trop petit. Pour les autres oui, si ils n'utilisent pas la chose en question ailleurs. Mais si c'est ton projet, t'as souvent tendance à pas trop en tenir compte et ça ne le choque pas.

  • # Bof

    Posté par  . Évalué à 3.

    Concrètement, c'est assez typique je pense : pour un projet, 4 dépôts git avec pour chacun 6 branches quasi vides, une arborescence /lib /app /include /doc /ressource, des submodules dans tous les sens, tous les fichiers README, TODO, INSTALL, une péta chiée de tests (avec une dépendance à la con pour la compilation), tout ça pour une charge utile de 600 lignes de code.

    Tu prend un cas ultra caricatural. Donc oui et on peu prendre un cas ultra caricatural dans l'autre sens donc non et voila on est pas plus avancé.

    Trouver le bon seuil entre la simplicité (au sens KISS) et l'over engenering n'est pas simple et renvoyer ceux qui n'ont pas fais le choix qui te convient à "cargo cult"/incompétence/etc ne me donne pas l'impression que tu ressent la difficulté du sujet.

    https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll

    • [^] # Re: Bof

      Posté par  . Évalué à 2.

      le 2nd de S, c est pour "stupid". c est tres mal choisi, parce que faire simple requiert d'etre smart. les gens stupides ne font pas du simple a maintenir.

      • [^] # Re: Bof

        Posté par  . Évalué à 3.

        C'est le code qui doit être stupide, pas celui qui l'écrit.

        • [^] # Re: Bof

          Posté par  . Évalué à 2.

          Justement, je ne considère pas un code simple a comprendre et qui fait quelque chose d'utile efficacement comme étant stupide.
          Par contre, j'ai bel et bien vu des bases de code qui se prétendent KISS dont le code fait quelque chose à l'utilité douteuse de manière non efficace.

          Je pense ici notamment à la base de code de wesnoth, qui, de mémoire, gérait à un moment la liste des unités avec une foultitude de vector/map/string, sur plusieurs niveaux d'imbrication, avec des struct/class dont la liste des propriétés (variables membres, donc) est longue comme le bras, et pas ordonnée du tout, ce qui implique, entres autres, du padding à la pelle et donc une augmentation de la ram utilisée.
          De mémoire, j'ai mesuré récemment que la RSS est d'environ 260 megs au démarrage, pour dépasser les 2 giga quand je suis entrée dans le lobby… de la précédente version stable (debian n'ayant pas l'actuelle stable dans les backports, et moi n'ayant aucune envie d'utiliser un autre gestionnaire de paquets pour ça, ni de compiler).

          Du coup (et wesnoth n'est pas le seul cas que j'ai vu, je prend juste comme exemple parce que c'est un très, très bon jeu libre et que pas mal de gens le connaissent), oui, j'ai un peu de mal avec cet acronyme de KISS… peut-être que dans d'autres cas, il est appliqué correctement, mais je n'ai pas encore vu ça.

          • [^] # Re: Bof

            Posté par  (site web personnel, Mastodon) . Évalué à 3.

            J'ai toujours pris « stupide », dans ce contexte, comme étant le « contraire d'inutilement compliqué » (et beaucoup de codes mais supposés simples ont tendance à appliquer le « pourquoi faire simple quand on peut faire compliqué » sans que la complexité se justifie vraiment.)

            “It is seldom that liberty of any kind is lost all at once.” ― David Hume

  • # Pour qui ?

    Posté par  (site web personnel) . Évalué à 3.

    Si tu veux que quelqu'un qui s’intéresse au truc ne mettes pas 3000 ans à comprendre comment c'est fait, l'usage d'outils standards et de conventions a quand même d'un certain avantage.

    Il y a quelques années je travaillais sur des sujets de GIS et je dois dire que beaucoup de code sortant des universités te faisait regretter autotools. Je ne sais pas comment ça a tourné aujourd'hui mais à l'époque s'envoyer GDAL avec le support de ECW et toutes ses dépendances à patcher et compiler, c'était en passe de devenir un métier en soit.

    Alors certes des fois c'est un peu overkill. Mais si le gars qui prend le truc pour produire un patch s'y retrouve en 5 mn, c'est l'essentiel non ?

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.