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é.

Pro tips from a seasoned designer

Not all software problems are coding problems

Many problems don’t require more tools (or toys), and more code. More code is always bad anyway, and should be avoided whenever possible. Very often, user’s problem is they can’t see how to bend existing features to fulfill their needs. This is solved by education, aka better documentation and more tutorials, and sometimes by better UI.

Listen but don’t listen to users

Users express what they want and what they like, never what they need. And you don’t need to listen to them to know what it will be:

  1. they will want the same thing as their neighbour just got,
  2. they will like what they are used to.

And then, for everything one likes, you will find another one to dislike it. So the Darktable way of solving conflict is to not solve conflict, but give everyone an option, a mode, a preference to enable that special thing they like, how they like it. This means more case in your switch, more nested if, more codepaths you will need to test now, debug, and maintain in the future, and then more preferences hiding the others in the pref window. Before you know it, the code is a tumor that nobody understands anymore, and fixing it only makes it more complicated.

When you scratch beneath the surface, you find than what people actually need is much closer to other people’s needs than what they say they want. So you can reconcile the needs much easier than the desires, and without compromising. But then you have to trace the root needs below the will, and that takes abstraction skills and psychology.

UI designers are dangerous idiots

Everybody who only sees, focuses and cares about the UI is a dangerous idiot. If your GUI is complicated, it means a lot more than just a “complicated GUI” : it means that the complexity of your backend has reached your frontend. I have found the hard way that GUI complexity is never separate, and can’t be solved separately, from backend complexity and overall application architecture. GUI is not parallel to backend architecture, it’s the termination of it.

The problem of UI designers is they typically don’t code, or if they do, they suck at low-level programming and software architecture. So they focus on what little they see and understand (typical streetlight effect ), and they only produce non-actionnable designs that conflicts with what the software actually needs to work. Because that GUI is only connecting user input to the backend, and if we need that many widgets, it’s because the architecture needs that many inputs. You can’t escape it : to remove widgets, you need to remove inputs, which means your architecture will have to work with fewer degrees of freedom first. That starts with simplifying the backend, which means stinky refactoring of dusty old code nobody understands anymore.

You don’t solve GUI issues with drawings and mockups, you solve GUI issues with solving backend issues. But then you need guys who understand both levels, and they may be too expensive for you.

Ask yourself 36 times per day what was the problem you were trying to solve

It’s super easy to get lost into technicalities when programming in a low-level language and fighting third-party libs or APIs, but sometimes the solution is simple and elegant and you got carried away too far into pointers and thread locks. Always go back to the initial problem at hand, that’s your lifeline to simplicity.

What is the problem ? Who faces it ? When ? How often ? Doing what ?

The best path is the simplest path towards your solution : low techs, little code, few layers.

Document your shitty design

Many times, I completely redid a design while documenting it, because it’s when you try to explain it that your realize it’s too complicated to explain, which means it’s too complicated to understand. If you can’t explain your design in a couple of paragraphs, or your documentation has too many “if this, then that”, there are usually two reasons :

  1. your GUI doesn’t expose the relevant info where user needs it, so you have to link half the documentation in your explanation to redirect users to everything they need to know or check before using the one thing you were documenting. The solution is to bring back relevant info where it’s needed.
  2. your GUI has too many trays, collapsible stuff, contextual behaviours, use cases or hidden preferences, and covering all bases makes you write a novel. The solution is to linearize the workflow, maybe remove options or split features.

GUI is how users control the backend, but it’s also where they learn about existing features and what they do. The documentation should provide context, guidelines and references regarding how we do stuff, but the GUI should explain what it does itself.

Of course, there is an obvious limitation to that : in a photography application, users need to understand photography and its language, which involves things like dynamic range, color gamut, tone mapping, etc. The GUI should be self-explanatory on how it’s supposed to be used, not remove the need to learn the trade (what should be done and how).

Design is an iterative process

An application is a virtual world in which one small change can reorder how the rest of the ecosystem adapts around it. Therefore any design change can trigger the need to change other things around (refactor tools, move widgets, prune features). Which then might trigger the need to correct the initial change again. It’s a step by step process in which it is foolish to even try to get everything right at each step, what matters it that each steps improves the environment from the previous.

Sometimes, (re)design can’t be done by small steps but by large leaps : that’s when you redo the architecture. These leaps will break many things around them, which is ok if the newer architecture is simpler and more robust overall, and if you give it some time to recover before taking the sledgehammer again. But that will create a transient state in which the new design will appear worse than the previous. This is telling us that how the design is percieved is not a valid input : design quality has to be assessed against its goals and evaluated with objective metrics, not with feelings and quick tests.

And sometimes, some steps are mistakes and should be reverted. The sunk cost fallacy  should not be used to justify that some redesign should be kept because it was a lot of work to achieve. It’s expected that all research & development doesn’t make it into production.

Whack-a-mole sessions mean your architecture has run its course

Whether you keep creating new bugs while fixing old ones, or you keep creating edge cases by extending some feature, it all points in the same direction : your architecture can’t be bent any more because it has outgrown its design requirements. It might be that the backend has grown too convoluted or it might be that the existing architecture was really not planned for what you are trying to make it do, but both ways, you will have to redo the architecture and stop hacking in-place. Otherwise, you are only adding technical debt.

But then, the development cost changes scale and that saturday-afternoon project might become a month-long project.

Best-practices are guidelines, not rules

Best-practices help developing sane habits and clean code, unless you don’t understand the problem they tried to solve and use them out of their scope of validity. In that case, they become cargo cult : trying to mimic the effects in the hope that it will magically fix the causes too.

The first that comes to mind is code reuse/code sharing. If reusing the same code for (seemingly similar) features leads to too much internal branching (nested if, switch/case, etc.), to handle all possible paths, what you win on code volume is lost on cyclomatic complexity, and, by the way, your features are not as similar as you thought.

Also, duplicating code might be a starting point to locally optimize the duplicate later: once you have the complete procedure in front of you, you may spot steps that can be cached or factorized. Whereas if the procedure is only opaque, high-level, reusable API methods, then you loose the ability to spot and remove redundant computations. So, there is a principle of data reuse/sharing (aka caching computed data that will be used later with no change, to spare CPU cycles) that can be made impossible by code reuse/sharing, because it obfuscates and abstracts data lifecycle.

This becomes critical on pixel loops: you want to collapse all pixel-wise operations into the same loop, to pay the memory I/O price only once. Which means that you will have to re-implement the same affine correction ($y = a * x + b$) into each loop using it, rather than having a reusable method that does just that in its own loop.

Don’t brace yourself

If you find yourself overwhelmed by some cryptic and random bugs that keep coming and that you can’t make sense of, don’t keep fighting blindly and take a step back. Then instrument debug helpers, or higher-level managers that keep track of internal states and give you a map of the software data values at any relevant point in its lifecycle. This is especially critical in asynchronous setups, where several threads create, access or compute stuff in parallel on different timelines, and the actual ordering sequence depends on runtime context.


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