Le Contrat de la Bibliothèque
Une application est jugée par ses utilisateurs ; une bibliothèque est jugée par les programmeurs, à un moment précis — lorsqu'ils veulent quelque chose que l'auteur n'avait pas imaginé. Une bonne bibliothèque n'est pas celle qui est pratique lorsqu'elle est utilisée comme prévu. C'est celle qui n'a pas besoin d'être jetée lorsqu'elle est utilisée autrement.
Chaque règle ci-dessous découle d'une asymétrie. À l'intérieur de la bibliothèque, vous pouvez tout changer, pour toujours. À l'extérieur, tout ce qu'un appelant peut observer est figé la première fois que quelqu'un en dépend — et avec suffisamment d'appelants, quelqu'un dépend de tout.
Ce qui se fige, et quand
La loi de Hyrum est la condition de fonctionnement, pas une curiosité : avec suffisamment d'utilisateurs, tout comportement observable devient porteur de charge — formulation d'exception, ordre d'énumération, sortie de log, timing, même un comportement que vous avez documenté comme indéfini. « Ce n'était jamais dans le contrat » ne répare pas l'appelant qui a livré contre lui.
Ainsi, la ligne de gel se situe au niveau de l'observabilité, pas du mot-clé public,
et trois conséquences en découlent :
- La surface commence petite. Un symbole, une fois public, est une promesse que vous pouvez élargir mais jamais retirer discrètement. « Public pour l'instant » est une dette contractée sans bénéfice ; exposez à la demande.
- Les formats survivent au code. Un format de fichier, un protocole réseau, un schéma persistant survit à chaque réécriture de la bibliothèque qui l'entoure. Sérialiser la structure interne telle quelle soude la couche figée à la couche modifiable.
- Un module interne franchit cette ligne à son second consommateur, pas à l'empaquetage. Dès que deux appelants existent, son API a la même économie de gel qu'un paquet publié — seule la zone d'impact est plus petite.
Le Versionnement Sémantique est l'orthographe publique de la même ligne, et commence par refuser de fonctionner sans une : « Un logiciel utilisant le Versionnement Sémantique DOIT déclarer une API publique » (SemVer 2.0.0, clause 1). Une fois déclarée, les incréments sont décidés plutôt que ressentis — une modification rétrocompatible de cette API DOIT augmenter MAJEUR (clause 8), une nouvelle fonctionnalité rétrocompatible DOIT augmenter MINEUR (clause 7), une correction rétrocompatible DOIT augmenter CORRECTIF (clause 6). La loi de Hyrum est ce qui rend la déclaration difficile : la spécification fige ce que vous avez déclaré, et les appelants figent ce qu'ils ont observé.
La spécification a également un nom pour le module qui n'a pas encore franchi la ligne. « La version majeure zéro (0.y.z) est destinée au développement initial. TOUT PEUT changer à tout moment » (clause 4). C'est l'étiquette honnête pour un module n'ayant qu'un consommateur, et le jour où il en gagne un second est le jour où la version 1.0.0 est due, pas le jour où il est empaqueté.
Ne prenez aucun contrôle
Séparez le mécanisme de la politique. Effectuer une requête HTTP est un mécanisme ; la relancer trois fois avec un backoff exponentiel est une politique, et une politique intégrée au cœur rétrécit la bibliothèque aux appelants qui la partagent. Fournissez des valeurs par défaut pratiques pour la journalisation, le threading, les relances, les délais d'attente, la mise en cache, la sérialisation — et rendez chacune d'elles remplaçable.
Le rôle de l'application est unique, et ce n'est pas le vôtre. Quitter le processus, écrire sur stdout, intercepter SIGINT, installer des gestionnaires d'exceptions de haut niveau, redimensionner le pool de threads : exactement un composant par processus peut faire ces choses, et une bibliothèque qui le fait combat le composant qui en a le droit. Lire les entrées du processus appartient au même rôle : les variables d'environnement, les fichiers de configuration et les arguments de ligne de commande sont lus à la racine de l'application, qui remet chaque valeur à la bibliothèque sous forme de paramètre ou d'objet d'options. L'appelant choisit alors d'où vient une valeur, et un test la transmet au lieu de configurer la machine.
La magie — auto-détection, auto-conversion, auto-reconnexion, auto-cache — n'est admissible que tant que l'appelant peut reconstruire pourquoi elle a fait ce qu'elle a fait. La commodité que l'utilisateur ne peut pas déboguer est un contrôle pris, pas un travail économisé.
N'ajoutez aucun coût caché
Le moment où les choses se produisent appartient à l'appelant. Un import qui démarre un thread, un constructeur qui ouvre une connexion, un initialiseur statique qui lit des variables d'environnement — chacun déplace un travail réel vers un moment que l'appelant n'a jamais choisi et ne peut pas déplacer.
Une dépendance fait partie de votre API. Son exécution, sa cadence de mise à jour, sa stabilité ABI, sa licence, ses propres effets secondaires d'initialisation sont tous transférés à chaque appelant ; vous choisissez en leur nom.
Les caractéristiques de performance sont un contrat. Transformer une recherche O(1) en O(n) — ou doubler les allocations et les copies — est un changement cassant portant une signature compatible, et les appelants le découvrent en production.
Là où les entrées arrivent plus vite qu'elles ne sortent — flux, événements, logs, files d'attente — un tampon interne illimité est une fuite mémoire différée. La contre-pression fait partie de la conception, pas un détail opérationnel.
La barre pour l'abstraction elle-même est le coût zéro, dans les deux clauses de Stroustrup. Ce que l'appelant n'utilise pas, il ne paie pas : l'étage de commodité ne doit pas taxer l'étage primitif avec ses allocations, son dispatch ou sa configuration. Et ce qu'il utilise, il ne pourrait pas raisonnablement l'écrire mieux à la main : une abstraction battue par du code fait main est contournée, et une bibliothèque contournée en est une jetée portant un numéro de version.
Ne conservez aucun état caché
Singletons, caches statiques, boucles d'événements globales, configuration à l'échelle du processus : chacun fonctionne tant que la vôtre est la seule bibliothèque dans le processus, et entre en collision dès qu'une seconde bibliothèque, un hôte de plugin, ou une suite de tests parallèle la partage. L'état vit dans une instance que l'appelant crée, possède, et peut créer deux fois.
Ne cachez aucun échec
La forme de l'API déclare ce qui peut arriver et qui est responsable. Erreurs, annulation, délai d'attente, succès partiel, absence — modélisés dans les types, pas acheminés via « cela n'arrive normalement jamais » et une exception. La même forme porte les réponses de propriété qu'un commentaire ne peut pas imposer : qui ferme le flux, si le tampon peut être réutilisé, sur quel thread le rappel arrive.
Conservez la cause.
Effondrer un échec en false ou null supprime exactement l'information dont la couche au-dessus avait besoin pour décider ;
le journaliser vous-même et continuer perd la même information et s'approprie la politique de journalisation de l'appelant.
La qualité se distingue sur le chemin anormal. Sur le chemin heureux, les bibliothèques sont interchangeables ; les délais d'attente, la reconnexion, la déconnexion en cours de flux, les lectures partielles, les données corrompues et les relances sont là où les différences vivent, donc concevez d'abord pour leur composition.
L'annulation se rétrofit mal — un jeton d'annulation traverse chaque signature qu'il touche. Les opérations d'E/S et de longue durée l'acceptent dès la première version.
Ne bloquez aucune échappatoire
Construisez deux étages.
Une API de haut niveau qui sert le cas courant en une ligne,
reposant sur des primitives publiques — ReadAsync() sur ReadAsync(Memory<byte>) — afin que l'appelant dont l'exigence tombe en dehors de l'étage supérieur descende au lieu de partir.
Une bibliothèque avec seulement l'étage supérieur est jetée à la première exigence qu'elle n'avait pas anticipée.
L'observabilité est injectée, pas possédée : les journaliseurs, les métriques, les traces se branchent sur la pile de l'appelant. Un format de diagnostic que seule votre bibliothèque lit est une télémétrie prise en otage.
Un impôt, deux devises
Cette compétence et l'Architecture Propre sont le même refus en deux devises. L'abstraction à coût zéro refuse l'impôt sur les performances d'exécution ; la Règle de Dépendance refuse l'impôt sur la capacité de changement. Dans les deux cas, une abstraction est jugée par ce qu'elle fait sacrifier au côté dépendant.
| Devoir ici | Son nom architectural |
|---|---|
| Mécanisme séparé de la politique | Cas d'utilisation tenu à l'écart du framework et du pilote |
| Valeurs par défaut remplaçables | Inversion de dépendance — le côté stable choisit l'implémentation |
| Primitives sous l'étage de commodité | Détails extérieurs interchangeables |
| Pas d'état global | Dépendances explicites, donc une frontière peut être tracée |
| Une dépendance fait partie de votre API | La Règle de Dépendance |
La forme saine pour l'appelant place votre bibliothèque en bas : un détail remplaçable sous leur politique et leur domaine. La forme inversée — l'application vivant à l'intérieur d'un framework, traînant son cycle de vie, ses types, son injection de dépendances, son modèle d'erreur, son modèle asynchrone — est la bibliothèque agissant comme l'application, écrite à l'échelle architecturale.
La forme générale : une bonne architecture minimise non pas le nombre de dépendances mais la liberté que chaque dépendance retire.
La phrase
Une abstraction dans une bibliothèque existe pour élargir les choix de l'appelant. Au moment où elle prend performance, contrôle ou information en échange, elle a cessé d'abstraire et a commencé à décider.
Quand ne pas appliquer
Un module avec un consommateur, un dépôt, une équipe n'a pas franchi la ligne de gel — il est en 0.y.z selon les termes de SemVer, qu'il porte ou non un numéro — et construire chaque jointure de politique remplaçable pour lui est une spéculation avec une facture de maintenance. Gardez les disciplines qui ne coûtent rien dès le premier jour — pas d'état global, échecs qui conservent leur cause, pas de travail avant que l'appelant ne le demande — et ajoutez les jointures lorsque le second consommateur arrive, ce qui est le moment où elles cessent d'être spéculatives.
Sources
- Versionnement Sémantique 2.0.0, semver.org : clause 1 (l'API publique DOIT être déclarée), clauses 6–8 (incréments CORRECTIF, MINEUR, MAJEUR), clause 4 (0.y.z), et l'entrée FAQ sur la dépréciation (la documenter, puis publier une version mineure avec la dépréciation en place). Vérification du libellé par rapport à la page.
- La loi de Hyrum est une observation, pas une norme, et est citée comme telle.