Be Software Architecture Wise
Be Wise Apps

Read in English · Leer en español · Auf Deutsch lesen

Section 3.3 sur 3.8 · Intermédiaire · 24 min de lecture

Masquage de l’information, abstraction et interfaces

Leçon vidéo

5 min · narré en anglais, avec sous-titres
Écouter seulement (audio)
Lire la transcription

Tout bon module garde un secret. Dans cette leçon, vous allez découvrir ce que signifie vraiment le masquage de l’information, comment les interfaces fonctionnent comme des contrats, pourquoi les abstractions fuient, et comment garder stables les interfaces entre composants.

Quand vous envoyez un colis, vous le remettez au guichet et recevez un numéro de suivi. Vous ne savez pas, et cela vous est égal, s’il voyage en camionnette, en train ou en avion, ni par quel dépôt il passe. La poste peut changer tout cela dès demain, car l’accord passé au guichet reste le même. Les bons modules fonctionnent comme ce guichet. Les ennuis commencent quand les clients comptent sur des détails que personne n’a promis, comme le fait que le colis arrive toujours avant dix heures.

Le masquage de l’information est le principe proposé par David Parnas en 1972. Chaque module cache une décision de conception difficile ou susceptible de changer, derrière une interface qui en révèle le moins possible. Les bons candidats au masquage sont les structures de données, l’ordre des opérations, les formats d’entrée et de sortie, les algorithmes et, aujourd’hui, le fournisseur ou la base de données que vous utilisez. Ce qui est caché peut changer à peu de frais. Ce qui est exposé devient coûteux à changer, parce que d’autres en dépendent.

Ne confondez pas cela avec l’encapsulation. L’encapsulation est un mécanisme du langage, comme les champs privés. Le masquage de l’information, c’est la décision de conception sur ce qu’il faut garder secret. Vous pouvez encapsuler parfaitement et ne rien cacher pour autant. Une classe avec des champs privés, et un getter et un setter publics pour chacun, dit à chaque appelant quels champs elle possède. Changez-les, et tous les appelants cassent. Une interface est un contrat, et la conception par contrat de Bertrand Meyer le rend précis. Les préconditions sont ce que l’appelant doit rendre vrai. Les postconditions sont ce que l’opération garantit ensuite. Les invariants restent vrais entre deux opérations, quelles qu’elles soient. Le contrat désigne aussi le responsable. Si une précondition est violée, le bug est chez l’appelant. Si une postcondition est violée, il est chez le fournisseur.

John Ousterhout propose une image utile. Représentez-vous un module comme un rectangle. Le bord supérieur est l’interface que tout le monde doit apprendre. La surface est la fonctionnalité qu’il fournit. Un module profond a une petite interface au-dessus de beaucoup de fonctionnalités. La gestion des fichiers sous Unix est son exemple préféré : open, read, write, seek et close cachent la mise en cache, les périphériques, les permissions et l’organisation du disque. Un module superficiel a une interface presque aussi complexe que ce qu’il fait, comme une classe d’enveloppe dont chaque méthode transmet un appel tel quel. Il ajoute un coût sans cacher grand-chose. C’est aussi un test précieux pour les services. Un service dont l’API se contente de refléter ses tables de base de données est superficiel, et reporte toute la vraie logique sur ses clients.

En 2002, Joel Spolsky a écrit que toutes les abstractions non triviales fuient dans une certaine mesure : leurs détails cachés vous affectent encore. Une bibliothèque de mapping de base de données cache SQL, jusqu’à ce qu’une boucle lance discrètement une requête par ligne. Soyez donc honnête sur les fuites. Un appel distant peut dépasser son délai d’expiration, ou réussir alors que la réponse se perd. Une bonne interface distante prend un délai d’expiration, et distingue une requête refusée d’un résultat inconnu. La loi de Hyrum ajoute un avertissement. Avec suffisamment d’utilisateurs d’une API, chaque comportement observable finira par être utilisé par quelqu’un : l’ordre des résultats, la formulation d’une erreur, et même la durée d’un appel. C’est pourquoi les API stables gardent une surface minimale, évoluent par ajout plutôt que par modification, versionnent explicitement les changements cassants et utilisent des tests de contrat pour détecter les accidents.

