Ansel est conçu, pas bidouillé. Les bidouilleurs peuvent se réjouir de “travailler” à accélérer la mort de Darktable en augmentant sa dette technique .

Qu’est-ce que la conception ?

La conception est un processus par lequel vous déroulez une méthodologie pour apporter une solution technique à un problème humain. Le processus de conception vise à converger vers la solution la plus appropriée, tout en combattant la tendance naturelle à se précipiter vers la première ou l’idée la plus confortable.

Sans exigences et conception, programmer est l’art d’ajouter des bugs à un fichier texte vide.
  1. La conception commence par un cas d’utilisation : une tâche définie à réaliser (sur une image), par un utilisateur défini, dans un délai défini. S’il n’y a pas de cas d’utilisation, alors pas de problème à résoudre, alors restez à l’écart de votre éditeur de code.
  2. La conception nécessite de connaître l’utilisateur cible : éducation/formation, niveau de maîtrise, etc.
  3. La conception nécessite de comprendre les besoins : dans le cadre d’Ansel, cela nécessitera souvent quelques connaissances en histoire de l’art et en photographie argentique,
  4. Une fois le problème et l’utilisateur compris, la conception nécessite de spécifier :
    • les fonctionnalités attendues de la solution,
    • la portée de la solution (où se situe la solution dans le cycle de vie d’une image ?),
    • les contraintes et exigences de la solution (supportant une norme, permettant de traiter n images par unité de temps, etc.),
    • une série de tests à réaliser pour valider la qualité de la solution, afin de limiter les opinions non productives, les biais et la subjectivité dans le processus de validation.
  5. Seulement après les maquettes et les brainstormings peuvent commencer, suivis de prototypes.

Qu’est-ce que la conception n’est pas ?

La conception ne traite pas avec :

  • exigences vagues d’un utilisateur indéfini, futur ou fantasmé,
  • “cela serait cool si…” (c’est comme ça que vous créez des collections de plugins incohérentes),
  • ce que les gens aiment (pour tout ce que quelqu’un aime, vous trouverez quelqu’un pour le détester),
  • ce que les gens pensent vouloir (ce n’est souvent pas ce dont ils ont besoin),
  • mots à la mode technologique magiques qui “sont l’avenir” et, en tant que tels, doivent être branchés partout, indépendamment de leur pertinence ou faisabilité (oui, je parle de l’IA, des NFT, de la blockchain, etc.)

Ce qu’est un bon design

Un bon design est :

  • minimaliste,
  • robuste,
  • à l’épreuve du temps,
  • générique et généralisé,
  • maintenable avec des ressources limitées,
  • informé par la science,
  • compatible/interopérable avec les normes de l’industrie.

Étant donné qu’Ansel est une application basée sur le workflow, le bon design prend également en compte le workflow dans son ensemble, et où le problème/solution s’intègrent.

Comment se fait un bon design ?

Pour aider le processus de conception, la communication doit rester concise, à propos, et les personnes participant à ce processus doivent s’assurer qu’elles ont une bonne compréhension de la théorie et du fond technique impliqués dans l’étendue du problème/solution.

Il convient de souligner que, bien que le projet soit axé sur le logiciel, toutes les solutions n’impliquent pas de codage. Parfois (souvent ?), une meilleure éducation ou une meilleure documentation est tout ce qu’il faut.

L’objectif d’un processus de conception sain est d’éviter de biaiser trop tôt les solutions avec son design/tech favori et d’éviter de se perdre dans les détails techniques, mais de toujours revenir aux principes de base de ce que nous faisons : post-traiter éventuellement de grands lots d’images brutes pour tous types de supports de sortie.

Cela est soutenu par le fait que les utilisateurs connaissent rarement leurs propres besoins, ou plutôt, les besoins qu’ils expriment sont rarement la racine de ce qu’ils veulent réellement. La tâche difficile de la conception est de couper à travers les branches pour aller à la racine, car résoudre le problème racine aboutit généralement à des solutions plus élégantes, génériques et minimalistes.

