Mot-clef « Développement »

Quelques trucs sur Jekyll #2

Lisibilité des erreurs

Par défaut, le serveur de développement de Jekyll remonte des erreurs assez sommaires en ne précisant que le template concerné. C’est inutilisable quand on débugue un template (même si un numéro de ligne ne serait pas du luxe) mais quand l’erreur vient d’un plugin c’est plutôt limité.

Pour un affichage un peu plus détaillé avec une trace d’exécution permettant de déterminer quelle ligne de quel plugin est en cause, il faut utiliser l’option --trace ou -t.

bundle exec jekyll serve --drafts -t

Fichiers sitemap.xml et robots.txt

Il existe un plugin pour générer le fichier sitemap.xml, par contre je n’ai rien trouvé pour gérer son intégration au fichier robots.txt. On peut certes faire un fichier robots.txt à la main mais le lien est censé être absolu, donc ce n’est pas super propre de le mettre en dur.

Finalement je m’en suis sorti en ajoutant un fichier robots.html avec le contenu suivant :

---
layout: null
permalink: robots.txt
---

Sitemap: {{ site.url }}/sitemap.xml

La directive permalink: robots.txt permet de forcer le bon nom de fichier, layout: null dit que le contenu ne doit pas du tout être habillé et comme on est dans une page normale on a accès aux variables globales, notamment site.url.

Éviter la profusion d’espaces dans les pages

Le moteur de template de Jekyll fonctionne en ajoutant des pseudo-balises dans le code HTML notamment pour indiquer les structures de contrôles, boucles, affectations de variables, etc.

Pour que le code reste lisible on fait des retours à la ligne entre ces différentes balises et on indente. Ceci aboutit à de très nombreuses lignes uniquement peuplées d’espaces (ceux de l’indentation).

Exemple :

<h2>Catégories</h2>
{% assign groupedCategories = site.categories | group_categories %}
{% for group in groupedCategories %}
  <h3>{{ group[0] }}</h3>
  <ul>
    {% assign categories = group[1] | sort_by_keys %}
    {% for category in categories %}
      <li><a href="/categories/{{ category[0]|slugify:'latin' }}/">{{ category[0] }}</a> ({{ category[1].size }})</li>
    {% endfor %}
  </ul>
{% endfor %}

Il est possible d’y remédier en utilisant un caractère - dans les balises. Chaque - suivant une ouverture de balise ({% devient {%-) indique que les espaces précédents doivent être ignorés et sur les fermetures ( (%} devient -%})) que les espaces suivants doivent être ignorés.

L’exemple précédent devient alors :

<h2>Catégories</h2>
{%- assign groupedCategories = site.categories | group_categories -%}
{% for group in groupedCategories %}
  <h3>{{ group[0] }}</h3>
  <ul>
    {%- assign categories = group[1] | sort_by_keys -%}
    {% for category in categories %}
      <li><a href="/categories/{{ category[0]|slugify:'latin' }}/">{{ category[0] }}</a> ({{ category[1].size }})</li>
    {%- endfor %}
  </ul>
{% endfor %}

C’est la même syntaxe que l’on retrouve dans d’autres moteurs de templates comme Twig pour PHP (la plupart des syntaxes sont assez proches en fait entre ces deux moteurs).

Remarque : cette syntaxe semble ne pas bien fonctionner avec le tag endraw, en effet j’ai des erreurs dès que je place un tiret un en début de ce tag (alors qu’à la fin et sur le tag raw ça marche)…


Plus on enlève de code, mieux ça marche

Encore récemment, dans le cadre du chiffrage d'une migration d'un projet vers la dernière version d'RBS Change (CMS / e-commerce dont j'ai déjà parlé plusieurs fois ici), l'un des développeurs disait en parlant de certaines fonctionnalités développées sur le projet en spécifique et qui entre temps ont été implémentées dans le produit que maintenant que le code spécifique était écrit, ça ne coûtait pas cher de le garder tel quel plutôt que de prendre le temps de le remplacer par des appels au code du produit.

À part si un besoin spécifique n'est pas compatible avec le code du produit, je suis intimement convaincu que c'est faux. Et ce pour un certain nombre de raisons.