Passons à un exemple commenté. Une entreprise de livraison de repas appelle directement le code du prestataire de paiement A à onze endroits. Le service financier veut ajouter le prestataire B, et pouvoir changer en moins d’une journée si l’un d’eux subit une panne. La conception introduit une interface de passerelle de paiement qui appartient à la boutique, dans ses propres termes : autoriser, capturer et rembourser. Le paiement et les remboursements ne dépendent que d’elle. Chaque prestataire reçoit un adaptateur qui implémente l’interface, et traduit vers l’API et les codes d’erreur de ce prestataire. Seuls les adaptateurs savent que les prestataires existent. La configuration choisit l’adaptateur de chaque pays au démarrage. La décision cachée, c’est quel prestataire, et comment l’appeler.

L’interface cache quel prestataire est utilisé, sa version, ses codes d’erreur, ses règles de nouvelle tentative et ses identifiants. Ses préconditions sont un montant positif en unités mineures, une devise prise en charge et une clé d’idempotence unique pour chaque tentative. Elle promet l’un de trois résultats. Approuvé signifie que les fonds sont réservés. Refusé signifie qu’aucun argent n’a bougé. Inconnu signifie que le résultat n’a pas pu être déterminé, et l’appel peut donc être relancé avec la même clé. La fuite du réseau fait partie du contrat, ce n’est pas une surprise. Le bénéfice : ajouter un prestataire voulait dire onze endroits d’appel et la table des commandes. Désormais, c’est un adaptateur, ses tests et une entrée de configuration.

La section complète montre la conception par contrat dans du code, et huit pratiques pour des API stables. Le masquage de l’information vous dit ce que chaque module garde pour lui, mais pas dans quel sens doivent pointer les dépendances. C’est l’objet de la section trois point quatre. À très vite.

Ce qu’il faut savoir

Le principe : cacher ce qui est susceptible de changer

Dans la section 3.1, l’exemple KWIC de David Parnas a montré que décomposer un programme par étapes de traitement dispersait chaque décision de conception entre plusieurs modules. Son alternative, le masquage de l’information (information hiding), confie à chaque module une décision à garder secrète : comment les lignes sont stockées, comment les décalages sont calculés, à quel moment le tri a lieu. L’interface du module est choisie pour révéler le moins possible de cette décision. Tout le reste du système ne dépend que de l’interface, si bien que la décision peut changer sans que les autres parties s’en aperçoivent.

La liste de Parnas des choses qui méritent d’être cachées reste une bonne liste de contrôle un demi-siècle plus tard : les structures de données et leur mode de stockage ; l’ordre dans lequel les opérations sont effectuées ; les détails du matériel, des périphériques et des systèmes d’exploitation ; les formats d’entrée et de sortie ; et les algorithmes qui pourraient être remplacés par de meilleurs. Les ajouts modernes suivent la même logique : quel fournisseur tiers est utilisé, quelle base de données, quel courtier de messages, quelle version d’une API externe, et quelles règles métier sont en cours de révision.

Le principe s’applique de la même façon à toutes les échelles. Une classe cache ses champs ; un paquet cache ses modules internes ; un service cache sa base de données, son langage et son déploiement ; l’API publique d’une organisation cache les services qui existent derrière elle. Dans chaque cas, ce qui est caché peut changer à peu de frais, et ce qui est exposé devient coûteux à changer, parce que d’autres en dépendent.

L’encapsulation n’est pas le masquage de l’information

Les deux termes sont souvent employés comme synonymes, mais ils ne désignent pas la même chose. L’encapsulation est un mécanisme : regrouper les données avec les opérations qui les manipulent et restreindre l’accès direct, au moyen de champs privés, de frontières de modules ou de modificateurs d’accès (section 0.3). Le masquage de l’information est un principe de conception : décider quelles décisions garder secrètes, et concevoir l’interface de façon qu’elle ne les révèle pas. L’encapsulation est l’un des outils qui servent à cacher l’information ; elle ne décide pas à votre place de ce qu’il faut cacher.

