Du « ça marche » au « c'est garanti »
À la leçon précédente, tu as vu des objets différents répondre au même message : Chien, Chat et Vache savent tous parler(), et la boucle les appelle sans se soucier de l'espèce. Mais une question reste en suspens : qui garantit que chaque animal sait vraiment parler ? Rien, pour l'instant. Si tu glisses un objet sans parler() dans la liste, la boucle plante.
L'interface, c'est le papier qui transforme « ça marche par chance » en « c'est garanti ». Le polymorphisme fait répondre les objets ; l'interface promet qu'ils répondront tous.
Le contrat : une liste de promesses, zéro implémentation
Pense à une prise électrique. Le mur expose deux trous à un format précis. Il ne sait pas, et ne veut pas savoir, ce que tu vas brancher : une lampe, un grille-pain, un téléphone. Tout ce qu'il exige, c'est que ta fiche respecte le format. Si elle le respecte, ça marche. C'est tout.
Une interface, c'est exactement cette prise. Elle déclare une liste de promesses (les méthodes à fournir) sans dire comment les tenir. Elle ne contient aucun code exécutable : juste les noms et la forme des méthodes attendues.
interface Notifier:
envoyer(message) // la promesse : QUOI, jamais COMMENT
// Chaque classe TIENT la promesse à sa façon
classe EmailNotifier implémente Notifier:
public envoyer(message): // ... envoie un e-mail
classe SmsNotifier implémente Notifier:
public envoyer(message): // ... envoie un SMS
L'interface Notifier ne dit rien de l'e-mail ni du SMS. Elle dit une seule chose : « tout Notifier sait envoyer(message) ». Le comment appartient à chaque classe.
Dépendre du contrat, pas de l'implémentation
Voici la vraie puissance. Imagine un service d'inscription qui doit prévenir l'utilisateur. Tu pourrais écrire ton code en dur, soudé à l'e-mail :
Un service Inscription reçoit un Notifier et appelle notifier.envoyer("Bienvenue"), sans jamais savoir s'il s'agit d'un e-mail, d'un SMS ou de Slack. Avant de dérouler : que faut-il changer dans Inscription pour passer de l'e-mail au SMS ? Et si demain le patron veut aussi notifier sur Slack ?
Voir la réponse
Rien ne change dans Inscription. Le service ne connaît que le contrat Notifier : « je sais envoyer(message) ». Passer de l'e-mail au SMS, c'est lui injecter un SmsNotifier à la place d'un EmailNotifier. Ajouter Slack, c'est écrire une nouvelle classe SlackNotifier qui implémente Notifier et l'injecter : pas une ligne du service à toucher. C'est ça, dépendre du contrat : le service s'appuie sur la promesse, jamais sur un fournisseur précis.
// ❌ Couplé en dur : Inscription connaît l'e-mail
classe Inscription:
public sInscrire(user):
new EmailNotifier().envoyer("Bienvenue " + user)
// pour passer au SMS, il faut rouvrir et modifier Inscription
// ✅ Couplé au contrat : Inscription ne connaît qu'un Notifier
classe Inscription:
privé notifier // un Notifier, peu importe lequel
constructeur(notifier):
this.notifier = notifier
public sInscrire(user):
this.notifier.envoyer("Bienvenue " + user)
// On injecte le fournisseur qu'on veut
new Inscription(new EmailNotifier()) // par e-mail
new Inscription(new SmsNotifier()) // par SMS, sans toucher Inscription
Dans la version du bas, Inscription ne dépend plus d'un fournisseur précis : elle dépend du contrat. Tu lui branches l'e-mail, le SMS, Slack, un faux notifier pour tes tests… le service reste identique. C'est le principe qu'on répète chez les pros : « programmer une interface plutôt qu'une implémentation ». En équipe, tu entendras aussi l'anglicisme « coder contre l'interface », calqué sur l'anglais to code against (où against veut dire « en s'appuyant sur », pas « en opposition à »).
Règle d'or : dépendre d'un contrat (l'interface) rend ton code ouvert au changement. Le jour où tu ajoutes un fournisseur, tu écris une nouvelle classe, tu ne réécris pas l'ancien code. Un nouveau fournisseur se branche, il ne se greffe pas.
Le même mécanisme, en vrai JavaScript exécutable. Le service ignore tout du canal : il appelle juste envoyer(). Lance le code, puis ajoute une classe SlackNotifier avec sa propre méthode envoyer() et injecte-la, sans toucher la classe Inscription.
class EmailNotifier {
envoyer(message) { return "📧 e-mail : " + message; }
}
class SmsNotifier {
envoyer(message) { return "📱 SMS : " + message; }
}
class Inscription {
constructor(notifier) {
this.notifier = notifier; // un Notifier, peu importe lequel
}
sInscrire(user) {
return this.notifier.envoyer("Bienvenue " + user);
}
}
// On injecte le canal voulu, le service ne change pas
console.log(new Inscription(new EmailNotifier()).sInscrire("Alice"));
console.log(new Inscription(new SmsNotifier()).sInscrire("Bob"));
Interface ou classe abstraite ?
Les deux fixent un contrat. La différence tient en une phrase : la classe abstraite peut, en plus du contrat, livrer un bout de code déjà écrit ; l'interface, jamais.
- Interface : que des promesses. Zéro code fourni. Une classe peut en respecter plusieurs.
- Classe abstraite : des promesses plus du code commun déjà écrit, partagé par tous les enfants. Une classe n'en a qu'un seul parent.
// Classe abstraite : elle DONNE déjà du code commun
classe abstraite NotifierBase:
public envoyer(message): // commun à tous : on horodate
log("[" + maintenant() + "] " + this.canal(message))
abstraite canal(message) // promesse : à compléter par l'enfant
classe EmailNotifier hérite NotifierBase:
public canal(message): retourne "e-mail → " + message
La règle simple : juste des promesses à imposer ? Prends une interface. Tu veux aussi partager du code tout fait entre les classes ? Prends une classe abstraite. Beaucoup de langages permettent même de combiner les deux : une classe abstraite qui implémente une interface.
Le fil qui relie tout
Reviens sur les deux leçons précédentes. La composition (leçon 5) marchait parce que la Voiture pouvait recevoir n'importe quel moteur exposant demarrer(). Le polymorphisme (leçon 6) marchait parce que la boucle faisait confiance à chaque animal pour répondre à parler().
Dans les deux cas, il y avait un contrat implicite. L'interface le rend explicite et vérifié : elle écrit noir sur blanc la promesse sur laquelle la composition et le polymorphisme s'appuient. C'est elle qui rend ces deux mécanismes sûrs : on ne croise plus les doigts en espérant que l'objet sait répondre, on l'exige par contrat.
Composition + polymorphisme + interface forment un trio : on assemble des pièces (composition), chaque pièce répond à sa façon (polymorphisme), et le contrat garantit qu'elles répondront toutes (interface). C'est la base de tout code souple et extensible.
From "it works" to "it's guaranteed"
In the previous lesson, you saw different objects answer the same message: Chien, Chat and Vache all know how to parler(), and the loop calls them without caring about the species. But one question stays open: who guarantees each animal really knows how to speak? Nothing, so far. Slip an object without parler() into the list, and the loop crashes.
The interface is the paper that turns "it works by luck" into "it's guaranteed". Polymorphism makes objects answer; the interface promises they all will.
The contract: a list of promises, zero implementation
Think of a wall socket. The wall exposes two holes in a precise shape. It doesn't know, and doesn't want to know, what you'll plug in: a lamp, a toaster, a phone. All it requires is that your plug matches the shape. If it matches, it works. That's it.
An interface is exactly that socket. It declares a list of promises (the methods to provide) without saying how to keep them. It holds no runnable code: just the names and shape of the expected methods.
interface Notifier:
envoyer(message) // the promise: WHAT, never HOW
// Each class KEEPS the promise its own way
class EmailNotifier implements Notifier:
public envoyer(message): // ... sends an e-mail
class SmsNotifier implements Notifier:
public envoyer(message): // ... sends an SMS
The Notifier interface says nothing about e-mail or SMS. It says one thing: "every Notifier knows how to envoyer(message)". The how belongs to each class.
Code against the contract, not against the implementation
Here's the real power. Picture a sign-up service that must notify the user. You could hard-wire your code to e-mail:
An Inscription service receives a Notifier and calls notifier.envoyer("Welcome"), never knowing whether it's e-mail, SMS or Slack. Before you expand: what must change in Inscription to switch from e-mail to SMS? And if tomorrow the boss also wants to notify on Slack?
See the answer
Nothing changes in Inscription. The service only knows the Notifier contract: "I know how to envoyer(message)". Switching from e-mail to SMS means injecting an SmsNotifier instead of an EmailNotifier. Adding Slack means writing a new SlackNotifier class that implements Notifier and injecting it: not one line of the service to touch. That's coding against the contract: the service depends on the promise, never on a specific provider.
// ❌ Hard-coupled: Inscription knows about e-mail
class Inscription:
public sInscrire(user):
new EmailNotifier().envoyer("Welcome " + user)
// to switch to SMS, you must reopen and edit Inscription
// ✅ Coupled to the contract: Inscription only knows a Notifier
class Inscription:
private notifier // a Notifier, whichever it is
constructor(notifier):
this.notifier = notifier
public sInscrire(user):
this.notifier.envoyer("Welcome " + user)
// Inject whichever provider you want
new Inscription(new EmailNotifier()) // by e-mail
new Inscription(new SmsNotifier()) // by SMS, without touching Inscription
In the bottom version, Inscription no longer depends on a specific provider: it depends on the contract. Plug in e-mail, SMS, Slack, a fake notifier for your tests… the service stays the same. That's the phrase the pros repeat: "code against the interface, not against the implementation".
Golden rule: depending on a contract (the interface) makes your code open to change. The day you add a provider, you write a new class, you don't rewrite the old code. A new provider plugs in, it doesn't get grafted on.
The same mechanism, in real, runnable JavaScript. The service knows nothing about the channel: it just calls envoyer(). Run the code, then add a SlackNotifier class with its own envoyer() method and inject it, without touching the Inscription class.
class EmailNotifier {
envoyer(message) { return "📧 e-mail: " + message; }
}
class SmsNotifier {
envoyer(message) { return "📱 SMS: " + message; }
}
class Inscription {
constructor(notifier) {
this.notifier = notifier; // a Notifier, whichever it is
}
sInscrire(user) {
return this.notifier.envoyer("Welcome " + user);
}
}
// Inject the channel you want, the service doesn't change
console.log(new Inscription(new EmailNotifier()).sInscrire("Alice"));
console.log(new Inscription(new SmsNotifier()).sInscrire("Bob"));
Interface or abstract class?
Both fix a contract. The difference fits in one sentence: the abstract class can, on top of the contract, ship a piece of already-written code; the interface never does.
- Interface: only promises. Zero code provided. A class can honor several.
- Abstract class: promises plus shared, already-written code for all the children. A class has only one parent.
// Abstract class: it ALREADY gives shared code
abstract class NotifierBase:
public envoyer(message): // shared by all: we timestamp
log("[" + now() + "] " + this.canal(message))
abstract canal(message) // promise: to be filled by the child
class EmailNotifier extends NotifierBase:
public canal(message): return "e-mail → " + message
The simple rule: only promises to enforce? Use an interface. You also want to share ready-made code across classes? Use an abstract class. Many languages even let you combine both: an abstract class that implements an interface.
The thread that ties it all together
Look back at the two previous lessons. Composition (lesson 5) worked because the Voiture could receive any engine exposing demarrer(). Polymorphism (lesson 6) worked because the loop trusted each animal to answer parler().
In both cases there was an implicit contract. The interface makes it explicit and checked: it writes down in black and white the promise composition and polymorphism rely on. It's what makes those two mechanisms safe: you no longer cross your fingers hoping the object can answer, you require it by contract.
Composition + polymorphism + interface form a trio: you assemble parts (composition), each part answers its own way (polymorphism), and the contract guarantees they all will (interface). That's the basis of all flexible, extensible code.
🎯 Pratique
S'entraîner (clique pour ouvrir) :
🧠 Rappel libre
Sans remonter dans la leçon : avec tes mots, qu'est-ce qu'une interface contient (et ne contient pas), et que veut dire « dépendre du contrat, pas de l'implémentation » ?
Notifier qui sait envoyer()) et non d'une classe précise. Résultat : on injecte e-mail, SMS ou Slack sans toucher au code appelant, comme la prise accepte n'importe quel appareil au bon format.⚖️ Juge le code de l'IA
Tu demandes à l'IA un service d'inscription qui peut notifier par e-mail ou SMS. Elle te propose ce code. Ton rôle de relecteur : l'accepter tel quel ou le rejeter, et dire pourquoi.
class Inscription {
sInscrire(user, canal) {
if (canal === 'email') return new EmailNotifier().envoyer(user);
else if (canal === 'sms') return new SmsNotifier().envoyer(user);
// ... un else if de plus à chaque nouveau canal
}
}
EmailNotifier et SmsNotifier, et teste le canal avec une cascade de if / else if. Conséquence : ajouter Slack oblige à rouvrir et modifier Inscription. Le bon réflexe : faire dépendre Inscription du contrat Notifier et lui injecter le notifier choisi (constructor(notifier) puis this.notifier.envoyer(...)). Là, un nouveau canal se branche sans toucher une ligne du service.this.notifier.envoyer(msg) sans jamais savoir si notifier est un e-mail, un SMS ou Slack. En quoi cela relie-t-il l'interface à la composition (leçon 5) et au polymorphisme (leçon 6) ?Tu sais écrire un contrat et faire reposer ton code dessus. Mais comment décider, dans un programme qui grossit, où va quoi ? Cinq lettres, cinq boussoles : SOLID. La dernière leçon du cours.
Leçon 8 : Bien concevoir : SOLID →