Les problèmes viennent en premier

La première étape du processus de conception d’Ansel est de soumettre une demande de fonctionnalité sur le Community. Les demandes de fonctionnalités ont été déplacées hors de Github car cette plateforme n’est pas accueillante pour les non-programmeurs et les non-anglophones (même si la Communauté ne prend en charge que le français et l’anglais).

Cette demande de fonctionnalité se concentrera sur le problème à résoudre et s’abstiendra de proposer toute solution. Le problème sera défini en termes de tâches à réaliser dans le workflow d’un photographe ou de résultat visuel attendu de l’image traitée, c’est-à-dire en termes de l’objectif final à atteindre, pas en termes d’outillage ou de technicité qui semblent nécessaires. Cela peut conduire à une discussion pour creuser les racines du problème, qui sont généralement bien cachées sous ce que l’utilisateur pense être son problème .

Aucune proposition de solution n’est acceptée à ce stade.

Les solutions viennent en second

Lorsque la définition et l’étendue du problème sont convenues entre les personnes impliquées dans la discussion, des solutions peuvent être proposées. Une discussion supplémentaire peut être nécessaire pour évaluer les inconvénients et les avantages de chaque solution, conduisant à l’adoption de la meilleure solution en principe. Les solutions sont définies par leurs fonctionnalités (c’est-à-dire ce qu’elles doivent faire), pas par leur technologie ou leurs moyens (comment elles devraient le faire).

Aucune proposition de prototype n’est acceptée à ce stade.

Les solutions adoptées entraîneront la création d’un nouveau problème trié dans le tableau Kanban de gestion de projet .

Elles pourraient être conditionnées à la recherche d’aspects théoriques et techniques pour évaluer leur faisabilité, auquel cas elles seront triées dans la colonne À rechercher du tableau Kanban. Les résultats de la recherche seront ajoutés au problème original jusqu’à ce que la faisabilité de la solution soit prouvée. Lorsqu’elle l’est, le problème sera déplacé dans la colonne “À faire” du tableau Kanban.

Les solutions adoptées peuvent être directement triées dans la colonne À faire si elles nécessitent uniquement des outils et technologies bien connus.

Idéalement, les points à tester et la procédure de test pour valider le prototype devraient être rédigés avant même d’avoir un prototype fonctionnel. Au minimum, les tests devraient garantir qu’aucune régression n’a eu lieu dans les fonctionnalités et outils associés.

Les prototypes viennent en troisième position

Seuls les problèmes triés dans la colonne “À faire” du tableau Kanban de gestion de projet  seront travaillés, par moi-même ou par quiconque souhaite les aborder.

Le prototype de la solution sera proposé dans une demande de tirage d’une branche thématique liée au problème original. Les branches thématiques doivent être rebasées sur la branche master, par exemple git rebase upstream master ou, si vous mettez à jour votre branche localement avec de nouveaux commits master, faites git pull ustream master --rebase ou configurez git pour qu’il tire globalement  via rebase plutôt que via merge. Cela garantit que l’historique de votre branche est maintenu propre avec un effort minimal, et garde également l’historique du master propre lorsque votre PR est fusionné.

Lorsque la demande de tirage du prototype est examinée et si elle respecte les normes de qualité du code (voir ci-dessous) tout en répondant aux spécifications de la solution adoptée, elle est approuvée et automatiquement triée dans la colonne “À tester/valider” du tableau Kanban de gestion de projet .

La validation vient en quatrième place

Les requêtes pull approuvées seront fusionnées tôt dans la branche candidate ou dev pour les tests, selon qu’elles puissent ou non casser les historiques d’édition d’images (en ajoutant de nouveaux paramètres de module ou en modifiant le schéma de base de données). Cette branche sera toujours la branche master avec toutes les requêtes pull en attente de validation sur le dessus. Cela est destiné à aider les personnes qui ne sont pas nécessairement à jour avec la fusion manuelle des branches git à tester. Contrairement à la branche dev, candidate ne devrait pas casser vos modifications.