Il est facile d’encapsuler parfaitement tout en ne cachant rien. Une classe dont les champs sont privés, avec un getter et un setter publics pour chacun, est entièrement encapsulée, et pourtant chaque appelant sait exactement quels champs existent et de quels types ils sont. Changez la représentation interne, et tous les appelants cassent. L’inverse se produit aussi : Python n’impose aucune confidentialité, et pourtant un module Python bien conçu peut cacher très efficacement ses décisions grâce à un ensemble de fonctions soigneusement choisi.

# Encapsulated, but the representation leaks through the interface
class Price:
    def __init__(self, pounds: float):
        self._pounds = pounds
    def get_pounds(self) -> float:          # callers now depend on 'float pounds'
        return self._pounds
    def set_pounds(self, value: float):
        self._pounds = value

# Information hiding: callers ask for what they need, not for the field
class Money:
    def __init__(self, minor_units: int, currency: str):
        self._minor, self._currency = minor_units, currency
    def add(self, other: "Money") -> "Money": ...
    def allocate(self, ratios: list[int]) -> list["Money"]: ...
    def format(self, locale: str) -> str: ...
# Switching from pence to a decimal type later changes only this class.
Les deux classes sont encapsulées ; seule la seconde cache la décision sur la façon dont un prix est stocké.

L’abstraction et les interfaces comme contrats

Une abstraction est une vue simplifiée de quelque chose, qui garde ce qui compte pour un objectif donné et laisse de côté le reste. Un fichier est une abstraction qui recouvre des blocs de disque, des caches et des pilotes ; un paiement est une abstraction qui recouvre un réseau de cartes, l’API d’un prestataire et le processus de règlement d’une banque. L’abstraction et le masquage de l’information sont les deux faces d’un même geste : choisir une abstraction décide de ce que montre l’interface ; cacher le reste permet de se fier à l’abstraction en toute sécurité.

Une interface est donc un contrat entre un module et ses utilisateurs. Sa partie formelle est ce qu’un vérificateur de types ou un schéma d’API peut voir : noms des opérations, types des paramètres et des valeurs de retour, types d’erreurs, noms des champs d’un corps JSON. Sa partie informelle est tout aussi contraignante : ce que fait l’opération, ses effets de bord, si elle peut être relancée sans risque, combien de temps elle prend, quelles erreurs sont possibles et ce qu’elles signifient. John Ousterhout souligne que la partie informelle ne peut être consignée que dans la documentation, et que si les utilisateurs doivent lire le code d’un module pour apprendre à s’en servir, le module ne fournit aucune véritable abstraction.

Conception par contrat

La conception par contrat, développée par Bertrand Meyer dans les années 1980 avec le langage Eiffel et décrite dans son livre Object-Oriented Software Construction et dans son article de 1992 Applying ‘Design by Contract’, rend le contrat précis. Chaque opération a une précondition, que l’appelant doit rendre vraie avant l’appel ; une postcondition, que l’opération garantit à son retour si la précondition était satisfaite ; et chaque classe ou module a des invariants, des conditions qui restent vraies entre deux opérations quelles qu’elles soient. Le contrat répartit les responsabilités : si une précondition est violée, le bug est chez l’appelant ; si une postcondition est violée, il est chez le fournisseur.

Meyer s’est opposé à l’habitude inverse, la programmation défensive, dans laquelle chaque routine vérifie tout au cas où quelqu’un l’appellerait mal. Les vérifications redondantes ajoutent du code, de la complexité et de nouveaux chemins de défaillance. Chaque condition devrait plutôt relever clairement de la responsabilité d’un seul côté. Une routine dont la précondition est exigeante est plus simple ; une routine dont la précondition est tolérante est plus facile à appeler ; ce choix est une décision de conception, prise explicitement. Aux frontières d’un système, là où les entrées viennent d’utilisateurs non fiables ou d’autres organisations, la validation n’est pas facultative : le contrat y prévoit que le module frontière vérifie lui-même tout (chapitre 8).

