Règle
Composition composition sur l'héritage
Profondeur héritage hiérarchies créer couplage couplage
et rendent code plus à comprendre et maintenir.
Langues prises en charge : 45+Introduction
L'héritage crée un couplage fort entre les classes parentes et enfants, rendant le code fragile et difficile à modifier. Lorsqu'une classe hérite d'un comportement, elle devient dépendante des détails d'implémentation de son parent. Les sous-classes qui surchargent des méthodes mais appellent toujours super sont particulièrement problématiques, mêlant leur propre logique à un comportement hérité de manière à provoquer des ruptures lorsque le parent change. La composition résout ce problème en permettant aux objets de déléguer à d'autres objets, créant un couplage lâche et une séparation claire des préoccupations.
Pourquoi c'est important
Préoccupations diverses et couplage fort : L'héritage force des préoccupations non liées dans la même hiérarchie de classes. Une classe de paiement récurrent qui hérite d'un processeur de paiement mélange la logique de planification avec le traitement des paiements. Lorsque vous devez appeler super.process() et ensuite ajouter votre propre comportement, vous êtes étroitement couplé à l'implémentation du parent. Si le parent process() si la méthode change, la classe enfant se comporte de manière inattendue.
Hériter d'un comportement indésirable : Les sous-classes héritent de tout de leurs parents, y compris des méthodes dont elles n'ont pas besoin ou qui nécessitent des implémentations différentes. Un paiement récurrent hérite refund() logique conçue pour les paiements uniques, mais les remboursements d'abonnement fonctionnent différemment. Vous surchargez les méthodes et créez de la confusion, ou vous vivez avec un comportement hérité inapproprié.
Problème de la classe de base fragile : Les modifications apportées aux classes parentes se propagent à toutes les sous-classes. Modifier la manière dont Paiement par carte de crédit traite les paiements affecte Paiement récurrent par carte de crédit même si le changement est sans rapport avec la planification. Cela rend le refactoring dangereux car vous ne pouvez pas prédire quelles sous-classes seront affectées.
Complexité des tests : Tester des classes profondément imbriquées dans une hiérarchie d'héritage nécessite de comprendre le comportement de la classe parente. Pour tester la planification des paiements récurrents, vous devez également gérer la logique de traitement des cartes de crédit, les appels d'API Stripe et la validation. La composition vous permet de tester la planification avec un simple objet de paiement simulé (mock).
Exemples de code
❌ Non conforme :
class Payment {
constructor(amount, currency) {
this.amount = amount;
this.currency = currency;
}
async process() {
throw new Error('Must implement in subclass');
}
async refund() {
throw new Error('Must implement in subclass');
}
async sendReceipt(email) {
// All paymet types need receipts
await emailService.send(email, this.buildReceipt());
}
}
class CreditCardPayment extends Payment {
constructor(amount, currency, cardToken, billingAddress) {
super(amount, currency);
this.cardToken = cardToken;
this.billingAddress = billingAddress;
}
async process() {
await this.validateCard();
return await stripe.charges.create({
amount: this.amount * 100,
source: this.cardToken,
currency: this.currency
});
}
async refund() {
await this.validateRefund();
return await stripe.refunds.create({ charge: this.chargeId });
}
async validateCard() {
// Card validation logic
}
}
// Problem: RecurringCreditCardPayment's main concern is dealing with scheduling
// and not the actual payment
class RecurringCreditCardPayment extends CreditCardPayment {
constructor(amount, currency, cardToken, billingAddress, schedule) {
super(amount, currency, cardToken, billingAddress);
this.schedule = schedule;
}
async process() {
// Problem: Need to override parent's process() but also use it
await super.process();
await this.scheduleNextPayment();
}
async scheduleNextPayment() {
// Subscription scheduling
}
// Problem: Inherits refund() from parent but refunding
// subscriptions needs different logic
}Pourquoi c'est incorrect : Paiement récurrent par carte de crédit hérite de la logique de traitement des paiements mais sa véritable préoccupation est la planification, pas les paiements. Elle doit appeler super.process() et l'envelopper d'un comportement de planification, créant un couplage étroit. La classe hérite refund() du parent, mais le remboursement des abonnements nécessite une logique différente de celle des paiements uniques. Les modifications apportées à Paiement par carte de crédit affecter Paiement récurrent par carte de crédit même lorsque ces changements sont sans rapport avec la planification.
✅ Conforme :
class CreditCardPayment extends Payment {
constructor(amount, currency, cardToken, billingAddress) {
super(amount, currency);
this.cardToken = cardToken;
this.billingAddress = billingAddress;
}
async process() {
await this.validateCard();
return await stripe.charges.create({
amount: this.amount * 100,
source: this.cardToken,
currency: this.currency
});
}
async refund() {
await this.validateRefund();
return await stripe.refunds.create({ charge: this.chargeId });
}
async validateCard() {
// Card validation logic
}
}
class RecurringCreditCardPayment {
constructor(creditCardPayment, schedule) {
this.creditCardPayment = creditCardPayment;
this.schedule = schedule;
}
async scheduleNextPayment() {
this.schedule.onNextCyle(() => {
await this.creditCardPayment.process();
})
}
}
const recurringCreditCardPayment = new RecurringCreditCardPayment(
new CreditCardPayment(),
new Schedule(),
);Pourquoi c'est important : Paiement récurrent par carte de crédit se concentre uniquement sur la planification et délègue le traitement des paiements au composant Paiement par carte de crédit instance. L'absence d'héritage signifie l'absence de couplage fort avec l'implémentation de la classe parente. Les modifications apportées au traitement des cartes de crédit n'affectent pas la logique de planification. L'instance de paiement peut être remplacée par n'importe quelle méthode de paiement sans modifier le code de planification.
Conclusion
Utilisez la composition pour séparer les préoccupations au lieu de les mélanger par l'héritage. Lorsqu'une classe a besoin de la fonctionnalité d'une autre classe, acceptez-la comme une dépendance et déléguez-lui plutôt que d'en hériter. Cela crée un couplage faible, facilite les tests et empêche les modifications dans une classe de casser une autre.