Si aucun bug ou problème n’est signalé après un certain temps et que le prototype remplit correctement son objectif initial, il sera fusionné dans master et le problème lié sera fermé et déplacé dans la colonne “Terminé” du tableau Kanban de gestion de projet .

Si le prototype se révèle insatisfaisant, il peut être rejeté et un autre devra être élaboré.

Conseils de pro d’un concepteur chevronné

Tous les problèmes logiciels ne sont pas des problèmes de code

Beaucoup de problèmes ne nécessitent pas davantage d’outils (ou de jouets), ni plus de code. Plus de code est de toute façon toujours une mauvaise chose, et devrait être évité chaque fois que possible. Très souvent, le problème de l’utilisateur est qu’il ne voit pas comment plier les fonctionnalités existantes pour répondre à ses besoins. Cela se résout par l’éducation, c’est-à-dire une meilleure documentation et davantage de tutoriels, et parfois par une meilleure interface.

Écoutez les utilisateurs, mais ne les écoutez pas

Les utilisateurs expriment ce qu’ils veulent et ce qu’ils aiment, jamais ce dont ils ont besoin. Et vous n’avez pas besoin de les écouter pour savoir ce que ce sera :

  1. ils voudront la même chose que celle que leur voisin vient d’obtenir,
  2. ils aimeront ce à quoi ils sont habitués.

Et puis, pour tout ce que l’un aime, vous en trouverez un autre pour le détester. Ainsi, la façon de Darktable de résoudre les conflits est de ne pas les résoudre, mais de donner à chacun une option, un mode, une préférence pour activer cette chose particulière qu’il aime, comme il l’aime. Cela signifie plus de case dans votre switch, plus de if imbriqués, plus de chemins d’exécution que vous devrez désormais tester, déboguer et maintenir à l’avenir, puis plus de préférences masquant les autres dans la fenêtre de préférences. Avant même de vous en rendre compte, le code est une tumeur que plus personne ne comprend, et la corriger ne fait que la rendre plus compliquée.

Lorsque vous grattez sous la surface, vous découvrez que ce dont les gens ont réellement besoin est bien plus proche des besoins des autres que ce qu’ils disent vouloir. Vous pouvez donc réconcilier les besoins bien plus facilement que les désirs, et sans compromis. Mais il vous faut alors remonter aux besoins fondamentaux sous la volonté, et cela demande des capacités d’abstraction et de la psychologie.

Les concepteurs d’interface sont de dangereux idiots

Quiconque ne voit, ne se concentre et ne se soucie que de l’interface est un dangereux idiot. Si votre interface graphique est compliquée, cela signifie bien plus qu’une simple « interface compliquée » : cela signifie que la complexité de votre backend a atteint votre frontend. J’ai découvert à mes dépens que la complexité de l’interface n’est jamais séparée, et ne peut être résolue séparément, de la complexité du backend et de l’architecture globale de l’application. L’interface n’est pas parallèle à l’architecture du backend, elle en est la terminaison.

Le problème des concepteurs d’interface est qu’ils ne codent généralement pas, ou s’ils le font, ils sont nuls en programmation bas niveau et en architecture logicielle. Ils se concentrent donc sur le peu qu’ils voient et comprennent (l’effet réverbère  typique), et ne produisent que des conceptions non actionnables qui entrent en conflit avec ce dont le logiciel a réellement besoin pour fonctionner. Parce que cette interface ne fait que relier les entrées de l’utilisateur au backend, et si nous avons besoin d’autant de widgets, c’est que l’architecture a besoin d’autant d’entrées. Vous ne pouvez pas y échapper : pour supprimer des widgets, vous devez supprimer des entrées, ce qui signifie que votre architecture devra fonctionner avec moins de degrés de liberté d’abord. Cela commence par simplifier le backend, ce qui signifie un refactoring nauséabond de vieux code poussiéreux que plus personne ne comprend.