class Account:
    """Invariant: balance_pence >= -overdraft_limit_pence."""

    def withdraw(self, amount_pence: int) -> None:
        # Preconditions (the caller's obligations)
        if amount_pence <= 0:
            raise ValueError("amount must be positive")
        if self.balance_pence - amount_pence < -self.overdraft_limit_pence:
            raise InsufficientFunds(self.id)
        old = self.balance_pence
        self.balance_pence -= amount_pence
        # Postcondition (the supplier's guarantee)
        assert self.balance_pence == old - amount_pence
        assert self.balance_pence >= -self.overdraft_limit_pence  # invariant
Un contrat mis par écrit : préconditions vérifiées à l’entrée, postcondition et invariant énoncés et vérifiés par des assertions.

Les trois mêmes idées s’appliquent aux services. Une spécification d’API, comme un document OpenAPI, énonce les préconditions (champs obligatoires, formats, authentification), les postconditions (la réponse et le changement d’état, par exemple une commande existe avec le statut PLACED) et les invariants sur lesquels un client peut compter (le total d’une commande ne change jamais après le paiement). Les tests de contrat pilotés par le consommateur, dans lesquels chaque client publie les attentes sur lesquelles il compte et le fournisseur les exécute dans son build, vérifient ces contrats automatiquement.

Les abstractions qui fuient

En novembre 2002, Joel Spolsky a publié The Law of Leaky Abstractions, qui énonce que toutes les abstractions non triviales fuient, dans une certaine mesure. Une abstraction qui fuit (leaky abstraction) est une abstraction dont les détails cachés affectent encore ses utilisateurs. TCP présente un flux fiable par-dessus un réseau non fiable, jusqu’à ce qu’un câble soit coupé et qu’il n’y parvienne plus. SQL vous permet d’énoncer ce que vous voulez, et non comment l’obtenir, jusqu’à ce que deux requêtes logiquement équivalentes diffèrent en vitesse d’un facteur mille et que vous deviez lire le plan d’exécution. Un mapping objet-relationnel (ORM) cache SQL jusqu’à ce qu’une boucle lance discrètement une requête par ligne.

La conséquence pour les architectes n’est pas que les abstractions sont mauvaises, mais qu’une interface doit être honnête sur les détails qui vont fuir. L’exemple classique est l’appel distant qui ressemble exactement à un appel de fonction local. Les appels réseau sont plus lents de plusieurs ordres de grandeur, peuvent dépasser leur délai d’expiration, peuvent réussir sur le serveur alors que la réponse se perd, et peuvent échouer indépendamment de l’appelant ; la section 6.1 les présente comme les illusions de l’informatique distribuée. Une bonne interface distante l’expose : elle prend un délai d’expiration en paramètre, renvoie des erreurs qui distinguent refusé de résultat inconnu, et accepte une clé d’idempotence pour que les nouvelles tentatives soient sans risque. Cacher complètement le réseau, c’est une fuite qui n’attend que son heure.

Modules profonds et modules superficiels

A Philosophy of Software Design de John Ousterhout (publié pour la première fois en 2018 ; deuxième édition en 2021) propose une manière utile de juger une interface. Représentez-vous chaque module comme un rectangle : le bord supérieur est son interface, que chaque utilisateur doit apprendre, et la surface est la fonctionnalité qu’il fournit. Un module profond a une petite interface au-dessus de beaucoup de fonctionnalités : il rembourse donc bien des fois le coût de son apprentissage. Un module superficiel a une interface presque aussi complexe que ce qu’il fait : il ajoute donc un coût sans cacher grand-chose.

Son exemple préféré de profondeur est l’entrée-sortie de fichiers sous Unix : une poignée d’appels (open, read, write, seek, close) cachent les systèmes de fichiers, l’organisation du disque, la mise en cache, les permissions, les périphériques et les accès concurrents. Son exemple de superficialité est une classe dont chaque méthode transmet un appel à une autre classe, ou une classe de liste chaînée dont l’interface expose presque tout ce qu’elle fait. Ousterhout soutient que le conseil courant de garder les classes et les méthodes très petites peut produire ce qu’il appelle la classite : de nombreuses classes superficielles dont les interfaces, mises bout à bout, représentent plus de complexité qu’un seul module plus profond.

Modules profonds et modules superficiels