Le point le plus évident pour moi c'est le coût en maintenance. S'il y a bien une chose que j'ai appris en travaillant sur un logiciel qui fait plusieurs centaines de milliers de lignes de code, c'est que plus on a de code, plus c'est couteux à maintenir. D'une part parce que chaque ligne ajoutée peut comporter des bugs ou s'avérer incompatible avec d'autres parties du logiciel et d'autre part parce que plus il y a de code, plus il est difficile de retrouver la source d'un problème. C'est d'autant plus vrai si l'équipe chargée du projet change. Quand le développeur est le même pendant des années, il peut connaitre assez bien son code pour s'y retrouver parmi les implémentations parallèles (et encore... que celui qui ne s'est jamais senti perdu en se replongeant dans du code qu'il avait écrit rien qu'un an plus tôt lève la main) mais un nouvel arrivant sur le projet mettra beaucoup plus de temps à s'y retrouver s'il doit apprendre à connaitre les implémentations parallèles en plus du produit lui-même.

On en arrive du coup à un second point connexe au précédent : l'évolutivité. Naïvement on se dit que vu que la fonctionnalité est codée spécifiquement, on peut faire ce qu'on veut avec et donc on est bien plus libre qu'en utilisant une fonctionnalité native du produit sur laquelle on n'aura pas autant la main. Ce n'est pas faux. Mais cela implique de se couper en partie des évolutions du produit et de devoir tout faire soi-même de son côté. De plus, se pose le même problème qu'évoqué précédemment où tout nouvel arrivant devra oublier ce qu'il sait déjà du produit pour apprendre ce que fait le projet.

Ensuite on a les coûts d'interface utilisateur et d'apprentissage. De deux choses l'une : soit on laisse les deux implémentations parallèles accessibles dans l'interface et là c'est l'utilisateur qui se sentira perdu, ne sachant quoi choisir (ce qui implique du coût de formation et de réparation de ce que l'utilisateur aura mal fait), soit on doit masquer de l'interface les éléments relatifs à l'implémentation standard pour les remplacer par l'implémentation spécifique (ce qui implique un coût initial plus un coût à chaque mise à jour pour revalider les choses et les réadapter si besoin).

Après on peut avoir du mal à jeter le produit de nombreuses heures de développement. C'est normal mais pour un développeur c'est une chose à laquelle il faut s'habituer. Un logiciel qui n'évolue pas, à part s'il est extrêmement ciblé sur un besoin très pointu qui n'évolue pas du tout (chose très rare), c'est un logiciel mort. Un logiciel vivant évolue continuellement au gré des nouveaux besoins, des nouvelles possibilités et des nouvelles idées. Seulement on ne peut pas se contenter d'empiler de nouvelles choses dessus, sous peine de voir l'ensemble s'effondrer sous sa complexité, de devenir inmaintenable et incompréhensible au nouvel arrivant.

Des choses qui semblaient - et potentiellement étaient réellement - pertinentes à un moment donné ne le seront plus un an plus tard parce que le besoin aura évolué ou simplement parce qu'à force d'y greffer des verrues, on aboutit à un ensemble qui ne ressemble plus à rien. Il ne faut donc pas hésiter à remplacer des fois des pans entiers du logiciel pour repartir sur des bases plus saines. Qui elles-mêmes dégénèreront plus ou moins rapidement (selon la qualité de l’implémentation et la vitesse à laquelle les besoins liés évoluent) avant d'être à leur tour remplacées à nouveau.

Ce principe vaut aussi bien au niveau macroscopique (une API entière finit par devenir trop lourde et doit être remplacée) qu'au niveau microscopique (une méthode donnée peut souvent être réécrite en cinq fois moins de code parce qu'elle prévoyait des cas qui n'existent plus ou bien parce qu'à force de refactoring, le code se répète trop). Il faut toutefois prendre garde à ne pas sauter trop vite aux conclusions et tout réécrire continuellement, sans quoi d'une part on n'avance plus et d'autre part, à aller trop vite, on passe à côté de subtilités qui ne sautent pas aux yeux sans examen approfondi (pour ce dernier point, des tests unitaires ou autres peuvent aider à éviter les régressions, encore faut-il avoir le temps ou simplement prendre le temps de les mettre en place).

Comme souvent il s'agit de trouver un juste milieu entre tout réécrire et conserver à tout prix l'existant. Mais quand la réécriture consiste à utiliser quelque chose qui existe par ailleurs et qu'on n'aura donc pas à maintenir soi-même, je pense qu'il n'y a pas à hésiter : si fonctionnellement c'est compatible, ça vaut le coup de jeter le spécifique pour utiliser du natif.

Voilà voilà, félicitations si vous avez tout lu jusqu'ici et n'hésitez pas à réagir dans les commentaires si vous avez quelque chose à ajouter sur le sujet ou des objections à formuler :)