Vous ne résolvez pas les problèmes d’interface avec des dessins et des maquettes, vous résolvez les problèmes d’interface en résolvant les problèmes de backend. Mais alors il vous faut des gens qui comprennent les deux niveaux, et ils peuvent être trop chers pour vous.

Demandez-vous 36 fois par jour quel était le problème que vous essayiez de résoudre

Il est super facile de se perdre dans les détails techniques lorsqu’on programme dans un langage bas niveau et qu’on se bat contre des bibliothèques ou API tierces, mais parfois la solution est simple et élégante et vous vous êtes laissé entraîner trop loin dans les pointeurs et les verrous de threads. Revenez toujours au problème initial à traiter, c’est votre bouée de sauvetage vers la simplicité.

Quel est le problème ? Qui y est confronté ? Quand ? À quelle fréquence ? En faisant quoi ?

Le meilleur chemin est le chemin le plus simple vers votre solution : peu de technique, peu de code, peu de couches.

Documentez votre conception merdique

À maintes reprises, j’ai entièrement refait une conception en la documentant, parce que c’est en essayant de l’expliquer qu’on se rend compte qu’elle est trop compliquée à expliquer, ce qui signifie qu’elle est trop compliquée à comprendre. Si vous ne pouvez pas expliquer votre conception en quelques paragraphes, ou si votre documentation contient trop de « si ceci, alors cela », il y a généralement deux raisons :

  1. votre interface n’expose pas l’information pertinente là où l’utilisateur en a besoin, si bien que vous devez lier la moitié de la documentation dans votre explication pour rediriger les utilisateurs vers tout ce qu’ils ont besoin de savoir ou de vérifier avant d’utiliser la seule chose que vous documentiez. La solution est de ramener l’information pertinente là où elle est nécessaire.
  2. votre interface comporte trop de tiroirs, d’éléments repliables, de comportements contextuels, de cas d’usage ou de préférences cachées, et couvrir tous les cas vous oblige à écrire un roman. La solution est de linéariser le flux de travail, éventuellement supprimer des options ou scinder des fonctionnalités.

L’interface est la façon dont les utilisateurs contrôlent le backend, mais c’est aussi là qu’ils découvrent les fonctionnalités existantes et ce qu’elles font. La documentation devrait fournir le contexte, les lignes directrices et les références concernant la manière dont nous faisons les choses, mais l’interface devrait expliquer d’elle-même ce qu’elle fait.

Bien sûr, il y a une limitation évidente à cela : dans une application de photographie, les utilisateurs doivent comprendre la photographie et son langage, ce qui implique des notions comme la plage dynamique, le gamut de couleurs, le mappage tonal, etc. L’interface devrait être explicite quant à la façon dont elle est censée être utilisée, non supprimer le besoin d’apprendre le métier (ce qui devrait être fait et comment).

La conception est un processus itératif

Une application est un monde virtuel dans lequel un petit changement peut réorganiser la façon dont le reste de l’écosystème s’adapte autour de lui. Par conséquent, tout changement de conception peut déclencher le besoin de changer d’autres choses autour (refactoriser des outils, déplacer des widgets, élaguer des fonctionnalités). Ce qui peut ensuite déclencher le besoin de corriger à nouveau le changement initial. C’est un processus pas à pas dans lequel il est stupide ne serait-ce que d’essayer de tout réussir à chaque étape ; ce qui compte, c’est que chaque étape améliore l’environnement par rapport à la précédente.

Parfois, la (re)conception ne peut se faire par petits pas mais par grands bonds : c’est quand on refait l’architecture. Ces bonds casseront beaucoup de choses autour d’eux, ce qui n’est pas grave si la nouvelle architecture est globalement plus simple et plus robuste, et si vous lui laissez un peu de temps pour se rétablir avant de reprendre la masse. Mais cela créera un état transitoire dans lequel la nouvelle conception paraîtra pire que la précédente. Cela nous indique que la façon dont la conception est perçue n’est pas une entrée valide : la qualité d’une conception doit être évaluée par rapport à ses objectifs et mesurée avec des métriques objectives, non avec des ressentis et des tests rapides.