Module profond : interface étroite sur une grande implémentation ; module superficiel : interface large sur peu d’implémentationModule profondpetite interfacegrande implémentation cachéemise en tampon, cache, relances,pilotes, permissions,disque, concurrenceex. open, read, write, seek, closegrand bénéfice pour le coût d’apprentissageMod. superficielgrande interfacepeu de traitementex. une classe d’enveloppedont chaque méthode passeun appel tel quelcoûte presque autant à apprendre qu’il rapporte
L’image d’Ousterhout : la largeur du bord supérieur est l’interface que les utilisateurs doivent apprendre ; la surface en dessous est la fonctionnalité qu’il cache.

Tout le monde ne suit pas Ousterhout aussi loin. Les partisans des petites classes et des petites fonctions, comme Robert C. Martin dans Clean Code, soutiennent que les unités minuscules sont plus faciles à nommer, à tester et à réutiliser ; Ousterhout et Martin ont débattu de la question dans un échange écrit publié en 2025. Les deux camps s’accordent sur l’objectif de fond, qui est de réduire la complexité qu’un lecteur doit garder en tête. À l’échelle de l’architecture, le test de profondeur est particulièrement précieux : un service dont l’API reflète ses tables de base de données une à une, avec création, lecture, mise à jour et suppression pour chacune, est superficiel, et reporte toute la vraie logique sur ses clients.

La loi de Hyrum : tout ce qui est observable devient le contrat

La loi de Hyrum, nommée d’après Hyrum Wright, alors ingénieur logiciel chez Google, par son collègue Titus Winters, et popularisée par Software Engineering at Google (2020), constate qu’avec un nombre suffisant d’utilisateurs d’une API, peu importe ce que vous promettez dans le contrat : tous les comportements observables de votre système finiront par être utilisés par quelqu’un. L’ordre dans lequel une fonction renvoie les éléments d’un ensemble non ordonné, la formulation exacte d’un message d’erreur, un champ toujours présent bien que documenté comme facultatif, le fait qu’un appel prenne 50 millisecondes : si les utilisateurs peuvent le voir, certains d’entre eux s’y fieront.

Cette loi explique pourquoi le masquage de l’information doit être conçu dès le départ, et non espéré. Chaque comportement exposé par accident fait partie du contrat de fait, et le modifier cassera quelque chose chez quelqu’un, aussi clairement que la documentation ait indiqué qu’il n’était pas spécifié. Certaines équipes rendent délibérément aléatoires les comportements non spécifiés, comme l’ordre d’itération, pour que personne ne puisse en venir à en dépendre ; Go rend aléatoire l’ordre d’itération des maps pour cette raison. D’autres gardent des réponses légères, pour qu’il y ait moins de choses dont dépendre.

Concevoir des API stables entre composants

Entre composants et services, les interfaces sont ce qu’il y a de plus coûteux à modifier, car les changer exige de se coordonner avec chaque consommateur. Les pratiques ci-dessous découlent de tout ce qui précède ; la section 0.6 a présenté les API et la section 7.7 traite en profondeur de la compatibilité des schémas.

Pratiques pour des API de composants et de services stables

PratiqueCe que cela signifiePourquoi cela fonctionne
Surface minimaleN’exposez que les opérations et les champs dont un consommateur connu a besoin ; ajoutez-en sur demande.Moins de choses dont dépendre, donc moins de choses à casser (loi de Hyrum).
Modéliser le domaine, pas le stockageProposez des opérations comme place_order et cancel_order plutôt qu’une création, une mise à jour et une suppression calquées sur les tables.Le stockage peut changer sans que le contrat change ; le module reste profond.
Changement par ajoutAjoutez des champs facultatifs et de nouvelles opérations ; ne renommez, ne supprimez et ne changez jamais le sens des éléments existants au sein d’une même version.Les anciens clients continuent de fonctionner ; les nouveaux utilisent les nouvelles parties.
Lecteur tolérant (tolerant reader)Les clients ignorent les champs qu’ils ne comprennent pas et ne dépendent pas de l’ordre des champs.Les fournisseurs peuvent ajouter des champs sans rien casser chez personne.
Versionnement expliciteIntroduisez une nouvelle version majeure (dans l’URL, un en-tête ou le schéma d’événement) pour les changements cassants, et faites tourner l’ancienne et la nouvelle côte à côte pendant une période annoncée.Les consommateurs migrent à leur propre rythme ; le couplage de déploiement est évité.
Expansion-contraction (expand and contract)Ajoutez la nouvelle forme, faites migrer les consommateurs, confirmez par la supervision que plus personne n’utilise l’ancienne forme, puis supprimez-la, dans des mises en production distinctes.Les changements cassants se font sans mise en production conjointe.
Tests de contratLes consommateurs publient leurs attentes ; le build du fournisseur les exécute.Les changements cassants accidentels sont détectés avant le déploiement.
Sémantique de défaillance honnêteDocumentez les délais d’expiration, la possibilité de nouvelle tentative, l’idempotence et le sens des erreurs comme parties du contrat.Évite les pires fuites de l’abstraction réseau.

