Regola
Privilegia la composizione rispetto all'ereditarietà
Gerarchie di ereditarietà profonde creano un accoppiamento stretto e rendono il codice più difficile da comprendere e manutenere.
Linguaggi supportati: 45+Introduzione
L'ereditarietà crea un forte accoppiamento tra classi genitrici e figlie, rendendo il codice fragile e difficile da modificare. Quando una classe eredita un comportamento, diventa dipendente dai dettagli implementativi della sua superclasse. Le sottoclassi che sovrascrivono i metodi pur continuando a chiamare super risultano particolarmente problematiche, poiché mescolano la propria logica con il comportamento ereditato in modi che si rompono quando il genitore subisce modifiche. La composizione risolve questo problema consentendo agli oggetti di delegare ad altri oggetti, creando un accoppiamento lasco e una chiara separazione delle responsabilità.
Perché è importante
Responsabilità miste e forte accoppiamento: L'ereditarietà forza responsabilità non correlate all'interno della stessa gerarchia di classi. Una classe di pagamenti ricorrenti che eredita da un processore di pagamento mescola la logica di pianificazione con l'elaborazione dei pagamenti. Quando devi chiamare super.process() e successivamente aggiungere il tuo comportamento personalizzato, ti ritrovi strettamente accoppiato all'implementazione del genitore. Se l'implementazione del genitore process() i metodi cambiano, la classe figlia si rompe in modo inaspettato.
Ereditarietà di comportamenti indesiderati: Le sottoclassi ereditano tutto dai genitori, inclusi i metodi di cui non hanno bisogno o che richiedono implementazioni differenti. Un pagamento ricorrente eredita refund() una logica progettata per pagamenti una tantum, ma i rimborsi degli abbonamenti funzionano diversamente. O si sovrascrivono i metodi creando confusione, o si convive con un comportamento ereditato non idoneo.
Problema della classe base fragile (Fragile base class problem): Le modifiche alle classi genitori si propagano a tutte le sottoclassi. Modificare il modo in cui CreditCardPayment elabora i pagamenti influisce su RecurringCreditCardPayment anche se la modifica è irrilevante per la pianificazione. Questo rende il refactoring pericoloso poiché non è possibile prevedere quali sottoclassi si romperanno.
Complessità dei test: Testare classi profonde in una gerarchia di ereditarietà richiede la comprensione del comportamento della classe genitrice. Per testare la pianificazione dei pagamenti ricorrenti, devi anche gestire la logica di elaborazione delle carte di credito, le chiamate API di Stripe e la validazione. La composizione permette di testare la pianificazione con un semplice oggetto di pagamento mock.
Esempi di codice
❌ 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
}Perché è errato: RecurringCreditCardPayment eredita la logica di elaborazione dei pagamenti ma il suo vero obiettivo è la pianificazione, non i pagamenti. Deve chiamare super.process() e racchiuderla con il comportamento di pianificazione, creando un forte accoppiamento (tight coupling). La classe eredita refund() dal genitore, ma il rimborso degli abbonamenti richiede una logica diversa rispetto ai pagamenti una tantum. Le modifiche a CreditCardPayment influiscono su RecurringCreditCardPayment anche quando tali modifiche sono irrilevanti per la pianificazione.
✅ 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(),
);Perché questo è importante: RecurringCreditCardPayment si concentra esclusivamente sulla pianificazione e delega l'elaborazione dei pagamenti all'istanza CreditCardPayment composta. L'assenza di ereditarietà significa nessun forte accoppiamento con l'implementazione della classe genitrice. Le modifiche all'elaborazione delle carte di credito non influiscono sulla logica di pianificazione. L'istanza di pagamento può essere sostituita con qualsiasi metodo di pagamento senza modificare il codice di pianificazione.
Conclusione
Usa la composizione per separare le responsabilità (separation of concerns) invece di mescolarle tramite l'ereditarietà. Quando una classe ha bisogno delle funzionalità di un'altra, accettala come dipendenza e delega ad essa anziché ereditare. Questo crea un accoppiamento lasco (loose coupling), semplifica i test e impedisce che le modifiche in una classe ne rompano un'altra.

