Consommer une API externe paraît anodin au premier abord. On fait un requests.get, on récupère un dictionnaire, et on l’utilise tel quel dans le reste du code. Le problème commence quand cette même structure JSON se retrouve disséminée dans dix fichiers, et que l’API change un nom de champ ou passe price de float à string. La correction devient une chasse au trésor.
La couche anti-corruption (Anti-Corruption Layer, ou ACL) répond à ce problème. Issue du Domain-Driven Design, elle agit comme un traducteur entre un système externe et votre logique métier. Un seul point de contact, un seul endroit à modifier quand l’API change.
Le problème : un couplage diffus
Imaginons un service qui récupère des informations de voitures depuis une API tierce :
def display_car(url: str) -> None:
response = requests.get(url)
data = response.json()
print(f"{data['brand']} {data['model']} - {data['color_hex']}")
def price_with_tax(url: str) -> float:
response = requests.get(url)
data = response.json()
return float(data['price']) * 1.20
Le dictionnaire renvoyé par l’API circule partout. Si le fournisseur renomme color_hex en paint_code, il faut modifier chaque fonction qui y accède. Pire, si price passe de "15000.00" à 15000, les conversions float() éparpillées dans le code deviennent autant de bombes à retardement.
Ce couplage est invisible jusqu’au jour où il fait mal. C’est une forme particulièrement insidieuse de connascence : la structure d’une donnée externe devient un contrat implicite partagé par tout le code.
La solution : un objet métier dédié
L’ACL introduit un type intermédiaire qui représente ce que votre application comprend, pas ce que l’API renvoie :
from dataclasses import dataclass
import requests
@dataclass(frozen=True)
class Car:
brand: str
model: str
color: str
price: float
def fetch_car(url: str) -> Car:
response = requests.get(url, timeout=5)
response.raise_for_status()
data = response.json()
return Car(
brand=data["brand"],
model=data["model"],
color=data["color_hex"],
price=float(data["price"]),
)
Le reste de l’application ne manipule plus que des Car. Les transformations (renommer color_hex en color, convertir price en float) sont concentrées dans fetch_car. Le frozen=True empêche toute mutation accidentelle d’un objet censé refléter une donnée externe. Pour aller plus loin sur les subtilités des dataclasses immuables, voir field(default_factory) en profondeur.
Ce que l’ACL apporte vraiment
Le bénéfice évident est la résilience aux changements externes. Si l’API renomme un champ, vous touchez à une seule fonction. Mais l’ACL apporte trois autres choses moins visibles.
Un vocabulaire métier propre. Une API peut renvoyer usr_first_nm, dob_iso, acct_st. Votre application ne devrait pas vivre avec ces noms. L’ACL traduit en first_name, birth_date, account_status, ce qui rend le code appelant lisible sans contexte externe.
Une frontière claire entre I/O et logique. Toute la partie réseau (timeout, erreurs HTTP, parsing JSON) reste dans la couche. La logique métier ne sait rien de requests ou de httpx. Cela facilite les tests : on remplace fetch_car par un stub qui renvoie un Car, sans monter de mock HTTP.
Une typage de bout en bout. Le dictionnaire data n’a aucun type. Une fois converti en Car, mypy ou Pyright peut vérifier chaque accès. Les erreurs de champ deviennent des erreurs au lint, pas au runtime.
Gérer les champs absents ou invalides
Le code de la note d’origine utilisait data.get("marque"), qui retourne None si la clé manque. C’est une fausse sécurité : None se propage dans le code et explose plus loin, loin de la cause réelle.
L’ACL est l’endroit idéal pour valider :
@dataclass(frozen=True)
class Car:
brand: str
model: str
color: str
price: float
def fetch_car(url: str) -> Car:
response = requests.get(url, timeout=5)
response.raise_for_status()
data = response.json()
try:
return Car(
brand=data["brand"],
model=data["model"],
color=data["color_hex"],
price=float(data["price"]),
)
except (KeyError, ValueError, TypeError) as exc:
raise ValueError(f"Réponse API invalide : {exc}") from exc
L’erreur est levée au moment où elle est détectable, avec un message explicite. Le reste du code reçoit soit un Car valide, soit une exception claire.
Pour des contrats plus complexes, Pydantic remplit le même rôle avec validation déclarative :
from pydantic import BaseModel, Field
class Car(BaseModel):
brand: str
model: str
color: str = Field(alias="color_hex")
price: float
def fetch_car(url: str) -> Car:
response = requests.get(url, timeout=5)
response.raise_for_status()
return Car.model_validate(response.json())
Pydantic gère le renommage via alias, les conversions de type, et lève une ValidationError détaillée si la réponse ne correspond pas au schéma attendu.
Quand ne pas mettre d’ACL
L’ACL a un coût : un type supplémentaire à maintenir, une fonction de conversion à écrire. Pour un script qui appelle une API une seule fois et affiche le résultat, c’est de la sur-ingénierie.
Le pattern devient pertinent dès que :
- la même donnée externe est consommée à plusieurs endroits
- le vocabulaire de l’API diffère de celui de votre domaine
- l’API est instable ou hors de votre contrôle
- vous voulez pouvoir remplacer le fournisseur sans réécrire le code métier
À l’inverse, pour un appel ponctuel à votre propre API interne dont vous contrôlez le contrat, l’ACL n’apporte pas grand-chose.
Conclusion
La couche anti-corruption n’est pas un pattern complexe. C’est une discipline : refuser de laisser fuiter les structures externes dans le code métier. Le dict renvoyé par une API reste à la frontière, et tout ce qui circule à l’intérieur de l’application est un objet typé, validé, et nommé selon votre vocabulaire.
Le jour où l’API change, vous saurez exactement quel fichier ouvrir.