Exemple commenté : une interface de paiement qui cache le prestataire

Une entreprise de livraison de repas encaisse ses paiements par l’intermédiaire du prestataire A. Le service financier veut ajouter le prestataire B pour réduire les frais dans un pays, et pouvoir changer de prestataire en moins d’une journée si l’un d’eux subit une panne. Aujourd’hui, le code de l’étape de paiement appelle directement le SDK du prestataire A à onze endroits, utilise ses codes d’erreur pour décider quoi afficher au client et stocke ses objets de transaction dans la table des commandes. Chacun de ces points est une décision qui a fui.

La conception introduit une interface PaymentGateway qui appartient à la boutique, et non à un prestataire. Elle s’exprime dans les termes propres à la boutique : des montants en entiers d’unités mineures avec une devise, l’identifiant de commande de la boutique, une clé d’idempotence et trois résultats possibles. Chaque prestataire reçoit un adaptateur qui implémente l’interface et traduit vers et depuis l’API, les codes d’erreur et les particularités de ce prestataire. La configuration choisit l’adaptateur selon le pays. C’est le même geste que l’intermédiaire de la section 2.6, vu cette fois comme du masquage de l’information : la décision cachée est quel prestataire, et comment l’appeler.

from typing import Protocol, Literal
from dataclasses import dataclass

@dataclass(frozen=True)
class PaymentResult:
    outcome: Literal["approved", "declined", "unknown"]  # unknown: safe to retry
    reference: str | None        # our reference, not the provider's object

class PaymentGateway(Protocol):
    def authorise(self, order_id: str, amount_minor: int, currency: str,
                  idempotency_key: str) -> PaymentResult: ...
    def capture(self, reference: str) -> PaymentResult: ...
    def refund(self, reference: str, amount_minor: int) -> PaymentResult: ...

class ProviderBAdapter:            # implements PaymentGateway
    def authorise(self, order_id, amount_minor, currency, idempotency_key):
        resp = self._client.create_charge(value=amount_minor / 100,   # B wants decimals
                                          currency=currency, key=idempotency_key)
        return PaymentResult(_map_status(resp.code), _store_ref(resp.id))
L’interface appartient à la boutique et parle sa langue ; seuls les adaptateurs savent qu’un prestataire existe.

L’interface de paiement et ses adaptateurs

Paiement dépend d’une interface PaymentGateway propre à la boutique ; les adaptateurs de deux prestataires l’implémentent et sont seuls à connaître leurs APIValidationRembours.de la boutiquePaymentGatewayauthorise, capture,refundConfigurationpar pays : prestataire BAdaptateurProviderAAdaptateurProviderBAPI de AAPI de Bchoix adaptateurau démarrageLes appelants ne voient que l’interface ; prestataires, codes d’erreur, relances et règles de devise restent aux adaptateursFlèches pleines : dépend de ou appelle. Pointillés : implémente ou sélectionne.
Les appelants dépendent d’une interface qui appartient à la boutique ; les adaptateurs l’implémentent, et la configuration choisit celui qui s’exécute.

Exemple commenté

Ce que l’interface cache, et ce qu’elle ne cache délibérément pas

Vérification de la conception à l’aune des idées de cette section.

