Jusqu'ici, on a manipulé des types simples, des nombres, du texte,
des booléens. Mais dans la vraie vie, les développeurs travaillent avec des concepts autrement plus riches, un
client, une commande, un produit. La programmation orientée objet, souvent abrégée POO, permet de créer ses
propres types pour modéliser ces concepts.
L'idée date des années 1960 et du langage Simula, conçu en Norvège pour simuler des systèmes réels comme le
trafic d'un port. Ses auteurs se sont rendu compte qu'il était plus simple de décrire un navire une bonne fois
pour toutes, avec ses caractéristiques et ce qu'il sait faire, que d'éparpiller ces informations dans des
dizaines de variables séparées. Soixante ans plus tard, la quasi-totalité du code que tu croises repose sur
cette idée.
Modéliser le métier
Avant d'écrire la moindre ligne de code, il faut comprendre le domaine métier. C'est pour ça qu'il est crucial de
briefer au maximum les développeurs : le vocabulaire utilisé, les concepts, l'organisation, le besoin actuel, les
idées futures... Tout ça peut modifier la conception au plus profond du code.
On appelle Domain Driven Design (DDD) l'approche de conception visant à calquer le code sur le domaine métier de
la vie réelle. Nous lui consacrerons
un chapitre entier plus loin.
Exemple :
Dans l'entreprise, il y a un service marketing et un service finance. Les deux suivent les ventes, mais pas de la même façon. Le marketing veut le nombre de paiements et le chiffre d'affaires. La finance a besoin du détail des transactions pour calculer la TVA, les frais du partenaire de paiement...
En tant que développeur, plusieurs questions se posent immédiatement :
- Le vocabulaire : le marketing parle de "paiements", la finance de "transactions". Est-ce vraiment le même concept ? Par exemple, un client doit payer son abonnement. On initie une transaction bancaire. Elle échoue. Dans 2 jours, on en lance une autre. On en déduit donc qu'un paiement est composé de plusieurs transactions. On peut maintenant modéliser cette liaison dans le code.
- Le périmètre : le marketing ne s'intéresse pas aux remboursements ni aux fraudes. Il ne regarde que les ventes. La finance, si.
- Les données : pour le marketing, un paiement c'est une date et un montant. Pour la finance, une transaction contient la TVA, le partenaire utilisé, les frais associés...
On va affiner pour mettre en place un langage commun entre tout le monde : le langage ubiquitaire. Cette
modélisation peut prendre un paquet de temps, mais c'est un investissement qui se rembourse sur le long terme.
Mon exemple préféré que je croise au moins 2 fois par an : "produit" vs "offre". C'est une confusion classique
chez les entreprises qui vendent des abonnements, où le même service se décline en une dizaine de formules
commerciales.
Les classes et les objets
Une fois le domaine modélisé, on crée des types dédiés dans le code. Pour ça, on écrit une classe : la description
de ce que contient et fait un concept.
Imagine un architecte qui dessine un plan pour une maison. À partir de ce plan, il peut construire tout un
lotissement de maisons quasi-identiques. La classe, c'est le plan. L'objet, c'est la maison construite.
class Client {
constructor(email) {
this.email = email
this.transactions = []
}
ajouterTransaction(montant, date) {
this.transactions.push({ montant: montant, date: date })
}
totalDepense() {
let total = 0
this.transactions.forEach(function(t) {
total = total + t.montant
})
return total
}
}
// Création d'un objet "robert" à partir de la classe Client
let robert = new Client("robert@gmail.com")
robert.ajouterTransaction(29.99, "2024-01-15")
robert.ajouterTransaction(29.99, "2024-02-15")
console.log(robert.email) // robert@gmail.com
console.log(robert.totalDepense()) // 59.98
Le mot-clé new crée un nouvel objet à partir de la classe. En langage technique on parle "de créer
une instance" ou "d'instancier" une classe. Le constructor
est la fonction qui s'exécute automatiquement à la création pour initialiser l'objet. Le mot-clé this
fait référence à l'objet lui-même : this.email signifie "l'email de cet objet-ci".
ajouterTransaction et totalDepense sont des fonctions attachées à la classe. Dans le
contexte d'un objet, on les appelle des "méthodes" plutôt que des "fonctions". C'est exactement le même concept,
juste un nom différent.
Robert est un Client de la même façon que 42 est un nombre et "toto" une chaîne de caractères. D'ailleurs, dans
certains langages, les types primitifs sont aussi représentés comme des objets. Sur une string par exemple, on peut
faire "bonjour".length pour connaître la longueur du texte.
L'encapsulation
Le premier intérêt de l'objet, c'est de ranger au même endroit des données et les traitements qui vont avec.
Le total dépensé par Robert se calcule dans la classe Client, et nulle part ailleurs. Le jour où
la règle change, parce qu'il faut désormais exclure les transactions remboursées, on modifie une seule
méthode et tout le programme en profite.
C'est ce qu'on appelle l'encapsulation. L'objet expose ce qu'il sait faire et protège la façon dont il le
fait. Vu de l'extérieur, on demande robert.totalDepense() sans jamais avoir à savoir comment la
liste des transactions est rangée à l'intérieur.
Exemple :
Une voiture expose une pédale d'accélérateur. Tu appuies, elle avance. Tu n'as pas besoin de savoir si c'est un moteur thermique, un moteur électrique, ou quelle quantité de carburant est injectée. Le constructeur peut changer tout le moteur sans changer ta façon de conduire, tant que la pédale garde le même rôle.
Sans encapsulation, la règle de calcul se retrouve recopiée dans quinze endroits du code. On en corrige quatorze le jour où elle change, et le quinzième produit un chiffre faux pendant des mois avant que quelqu'un ne s'en aperçoive.
L'héritage
En modélisant un métier, on tombe vite sur deux concepts presque identiques, qui ne se séparent que sur
quelques détails. Un client particulier et un client professionnel partagent un email et des transactions, mais
le second a en plus un numéro de TVA et des conditions tarifaires. Plutôt que de tout recopier, on peut dire
que ClientPro est un type particulier de Client. Techniquement, on va mettre en place
un héritage des propriétés et des comportements de la classe mère Client vers la classe enfant
ClientPro. Dans la plupart des langages, on dit que la classe ClientPro "étend" la
classe Client, qu'elle récupère automatiquement tout ce que Client contient et sait
faire, et qu'elle y ajoute de nouvelles fonctionnalités.
Pour la suite de ce chapitre, on quitte le JavaScript. PHP se prête mieux à ces concepts, parce qu'il permet de dire explicitement ce qu'on cherche justement à montrer ici. Tu devrais malgré tout réussir à lire la quasi-totalité de ce qui suit, et c'est une nouvelle preuve que les langages de programmation se ressemblent bien plus qu'on ne le croit. À la fin de ce guide, tu arriveras à lire l'essentiel du code que tu croiseras, quel que soit le langage dans lequel il est écrit.
class ClientPro extends Client
{
public function __construct(string $email, public string $numeroTva)
{
parent::__construct($email); // on appelle le constructeur de Client
}
public function totalDepenseHorsTaxe(): float
{
return $this->totalDepense() / 1.2;
}
}
La question à se poser avant de mettre en place un héritage entre un enfant X et un parent Y : est-ce que X
est un Y ?
Mais attention, "être" quelque chose ne fait pas tout. Personne n'aurait l'idée d'inventer une classe mère
TrucQuiRoule au-dessus d'une boule de bowling et d'une voiture. Se ressembler sur un point ne
fait pas une famille.
Autre limite à connaître, presque tous les langages refusent l'héritage multiple, où une classe descendrait de deux mères à la fois. Une classe n'a donc qu'un seul parent direct. Et c'est tant mieux ! Nous allons voir juste après que l'héritage n'est pas toujours la bonne réponse, et qu'il peut introduire de mauvaises conceptions sur le long terme. Rien n'interdit en revanche d'empiler les générations, la mère ayant à son tour une mère.
Généralisation et spécialisation
L'héritage sert surtout à préciser des concepts qui n'ont pas vraiment de sens tout seuls. Par exemple, un
paiement se fait par carte bancaire ou par prélèvement, chacun avec ses règles et ses informations propres. Ces
deux classes partagent beaucoup, on leur donne donc une mère commune MoyenDePaiement.
class MoyenDePaiement
{
public bool $estParDefaut = false;
public string $ajouteLe = "2026-03-14";
public function definirParDefaut(): void
{
$this->estParDefaut = true;
}
public function debiter(float $montant): void
{
// ... et là, on écrit quoi ?
}
}
class CarteBancaire extends MoyenDePaiement
{
public function __construct(
public string $nomDuTitulaire,
public string $numero,
public int $cvc,
) {}
public function debiter(float $montant): void { /* appel au partenaire bancaire */ }
}
class Prelevement extends MoyenDePaiement
{
public function __construct(
public string $iban,
public string $referenceDuMandat,
) {}
public function debiter(float $montant): void { /* génération d'un ordre SEPA */ }
}
La classe MoyenDePaiement sert de type principal pour les règles métier qui se contentent du
concept général, sans avoir besoin de savoir à quoi elles ont affaire. Lister les moyens enregistrés d'un
client, désigner celui à utiliser par défaut, retenir depuis quand il est là, tout ça se traite pareil pour
une carte et pour un prélèvement, et s'écrit donc une seule fois dans la mère.
Sauf qu'une méthode résiste : debiter. Chaque enfant sait la remplir, la mère non, débiter une
carte et générer un ordre SEPA n'ont rien en commun. Et surtout, le concept de moyen de paiement lui-même est
trop vague, trop abstrait. Si tu arrives à la caisse d'un magasin en annonçant que tu vas payer avec ton moyen
de paiement, on te demandera lequel tu vas concrètement utiliser.
Beaucoup de langages offrent la possibilité de déclarer explicitement la classe MoyenDePaiement
comme étant abstraite. Les développeurs ne pourront alors plus créer un moyen de paiement sans préciser
concrètement s'il s'agit d'une carte ou d'un prélèvement. Il suffit du mot-clé abstract, posé devant la
classe et devant la méthode qu'on refuse d'écrire.
abstract class MoyenDePaiement
{
public bool $estParDefaut = false;
// Écrite ici une seule fois, elle sert à tous les enfants
public function definirParDefaut(): void
{
$this->estParDefaut = true;
}
// Déclarée, mais pas écrite, chaque enfant devra fournir sa version
abstract public function debiter(float $montant): void;
}
new MoyenDePaiement(); // refusé : la classe est abstraite
new CarteBancaire("Robert", "4970...", 123); // autoriséL'intérêt est double : on empêche la création d'objets qui n'ont aucun sens dans le métier, et on oblige chaque enfant à fournir les comportements que la mère déclare sans savoir les écrire.
Quand l'héritage se retourne contre toi
L'héritage est séduisant, et c'est justement son piège. Il fige une seule façon de classer les choses, décidée en début de projet, quand on ne connaît encore ni tous les cas ni ce que le métier demandera dans deux ans. Et tout le reste du code s'appuie ensuite dessus. Reprenons la hiérarchie de tout à l'heure.
abstract class MoyenDePaiement
{
abstract public function debiter(float $montant): void;
abstract public function rembourser(float $montant): void;
}
class CarteBancaire extends MoyenDePaiement { /* numéro, cvc, date d'expiration */ }
class Prelevement extends MoyenDePaiement { /* iban, référence du mandat */ }Sur le papier, c'est limpide. Une carte est un moyen de paiement, un prélèvement aussi. Les ennuis commencent quand le métier évolue, ce qu'il fait toujours :
- Un cas qui ne rentre pas : six mois plus tard, on ajoute le paiement à la livraison. Rien n'est
débité au moment de la commande, l'argent change de main plus tard, en espèces, et il n'y a rien à
rembourser automatiquement. Or
debiteretremboursersont posés dans la classe mère, donc imposés à tout ce qui en descend. Pour faire entrer ce nouveau cas, il faut toucher à la racine, celle dont dépend tout le reste. - Une deuxième façon de classer : arrive le paiement en trois fois. Une carte peut être en trois fois,
un prélèvement aussi. Il y a donc maintenant deux manières de trier les moyens de paiement, par instrument
et par échéancier, alors qu'un héritage n'en accepte qu'une puisque chaque classe n'a qu'un seul parent. Il
faut choisir celle qui portera la hiérarchie, et écrire une classe pour chaque combinaison de l'autre,
CarteEnTroisFois,PrelevementEnTroisFois. Et si demain un troisième critère apparaît, le paiement à trente jours réservé aux professionnels par exemple, le nombre de variantes à écrire est encore multiplié. - La tentation de mal hériter : la classe d'à côté contient déjà le code dont tu as besoin, et il
suffit d'en hériter pour le récupérer. C'est là que les hiérarchies tournent à l'absurde et qu'une voiture
devient un
TrucQuiRoule.
La composition
Les développeurs ont tiré les leçons de décennies d'héritage à outrance. Ils préfèrent maintenant "composer" des objets. Au lieu de faire descendre une classe d'une autre, on lui donne les briques dont elle a besoin, chacune responsable d'une seule chose. On ne dit plus qu'un moyen de paiement est d'un certain type, mais qu'il a un instrument et un échéancier.
// Le moyen de paiement ne sait ni comment l'argent circule,
// ni comment la somme est découpée. Il délègue à ses briques.
class MoyenDePaiement
{
public function __construct(
private Instrument $instrument, // une carte, un iban, un compte Paypal
private Echeancier $echeancier, // tout de suite, en trois fois, à la livraison
) {}
public function encaisser(float $montant): void
{
foreach ($this->echeancier->decouper($montant) as $echeance) {
$this->instrument->debiter($echeance);
}
}
}
$carteEnTroisFois = new MoyenDePaiement(new Carte("4970...", 123), new EnPlusieursFois(3));
$prelevement = new MoyenDePaiement(new Iban("FR76..."), new ToutDeSuite());
$paypal = new MoyenDePaiement(new ComptePaypal("robert@gmail.com"), new ToutDeSuite());
Les pièges de tout à l'heure disparaissent. Le paiement à la livraison devient un échéancier de plus, et le
découpage en trois fois est écrit une seule fois et sert à tous les instruments.
Chaque brique reste indépendante, testable et remplaçable, et on peut en changer une sans toucher aux autres.
Préférer la composition à l'héritage <adage de développeur>
Ça ne condamne pas l'héritage, qui reste le bon outil quand la relation est durable et évidente. La question à se poser tient en une phrase : est-ce que cette chose est définitivement l'autre, ou est-ce qu'elle a seulement une particularité de plus ?
Pourquoi c'est important pour toi ?
Même si tu n'écriras peut-être jamais de code, comprendre cette démarche de modélisation change ta façon de
communiquer avec les équipes techniques. Quand un développeur te demande de clarifier la différence entre un
"produit" et une "offre", ce n'est pas du pinaillage. C'est la fondation sur laquelle tout le système repose.
Un vocabulaire flou en entrée produit un code flou en sortie. Et un code flou produit des bugs, de la dette
technique et des fonctionnalités qui ne correspondent pas au besoin.
Ça explique aussi pourquoi certaines demandes qui semblent minuscules coûtent cher. "Ajouter juste un statut
sur la commande" peut toucher une classe dont dépendent la facturation, les emails et les statistiques. À
l'inverse, une demande qui te paraît énorme est parfois triviale, parce que le concept avait été bien modélisé
dès le départ.
Prends le temps de formaliser ton vocabulaire métier, même en dehors du code. Qu'une "commande" ne désigne pas tout à fait la même chose au marketing et à la logistique n'est pas un problème, chaque métier a son modèle et le rôle des développeurs est de traduire de l'un vers l'autre. Ce qui coûte cher, c'est quand personne n'a remarqué la différence et que deux équipes croient parler du même concept.