Apprenez les principes d'abstraction des données de Barbara Liskov pour concevoir des interfaces stables, réduire les ruptures et construire des systèmes maintenables avec des API claires et fiables.

Barbara Liskov est une informaticienne dont les travaux ont discrètement façonné la manière dont les équipes logicielles modernes construisent des systèmes qui ne s'effondrent pas. Ses recherches sur l'abstraction des données, la protection de l'information et, plus tard, le principe de substitution de Liskov (LSP) ont influé sur tout, des langages de programmation à notre façon courante de penser les API : définir un comportement clair, protéger les internals et rendre sûr le fait que d'autres dépendent de votre interface.
Une API fiable n'est pas juste « correcte » au sens théorique. C'est une interface qui aide un produit à avancer plus vite :
Cette fiabilité est une expérience : pour le développeur qui appelle votre API, pour l'équipe qui la maintient, et pour les utilisateurs qui en dépendent indirectement.
L'abstraction des données consiste à faire interagir les appelants avec un concept (un compte, une file, un abonnement) via un petit ensemble d'opérations — pas via les détails désordonnés de comment c'est stocké ou calculé.
Quand vous cachez les détails de représentation, vous éliminez des catégories entières d'erreurs : plus personne ne peut « par accident » s'appuyer sur un champ de base de données qui n'était pas destiné au public, ni muter un état partagé d'une manière que le système ne supporte pas. Autre point important : l'abstraction réduit le coût de coordination : les équipes n'ont pas besoin d'autorisation pour refactorer les internals tant que le comportement public reste cohérent.
À la fin de cet article, vous aurez des façons pratiques de :
Si vous voulez un résumé rapide plus tard, allez à /blog/a-practical-checklist-for-designing-reliable-apis.
L'abstraction des données est une idée simple : vous interagissez avec quelque chose par ce qu'il fait, pas par la façon dont il est construit.
Pensez à un distributeur automatique. Vous n'avez pas besoin de savoir comment les moteurs tournent ou comment les pièces sont comptées. Il suffit des commandes (« sélectionner un article », « payer », « recevoir l'article ») et des règles (« si vous payez assez, vous obtenez l'article ; si c'est en rupture, vous êtes remboursé »). Voilà l'abstraction.
En logiciel, l'interface est le « ce que ça fait » : noms des opérations, données d'entrée acceptées, sorties produites et erreurs à attendre. L'implémentation est le « comment ça marche » : tables de base, stratégie de cache, classes internes et astuces de performance.
Garder ces deux niveaux séparés permet d'avoir des API stables même quand le système évolue. Vous pouvez réécrire les internals, changer de bibliothèque ou optimiser le stockage — l'interface reste la même pour les utilisateurs.
Un type de données abstrait est un « conteneur + opérations autorisées + règles », décrit sans s'engager sur une structure interne particulière.
Exemple : une Stack (LIFO).
La clé est la promesse : pop() retourne le dernier push(). Que la pile utilise un tableau, une liste chaînée ou autre chose est privé.
La même séparation s'applique partout :
POST /payments est l'interface ; contrôles antifraude, retries et écritures en base sont l'implémentation.client.upload(file) est l'interface ; découpage en chunks, compression et requêtes parallèles sont l'implémentation.Quand vous concevez avec l'abstraction, vous vous concentrez sur le contrat que les utilisateurs attendent — et vous vous donnez la liberté de changer tout ce qu'il y a derrière sans les casser.
Un invariant est une règle qui doit toujours être vraie à l'intérieur d'une abstraction. Si vous concevez une API, les invariants sont les garde-fous qui empêchent vos données de dériver vers des états impossibles — comme un compte bancaire avec deux monnaies à la fois, ou une commande « complétée » sans article.
Considérez un invariant comme « la forme de la réalité » pour votre type :
Cart ne peut pas contenir de quantités négatives.UserEmail est toujours une adresse email valide (pas « validée plus tard »).Reservation a start < end, et les deux dates sont dans le même fuseau.Si ces affirmations cessent d'être vraies, votre système devient imprévisible, car chaque fonctionnalité doit deviner ce que signifie une donnée « cassée ».
De bonnes APIs font respecter les invariants aux frontières :
Cela améliore naturellement le traitement d'erreurs : au lieu d'échecs vagues plus tard (« quelque chose s'est mal passé »), l'API peut expliquer quelle règle a été violée (« end doit être après start »).
Les appelants ne devraient pas avoir à mémoriser des règles internes du type « cette méthode marche seulement après avoir appelé normalize() ». Si un invariant dépend d'un rituel spécial, ce n'est pas un invariant — c'est un piège.
Concevez l'interface pour que :
Quand vous documentez un type d'API, notez :
Une bonne API n'est pas juste un ensemble de fonctions — c'est une promesse. Les contrats rendent cette promesse explicite, afin que les appelants puissent se fier au comportement et que les mainteneurs puissent changer l'implémentation sans surprendre personne.
Au minimum, documentez :
Cette clarté rend le comportement prévisible : les appelants savent quelles entrées sont sûres et quels résultats gérer, et les tests peuvent vérifier la promesse au lieu d'en deviner l'intention.
Sans contrats, les équipes se fient à la mémoire et aux normes informelles : « ne passez pas null ici », « cet appel réessaie parfois », « il renvoie vide en cas d'erreur ». Ces règles se perdent au fil de l'onboarding, des refactors ou des incidents.
Un contrat écrit transforme ces règles cachées en savoir partagé. Il devient aussi une cible stable pour les revues de code : les discussions deviennent « ce changement satisfait-il toujours le contrat ? » au lieu de « chez moi ça marchait ».
Vague : « Crée un utilisateur. »
Mieux : « Crée un utilisateur avec un email unique.
email doit être une adresse valide ; l'appelant doit avoir la permission users:create.userId ; l'utilisateur est persisté et immédiatement récupérable.409 si l'email existe déjà ; retourne 400 pour champs invalides ; aucun utilisateur partiel n'est créé. »Vague : « Récupère des éléments rapidement. »
Mieux : « Retourne jusqu'à limit éléments triés par createdAt décroissant.
nextCursor pour la page suivante ; les cursors expirent après 15 minutes. »L'information hiding est l'aspect pratique de l'abstraction des données : les appelants doivent dépendre de ce que l'API fait, pas de comment elle le fait. Si les utilisateurs ne voient pas vos internals, vous pouvez les changer sans transformer chaque release en un breaking change.
Une bonne interface publie un petit ensemble d'opérations (create, fetch, update, list, validate) et garde la représentation — tables, caches, files, limites de service — privée.
Par exemple, « ajouter un article au panier » est une opération. « CartRowId » de votre base est un détail d'implémentation. Quand vous exposez ce détail, vous invitez les utilisateurs à construire leur logique dessus, ce qui fige votre capacité à changer.
Quand les clients ne dépendent que d'un comportement stable, vous pouvez :
…et l'API reste compatible parce que le contrat n'a pas bougé. C'est le vrai avantage : stabilité pour les utilisateurs, liberté pour les mainteneurs.
Quelques façons dont les internals s'échappent accidentellement :
status=3 au lieu d'un nom clair ou d'une opération dédiée.Préférez des réponses qui décrivent le sens, pas le mécanisme :
"userId": "usr_...") plutôt que des numéros de ligne en base.Si un détail peut changer, ne le publiez pas. Si les utilisateurs en ont besoin, promouvez-le en tant que partie délibérée et documentée de la promesse d'interface.
Le Principe de Substitution de Liskov (LSP) en une phrase : si un morceau de code fonctionne avec une interface, il doit continuer à fonctionner quand vous y substituez n'importe quelle implémentation valide de cette interface — sans cas spéciaux.
LSP parle moins d'héritage et plus de confiance. Quand vous publiez une interface, vous faites une promesse sur le comportement. LSP dit que chaque implémentation doit tenir cette promesse, même si elle utilise une approche interne très différente.
Les appelants se fient à ce que votre API dit — pas à ce qu'elle fait actuellement. Si une interface dit « vous pouvez appeler save() avec n'importe quel enregistrement valide », alors chaque implémentation doit accepter ces enregistrements valides. Si une interface dit « get() retourne une valeur ou un résultat clair ‘not found’ », alors les implémentations ne peuvent pas lancer des erreurs aléatoires ou retourner des données partielles.
Une extension sûre signifie que vous pouvez ajouter de nouvelles implémentations (ou changer de fournisseur) sans forcer les utilisateurs à réécrire leur code. C'est le bénéfice pratique du LSP : garder les interfaces remplaçables.
Deux façons communes de rompre la promesse :
Entrées plus restreintes (préconditions plus strictes) : une nouvelle implémentation rejette des entrées que la définition de l'interface autorisait. Exemple : l'interface accepte toute chaîne UTF‑8 comme ID, mais une implémentation n'accepte que des IDs numériques.
Sorties affaiblies (postconditions moins fortes) : une implémentation retourne moins que promis. Exemple : l'interface dit que les résultats sont triés, uniques ou complets — et une implémentation renvoie des données non triées, des doublons ou en supprime silencieusement.
Violation subtile : changer le comportement d'échec — si une implémentation renvoie « non trouvé » et qu'une autre lance une exception pour la même situation, les appelants ne peuvent pas substituer l'une à l'autre en toute sécurité.
Pour supporter des « plug-ins », rédigez l'interface comme un contrat :
Si une implémentation nécessite vraiment des règles plus strictes, ne les cachez pas derrière la même interface. Soit (1) définissez une interface séparée, soit (2) faites de la contrainte une capacité explicite (par ex. supportsNumericIds() ou une configuration documentée). Ainsi, les clients s'engagent en connaissance de cause plutôt que d'être surpris par une « substitution » qui n'est pas réellement substituable.
Une interface bien conçue paraît « évidente » à utiliser car elle expose seulement ce dont l'appelant a besoin — et pas plus. La vision de Liskov sur l'abstraction des données vous pousse vers des interfaces étroites, stables et lisibles, pour que les utilisateurs puissent s'y fier sans connaître les détails internes.
Les grosses APIs mélangent souvent des responsabilités sans lien : configuration, changements d'état, reporting et troubleshooting au même endroit. Cela rend difficile de comprendre ce qu'il est sûr d'appeler et quand.
Une interface cohésive regroupe des opérations appartenant à la même abstraction. Si votre API représente une file, concentrez-vous sur les comportements de file (enqueue/dequeue/peek/size), pas sur des utilitaires généraux. Moins de concepts signifie moins de voies d'utilisation incorrecte.
« Flexible » veut souvent dire « flou ». Des paramètres comme options: any, mode: string ou plusieurs booléens (par ex. force, skipCache, silent) créent des combinaisons mal définies.
Privilégiez :
publish() vs publishDraft()), ouSi un paramètre oblige les appelants à lire la source pour savoir ce qui arrive, ce n'est pas une bonne abstraction.
Les noms communiquent le contrat. Choisissez des verbes qui décrivent un comportement observable : reserve, release, validate, list, get. Évitez les métaphores cryptiques et les termes surchargés. Si deux méthodes semblent similaires, les appelants supposeront qu'elles se comportent de manière similaire — faites en sorte que ce soit vrai.
Séparez une API quand vous remarquez :
Des modules séparés vous permettent d'évoluer les internals tout en gardant la promesse centrale. Si vous prévoyez de la croissance, envisagez un « core » mince plus des extensions ; voir aussi /blog/evolving-apis-without-breaking-users.
Les API évoluent rarement peu. De nouvelles fonctionnalités arrivent, des cas limites sont découverts, et des « petites améliorations » peuvent silencieusement casser des applications réelles. Le but n'est pas de geler une interface, mais de l'évoluer sans violer les promesses dont les utilisateurs dépendent déjà.
Le versioning sémantique est un outil de communication :
Limite : il faut aussi du jugement. Si une « correction » change un comportement sur lequel des appelants comptaient, c'est un breaking change en pratique — même si l'ancien comportement était accidentel.
Beaucoup de breaking changes n'apparaissent pas au compilateur :
Pensez en termes de préconditions et postconditions : ce que les appelants doivent fournir et ce qu'ils peuvent compter recevoir.
La dépréciation fonctionne quand elle est explicite et datée :
L'abstraction de type Liskov aide parce qu'elle restreint ce que les utilisateurs peuvent exploiter. Si les appelants se contentent du contrat — pas de la structure interne — vous pouvez changer formats de stockage, algorithmes et optimisations librement.
Concrètement, de bons outils aident aussi. Par exemple, si vous itérez vite sur une API interne pour une app React ou un backend Go + PostgreSQL, un workflow d'accélération comme Koder.ai peut accélérer l'implémentation sans changer la discipline centrale : vous voulez toujours des contrats nets, des identifiants stables et une évolution rétrocompatible. La vitesse est un multiplicateur — autant multiplier les bonnes habitudes d'interface.
Une API fiable n'est pas celle qui ne tombe jamais en panne, mais celle qui échoue de manière compréhensible, gérable et testable. La gestion des erreurs fait partie de l'abstraction : elle définit ce que signifie « usage correct » et ce qui arrive quand le monde (réseau, disque, permissions, temps) n'est pas d'accord.
Commencez par séparer deux catégories :
Cette distinction garde l'interface honnête : les appelants savent ce qu'ils peuvent corriger dans le code vs ce qu'ils doivent gérer à l'exécution.
Votre contrat doit impliquer le mécanisme :
Ok | Error) quand les échecs sont attendus et que vous voulez que les appelants les gèrent explicitement.Peu importe le choix, soyez cohérent sur toute l'API pour que les utilisateurs n'aient pas à deviner.
Listez les échecs possibles par opération en termes de sens, pas de détails d'implémentation : « conflit dû à une version obsolète », « non trouvé », « permission refusée », « rate limited ». Fournissez des codes d'erreur stables et des champs structurés pour que les tests puissent affirmer le comportement sans matcher des chaînes.
Documentez si une opération est sûre à retenter, dans quelles conditions, et comment obtenir l'idempotence (clés d'idempotence, IDs de requête naturels). Si un succès partiel est possible (opérations en lot), définissez comment succès et échecs sont rapportés, et quel état les appelants doivent supposer après un timeout.
Une abstraction est une promesse : « Si vous appelez ces opérations avec des entrées valides, vous obtiendrez ces résultats, et ces règles tiendront toujours. » Les tests sont le moyen de tenir cette promesse au fil des changements.
Commencez par traduire le contrat en vérifications automatisables.
Les tests unitaires doivent vérifier les postconditions et les cas limites de chaque opération : valeurs retournées, changements d'état et comportement en erreur. Si votre interface dit « supprimer un élément non existant renvoie false et ne change rien », écrivez exactement ça.
Les tests d'intégration doivent valider le contrat au travers des frontières réelles : base de données, réseau, sérialisation et auth. Beaucoup de « violations de contrat » n'apparaissent qu'au moment de coder/decoder les types ou quand les retries/timeouts interviennent.
Les invariants sont des règles qui doivent rester vraies pour n'importe quelle séquence d'opérations valides (ex. « le solde ne devient jamais négatif », « les IDs sont uniques », « les éléments retournés par list() peuvent être récupérés par get(id) »).
Le property-based testing vérifie ces règles en générant beaucoup d'entrées valides et de séquences d'opérations aléatoires, cherchant des contre-exemples. Conceptuellement, vous dites : « Peu importe l'ordre des appels, l'invariant tient. » C'est excellent pour trouver des cas limites étranges que les humains n'ont pas envisagés.
Pour les APIs publiques ou partagées, laissez les consommateurs publier des exemples de requêtes qu'ils effectuent et des réponses sur lesquelles ils comptent. Les fournisseurs exécutent ensuite ces contrats en CI pour confirmer que les changements ne cassent pas des usages réels — même quand l'équipe fournisseur ne connaissait pas ces usages.
Les tests ne couvrent pas tout, donc surveillez des signaux qui suggèrent que le contrat change : modifications de la forme de réponse, augmentation des taux 4xx/5xx, nouveaux codes d'erreur, pics de latence et échecs de désérialisation « champ inconnu ». Surveillez par endpoint et par version pour détecter la dérive tôt et réagir.
Si vous supportez des snapshots ou des rollback dans votre pipeline de livraison, ils se combinent naturellement avec cet état d'esprit : détecter la dérive tôt, puis revenir en arrière sans forcer les clients à s'adapter en pleine urgence. (Koder.ai, par exemple, inclut snapshots et rollback dans son workflow, ce qui s'aligne bien avec l'approche « contrats d'abord, changements ensuite ».)
Même les équipes qui valorisent l'abstraction tombent dans des pratiques qui semblent « pratiques » sur le moment mais transforment progressivement une API en un ensemble de cas particuliers. Voici quelques pièges récurrents — et quoi faire à la place.
Les feature flags sont utiles pour le déploiement, mais le problème arrive quand les flags deviennent des paramètres publics et long-terme : ?useNewPricing=true, mode=legacy, v2=true. Avec le temps, les appelants les combinent de manières imprévues et vous vous retrouvez à supporter plusieurs comportements indéfiniment.
Approche plus sûre :
Les APIs qui exposent des IDs de table, des clés de jointure ou des filtres « en forme de SQL » (ex. where=...) obligent les clients à apprendre votre modèle de stockage. Un changement de schéma devient alors un changement d'API cassant.
Modélisez plutôt l'interface autour des concepts métier et des identifiants stables. Laissez les clients demander ce qu'ils veulent dire (« commandes pour un client sur une plage de dates »), pas comment vous le stockez.
Ajouter un champ semble inoffensif, mais des ajouts répétés peuvent brouiller les responsabilités et affaiblir les invariants. Les clients commencent à dépendre de détails accidentels et le type devient une collection fourre-tout.
Évitez le coût à long terme en :
Trop d'abstraction peut bloquer des besoins réels — pagination qui ne peut pas exprimer « commencer après ce curseur », ou endpoint de recherche qui ne peut pas faire une « correspondance exacte ». Les clients contournent alors l'API (appels multiples, filtrage local), causant pire performance et plus d'erreurs.
La solution : flexibilité contrôlée : fournissez un petit ensemble de points d'extension bien définis (par ex. opérateurs de filtre supportés), plutôt qu'une échappatoire ouverte.
La simplification n'implique pas de retirer du pouvoir. Dépréciez des options confuses, mais conservez la capacité sous une forme plus claire : remplacez des paramètres qui se recoupent par un objet de requête structuré, ou scindez un endpoint « tout faire » en deux endpoints cohésifs. Puis accompagnez la migration par une doc versionnée et un calendrier de dépréciation (voir /blog/evolving-apis-without-breaking-users).
Vous pouvez appliquer les idées d'abstraction de Liskov avec une checklist simple et répétable. Le but n'est pas la perfection — c'est rendre les promesses de l'API explicites, testables et sûres à faire évoluer.
Utilisez des blocs courts et cohérents :
transfer(from, to, amount)amount > 0 et les comptes existentInsufficientFunds, AccountNotFound, TimeoutPour aller plus loin, cherchez : Abstract Data Types (ADTs), Design by Contract, et le Principe de Substitution de Liskov (LSP).
Si votre équipe garde des notes internes, placez-les depuis une page comme /docs/api-guidelines pour que le workflow de revue reste facile à réutiliser — et si vous construisez de nouveaux services rapidement (manuellement ou avec un assistant de build comme Koder.ai), traitez ces lignes directrices comme non négociables. Les interfaces fiables sont la façon dont la rapidité se capitalise au lieu de se retourner contre vous.
Elle a popularisé les notions de data abstraction et information hiding, qui se traduisent directement en conception d'API moderne : publier un contrat petit et stable et garder l'implémentation flexible. Le bénéfice est concret : moins de changements cassants, des refactorings plus sûrs et des intégrations plus prévisibles.
Une API fiable est celle sur laquelle les appelants peuvent compter dans le temps :
La fiabilité, ce n'est pas « ne jamais échouer », mais échouer de façon prévisible et respecter le contrat.
Rédigez le comportement comme un contrat :
Incluez les cas limites (résultats vides, doublons, ordre) pour que les appelants puissent implémenter et tester contre la promesse.
Un invariant est une règle qui doit toujours être vraie à l'intérieur d'une abstraction (par ex. « la quantité n'est jamais négative »). Faites respecter les invariants aux frontières :
Cela réduit les bugs en aval parce que le reste du système n'a plus à gérer des états impossibles.
L'information hiding signifie exposer des opérations et du sens, pas la représentation interne. Évitez d'assujettir les consommateurs à des détails que vous pourriez vouloir changer plus tard (tables, caches, shard keys, statuts internes).
Tactiques pratiques :
usr_...) plutôt que les identifiants de table.Parce qu'ils figent votre implémentation. Si des clients s'appuient sur des filtres en forme de table, des clés de jointure ou des IDs internes, un refactor de schéma devient un changement d'API cassant.
Préférez les questions de domaine plutôt que les questions de stockage, par ex. « commandes pour un client sur une plage de dates », et gardez le modèle de stockage privé derrière le contrat.
LSP signifie : si du code fonctionne avec une interface, il doit continuer à fonctionner avec n'importe quelle implémentation valide de cette interface sans cas spéciaux. En termes d'API, c'est la règle « ne surprenez pas l'appelant ».
Pour garantir des implémentations substituables, standardisez :
Surveillez :
Si une implémentation a vraiment besoin de contraintes supplémentaires, publiez une interface séparée ou une capacité explicite pour que les clients s'engagent en connaissance de cause.
Gardez les interfaces petites et cohésives :
options: any et les piles de booléens qui créent des combinaisons ambiguës.Concevez les erreurs comme partie du contrat :
La cohérence importe plus que le mécanisme exact (exceptions vs types résultat) tant que les appelants peuvent prédire et gérer les résultats.
status=3).reserve, release, list, validate).Si différents rôles ou rythmes de changement existent, séparez les modules/ressources (voir /blog/evolving-apis-without-breaking-users).