Et parfois, certaines étapes sont des erreurs et devraient être annulées. Le sophisme des coûts irrécupérables  ne devrait pas servir à justifier qu’une refonte soit conservée parce qu’elle a demandé beaucoup de travail. Il est normal que toute la recherche et développement n’arrive pas jusqu’en production.

Les séances de tape-taupe signifient que votre architecture a fait son temps

Que vous créiez sans cesse de nouveaux bugs en corrigeant les anciens, ou que vous créiez sans cesse des cas limites en étendant une fonctionnalité, tout pointe dans la même direction : votre architecture ne peut plus être pliée davantage parce qu’elle a dépassé ses exigences de conception. Il se peut que le backend soit devenu trop alambiqué ou que l’architecture existante n’ait vraiment pas été prévue pour ce que vous essayez de lui faire faire, mais dans les deux cas, vous devrez refaire l’architecture et cesser de bricoler sur place. Sinon, vous ne faites qu’ajouter de la dette technique.

Mais alors, le coût de développement change d’échelle et ce projet d’un samedi après-midi pourrait devenir un projet d’un mois.

Les bonnes pratiques sont des lignes directrices, non des règles

Les bonnes pratiques aident à développer des habitudes saines et un code propre, sauf si vous ne comprenez pas le problème qu’elles cherchaient à résoudre et que vous les utilisez hors de leur domaine de validité. Dans ce cas, elles deviennent du culte du cargo : essayer d’imiter les effets dans l’espoir que cela corrigera aussi les causes comme par magie.

La première qui vient à l’esprit est la réutilisation/le partage de code. Si réutiliser le même code pour des fonctionnalités (apparemment similaires) conduit à trop de branchements internes (if imbriqués, switch/case, etc.) pour gérer tous les chemins possibles, ce que vous gagnez en volume de code est perdu en complexité cyclomatique et, au passage, vos fonctionnalités ne sont pas aussi similaires que vous le pensiez.

De plus, dupliquer du code peut être un point de départ pour optimiser localement le doublon plus tard : une fois que vous avez la procédure complète sous les yeux, vous pouvez repérer des étapes qui peuvent être mises en cache ou factorisées. Alors que si la procédure n’est constituée que de méthodes d’API opaques, de haut niveau et réutilisables, vous perdez la capacité de repérer et d’éliminer les calculs redondants. Ainsi, il existe un principe de réutilisation/partage de données (c’est-à-dire mettre en cache des données calculées qui seront réutilisées plus tard sans changement, pour épargner des cycles CPU) qui peut être rendu impossible par la réutilisation/le partage de code, parce qu’il obscurcit et abstrait le cycle de vie des données.

Cela devient critique sur les boucles de pixels : vous voulez fusionner toutes les opérations pixel par pixel dans la même boucle, afin de ne payer qu’une seule fois le prix des E/S mémoire. Ce qui signifie que vous devrez réimplémenter la même correction affine ($y = a * x + b$) dans chaque boucle qui l’utilise, plutôt que d’avoir une méthode réutilisable qui ne fait que cela dans sa propre boucle.

Ne vous crispez pas

Si vous vous retrouvez submergé par des bugs cryptiques et aléatoires qui reviennent sans cesse et auxquels vous ne trouvez aucun sens, ne continuez pas à vous battre à l’aveugle et prenez du recul. Instrumentez alors des aides au débogage, ou des gestionnaires de plus haut niveau qui suivent les états internes et vous donnent une carte des valeurs de données du logiciel à tout point pertinent de son cycle de vie. C’est particulièrement critique dans les configurations asynchrones, où plusieurs threads créent, accèdent ou calculent des choses en parallèle sur des chronologies différentes, et où la séquence d’ordonnancement réelle dépend du contexte d’exécution.


Translated from English by : Aurélien Pierre, ChatGPT, Claude. In case of conflict, inconsistency or error, the English version shall prevail.