Chaque personne qui joue un rôle d’architecte adore les schémas avec des rectangles pour montrer à quel point le niveau d’architecture proposé est modulaire, technologique et intelligent.
Certes cette approche peut être utile pour la communication d’idées avec les décideurs et l’équipe technique, elle cache néanmoins une partie de la vérité.
Law of leaky abstraction (la loi des abstractions fuyantes) nous permet de voir l’image caché derrière nos architectures découplées. L’idée de cette loi provient de Joel Spolsky (https://www.joelonsoftware.com/2002/11/11/the-law-of-leaky-abstractions/) : si vous avez une architecture (technique ou applicative) bien découplée, cela ne veut jamais dire que vous pouvez cacher tous les détails de sa réalisation.
Autrement dit, quand vous dessinez vos rectangles pour décrire votre architecture, il faut toujours avoir à l’esprit que ces boîtes sont floues et qu’elles ont inévitablement des intersections.
Selon moi, cette loi est aujourd’hui encore plus importante qu’au moment quand elle a été formulée, sans pour autant qu’on ne l’apprenne à l’école.
Exemples classiques – SQLs et ORMs
Les bases de données relationnelles sont si populaires car elles nous donnent une abstraction très utile. Nous n’avons plus besoin d’écrire le code qui ouvre des fichiers, itère par des lignes des fichiers, crée des dictionnaires pour agréger les résultats. Nous pouvons juste exécuter une requête SQL type select code,sum(montant) from my_table group by code
. C’est une superbe abstraction et c’est tout sauf un hasard s’il y a des personnes qui savent optimiser ces requêtes…
Autrement dit, SQL est suffisamment complexe pour qu’on puisse exprimer la même demande mais de manière différente tout en obtenant une requête qui s’exécute 100 fois plus vite (ou plus long) qu’une autre.
L’exemple classique : si nous faisons une jointure entre trois tables qui ont des champs communs, il ne faut jamais ajouter trop de conditions. En effet, même si logiquement cela ne change rien, techniquement le plan d’exécution d’une telle requête peut être non-optimal :
select * from table1, table2, table3 where table1.column1=table2.column1 and table1.column1=table3.column1 and table2.column1=table3.column1 --condition de trop
Nous utilisons une abstraction, un modèle mental et nous n’avons pas forcément besoin d’aller regarder sous le capot de notre base de données tant que tout fonctionne comme prévu, mais de temps en temps notre abstraction ne fonctionne plus… et là, nous avons une abstraction qui fuit.
La situation est encore plus complexe avec l’utilisation des ORMs (i.e. les bibliothèques qui font une abstraction entre un langage de programmation et une base de données). À titre d’exemple, un développeur web m’énonçait qu’il n’est pas obligé de connaître le SQL pour programmer son application – l’ORM s’en occupe ». I.e. ici nous avons une abstraction devant une autre abstraction. Au lieu d’utiliser les fichiers, on utilise la base de données, mais au lieu de faire un « select » depuis cette base de données, il utilise le code type customerData = customers.get(17)
et puis l’outil s’occupe de génération du code SQL et de la préparation d’objet de retour. Encore une fois, l’ORM fonctionne bien jusqu’au moment quand il ne fonctionne plus et pour comprendre le problème, nous devons passer par une couche de plus pour déterminer la cause de problème. Plus tard, le même développeur m’expliquait que l’approche ORM c’est compliqué car les requêtes sont non-optimales et en plus, c’est difficile à maintenir…
Conclusion : vos abstractions auront des fuites, donc restez « proches » de la technologie employée.
No-code/low-code
J’adore le mouvement « no-code/low-code », surtout sa partie dite « self-service » pour les utilisateurs. Malheureusement, il existe des domaines où les changements n’iront pas si vite…
Par exemple, quand j’entends « no-code/low-code pour l’intégration d’information », mon merde-o-metre interne approche de la zone rouge. Je fais ici allusion à un pub d’une société française spécialisée…
Pourquoi ? Supposons que les informaticiens configurent l’outil et les connexions vers les bases de données. Supposons aussi que les informaticiens importent les métadonnées pour que les outils fonctionnent. Supposons que le métier connait le modèle de données et la notion de la jointure. Supposons que l’interface d’intégration est jolie et compréhensible.
Mais… durant l’import de données, il s’avère que cet outil (qui fait une abstraction entre nous et les détails techniques de flux de données) utilise des séparateurs de champs. Hors si ces séparateurs existent aussi dans vos données, l’outil ne fonctionnera plus et ce n’est qu’un informaticien qui connait l’outil qui pourra aller dans les débris des requêtes SQL/code Java pour trouver et corriger la cause. Pourtant les utilisateurs sont censés être indépendant ? La promesse c’était bien qu’un informaticien n’était pas nécessaire ?
Loin de moi l’idée de vouloir incriminer quelqu’un, mais sachez que j’ai personnellement ouvert un ticket sur le sujet il y a 6 ans et à ce jour, le problème n’est toujours pas résolu, à ma connaissance. 🙂
C’est juste une démonstration claire des problèmes potentiels, je ne parle pas de code sous-optimal, complexité de sujets de qualité de données, de consolidation, etc.
Nos ordinateurs
Il y a quelques temps, j’ai parlé avec un expert canadien qui mettait en place un système de rapprochement de données (identification de doublons dans les Master Data). Il me disait qu’ils ont acheté un système qui « n’est même pas parallèle, alors que dans nos ordinateurs il y a déjà des dizaines de cores ».
Alors, je comprends parfaitement le point, mais j’ai des choses à dire pour protéger nos amis développeurs. Imaginons une seconde un CPU, le cerveau de votre ordinateur.
Le CPU ne peut rien faire sans accès à la mémoire vive (RAM)… i.e. tout le temps le CPU « parle » avec la mémoire pour lire et écrire les données. Le CPU reçoit les commandes depuis la mémoire, il exécute ces commandes, décide qu’il doit lire les donnée, il les récupère, fait un calcul et sauvegarde ses résultats.
Même si nous regardons plus bas, au niveau de langage de programmation le plus proche de CPU – assembler, nous allons y trouver la commande « MOV » (move, déplacer), par exemple. Cette commande peut lire les données depuis la mémoire vive et écrire dans la mémoire vive. Voilà j’exécuté « mov [2000H],eax
» et les données depuis le registre EAX de CPU sont envoyées à l’adresse 2000H de la mémoire vive. N’est-ce pas ?
Oops. Non. Dans nos rêves seulement. C’est aussi un modèle mental. Le CPU et la mémoire sont bien plus complexes. Notamment, il est trop cher pour le CPU de mettre à jour la mémoire vive tout de suite, c’est pour cela qu’il a des « caches » où il garde toutes modifications avant de les envoyer vers la mémoire vive. En fait, votre commande de la mise à jour peut ne jamais mettre à jour la RAM… pourtant c’est la commande que nous avons envoyé.
Ces optimisations permettent à nos ordinateurs de fonctionner plus vite, mais cela complexifie le développement de programmes parallèles. Il est facile de lancer plusieurs programmes, mais il est très compliqué de faire en sorte qu’un seul programme utilise plusieurs cores de CPU justement à cause de fait qu’il faut toujours vérifier si la mémoire a bien été mise à jour par le processus parallèle.
Même cette abstraction fuit ! Si vous voulez des détails googlez « memory barrier », « memory model », « happens before »…
Microservices
Dans une autre vie, j’ai cherché un développeur Java pour maintenir notre application. J’ai trouvé une personne brillante avec des compétences extraordinaires. La première proposition qu’elle a fait après une semaine d’analyse de l’application c’est de passer en « microservices ». Malheureusement, elle a été déçue par ma réaction et voici pourquoi :
- le code que nous écrivons suppose que l’appel de service web c’est presque comme l’utilisation d’un objet local (via factory, par exemple) ;
- …mais cette abstraction a des fuites :
- …et si le réseau n’est pas disponible ;
- …et si je dois être « transactionnel », alors une partie de services peut refuser la transaction ;
- …et s’il faut faire un « restore » des bases de données et quoi faire si les bases de données ne sont pas restaurées jusqu’au point « synchronisé » ;
- etc.
Du coup, la proposition « faire comme avant, mais mieux » ne marche pas. Au cas où, je suis bien au courant qu’il existe des transactions distribuées, message queues, sagas, etc… mais nous ne sommes ni Netflix ni Amazon pour gérer une telle complexité afin de gagner quelques points au niveau d’architecture.
Cloud
J’adore le mouvement « cloud »… et je pense que chaque chef de projet qui a du attendre plusieurs mois avant l’installation de son serveur par les techniciens vous confirmera qu’il y a une différence entre quelques clicks qu’il peut potentiellement faire lui-même avec 1 minute d’attente tout au plus et plusieurs mois d’échanges avec des tonnes de fiches de sécurité à remplir. Cependant, le cloud est parfois un exemple d’abstractions qui fuient…
Supposons que vous faites partie d’une multinationale qui décide de mettre en place un ERP unique. Probablement, vous avez au moins deux choix :
- centraliser l’effort « soft », mais laisser les SI techniquement indépendants (installés dans les locaux sur les serveurs physiques en France, aux US, au Japon, etc) ;
- centraliser aussi la gestion de « hard » – et tout envoyer vers le cloud.
La solution est claire – mettons tout dans le cloud : c’est plus simple car Amazon (Google, Micro$oft, IBM, Orange, etc) s’occupe de la gestion de serveurs, notre SI peut être distribué, peut utiliser les « managed services », etc.
Du coup, c’est comme « chez nous », mais en mieux… à la condition d’avoir Internet… et si votre business touche certains pays d’Asie, d’Afrique, d’Amérique de Sud (et même certains régions en France), vous prenez le risque d’apprendre « l’inégalité numérique » the hard way.
Il y a une autre chose : app en cloud n’est pas « comme app sur un seul serveur, mais mieux »… si tout ce que vous faites c’est une application CRUD – il n’y aura pas de différence probablement, mais il vaut mieux que votre application commence à respecter certaines normes – par exemple elle doit être « stateless » si possible, i.e. garder l’état de choses que dans une base de données, sinon si vous avez besoin de cache complexe pour les données « chaudes », vous allez avoir les mêmes problèmes que avec les caches CPU, mais au niveau suivant de la complexité (sinon voir théorème CAP).
Conclusion
Le monde informatique se base sur des abstractions et des modèles mentaux, mais chaque modèle mental complexe a ses défauts. Probablement, nous devrions y réfléchir avant d’ajouter de la complexité à tous nos projets ?
Si vous avez du temps, que vous comprenez l’anglais, que vous voulez un avis « frais » et que vous n’avez pas peur de sujets techniques, voici une vision de la personne qui n’utilise presque aucune abstraction pour son travail car Game Dev c’est du hardcore :
Bonne santé à vous et à vos systèmes.