Décision cachéeQuel prestataire, son SDK et sa version d’API, ses codes d’erreur, ses règles de nouvelle tentative, ses montants décimaux ou entiers, ses identifiants de transaction.
Contrat (préconditions)Un montant positif en unités mineures, une devise ISO 4217 prise en charge, une clé d’idempotence unique par tentative de paiement.
Contrat (postconditions)approved signifie que les fonds sont réservés et qu’une référence est renvoyée ; declined signifie qu’aucun argent n’a bougé ; unknown signifie que le résultat n’a pas pu être déterminé et que le même appel peut être relancé avec la même clé.
Absence délibérée de masquageL’interface expose unknown au lieu de prétendre que les paiements réussissent ou échouent toujours : la fuite du réseau fait partie du contrat, ce n’est pas une surprise.
Coût d’un nouveau prestataireAvant : des modifications à 11 endroits d’appel, plus la table des commandes. Après : une nouvelle classe d’adaptateur, ses tests et une entrée de configuration.
ProfondeurTrois opérations cachent une part importante de logique propre à chaque prestataire : profonde plutôt que superficielle.

Méfiez-vous du piège du plus petit dénominateur commun : si le prestataire B offre une fonctionnalité utile qui manque à A, comme le paiement en plusieurs fois, soit l’interface s’enrichit d’une capacité facultative, soit la fonctionnalité reste indisponible. Les abstractions au-dessus de fournisseurs sacrifient toujours une partie des points forts de chacun en échange de la liberté d’en changer.

La loi de Hyrum s’applique toujours : si l’étape de paiement se met à dépendre de la latence habituelle du prestataire A, un prestataire B plus lent le révélera. Inscrivez les délais d’expiration dans le contrat.

Comment les professionnels l’appliquent

Les concepteurs expérimentés partent des besoins des appelants pour concevoir une interface, en l’écrivant dans le langage de l’appelant, et ne conçoivent l’implémentation qu’ensuite. Ils tiennent la liste des décisions que cache chaque module important et examinent les nouvelles interfaces au regard de cette liste. Ils traitent chaque API publique comme un produit doté d’un cycle de vie (proposition, revue, versionnement, dépréciation) parce que, d’après la loi de Hyrum, tout ce qui est publié sera utilisé. Et ils acceptent que les abstractions fuient, si bien qu’ils rendent les fuites explicites : budgets de latence, sémantique des erreurs et règles de nouvelle tentative figurent dans le contrat.

Le masquage de l’information vous dit ce que chaque module doit garder pour lui. Il ne vous dit pas dans quel sens doivent pointer les dépendances entre modules, ni ce qui se passe quand elles forment des boucles. C’est le sujet de la section 3.4.

Vérifiez votre compréhension

Pourquoi une classe dont tous les champs sont privés, avec des getters et des setters publics pour chacun, peut-elle quand même échouer à masquer l’information ?

Parce que l’interface révèle la représentation interne exacte : les appelants en dépendent, et tout changement de représentation les casse. L’encapsulation est présente, mais aucune décision n’est cachée.

En conception par contrat, qui est en faute si une précondition est violée ?

L’appelant, car satisfaire la précondition est son obligation ; une postcondition violée signale un bug chez le fournisseur.

Qu’est-ce qui rend un module profond au sens d’Ousterhout ?

Il fournit beaucoup de fonctionnalités derrière une interface petite et simple, si bien que le coût d’apprentissage de l’interface est faible par rapport à ce qu’elle cache.

Pourquoi la loi de Hyrum plaide-t-elle pour des réponses d’API réduites ?

Tout champ ou comportement observable finira par être utilisé par un client ; tout ce qui est exposé devient donc difficile à changer, et exposer moins laisse plus de liberté.

Suivant · 3.4DépendancesDans quel sens les dépendances doivent pointer et pourquoi : les principes des dépendances acycliques, des dépendances stables et des abstractions stables de Robert C. Martin, les métriques d’instabilité, de degré d’abstraction et de distance, la suppression des cycles, l’organisation en couches et les vérifications automatisées des dépendances.Continuer →

Sources et lectures complémentaires

Une question sur cette section ? ArchitectBot, l’assistant d’étude IA du cours, répond d’abord à partir du cours.

Demander : « Comment concevoir une API interne entre les services de deux équipes pour pouvoir changer l’implémentation sans que l’une casse l’autre ? »