Regola
Preferenza composizione rispetto ereditarietà
Eredità gerarchie gerarchie creare stretto accoppiamento
e rendono il codice più da capire e da mantenere.
Lingue supportate: 45+Introduzione
L'ereditarietà crea uno stretto accoppiamento tra classi padre e figlio, rendendo il codice fragile e difficile da modificare. Quando una classe eredita un comportamento, diventa dipendente dai dettagli di implementazione della classe padre. Le sottoclassi che sovrascrivono i metodi ma continuano a chiamare super sono particolarmente problematici, poiché mescolano la propria logica con comportamenti ereditati in modi che non funzionano più quando l'oggetto padre cambia. La composizione risolve questo problema consentendo agli oggetti di delegare ad altri oggetti, creando un accoppiamento debole e una chiara separazione dei ruoli.
Perché è importante
Preoccupazioni contrastanti e forte interconnessione: L'ereditarietà costringe a raggruppare aspetti non correlati nella stessa gerarchia di classi. Una classe di pagamenti ricorrenti che eredita da un processore di pagamenti mescola la logica di pianificazione con l'elaborazione dei pagamenti. Quando è necessario chiamare super.process() e poi aggiungi il tuo comportamento, ti ritrovi strettamente legato all'implementazione del genitore. Se il genitore process() Se il metodo viene modificato, la classe derivata smette di funzionare in modi imprevisti.
Ereditare comportamenti indesiderati: Le sottoclassi ereditano tutto dalle classi padre, compresi i metodi di cui non hanno bisogno o che richiedono implementazioni diverse. Un pagamento ricorrente eredita refund() La logica è stata progettata per i pagamenti una tantum, ma i rimborsi relativi agli abbonamenti funzionano in modo diverso. O si sovrascrivono i metodi, creando confusione, oppure si deve accettare un comportamento ereditato non appropriato.
Problema della classe base "Fragile": Le modifiche apportate alle classi padre si ripercuotono su tutte le sottoclassi. Modificando il modo in cui Pagamento con carta di credito i processi di pagamento influiscono su Pagamento ricorrente con carta di credito anche se la modifica non ha alcuna rilevanza ai fini della pianificazione. Ciò rende il refactoring pericoloso, poiché non è possibile prevedere quali sottoclassi smetteranno di funzionare correttamente.
Complessità dei test: testare classi situate ai livelli più profondi di una gerarchia di ereditarietà richiede la comprensione del comportamento della classe padre. Per testare la pianificazione dei pagamenti ricorrenti, è necessario occuparsi anche della logica di elaborazione delle carte di credito, delle chiamate all’API di Stripe e della convalida. La composizione consente di testare la pianificazione utilizzando un semplice oggetto di pagamento simulato.
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é è sbagliato: Pagamento ricorrente con carta di credito eredita la logica di elaborazione dei pagamenti, ma il suo vero obiettivo è la pianificazione, non i pagamenti. Deve chiamare super.process() e lo integra con un meccanismo di pianificazione, creando un forte accoppiamento. La classe eredita da refund() dal conto principale, ma il rimborso degli abbonamenti richiede una logica diversa rispetto ai pagamenti una tantum. Le modifiche a Pagamento con carta di credito influire Pagamento ricorrente con carta di credito anche quando tali modifiche non incidono sulla programmazione.
✅ 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é è importante: Pagamento ricorrente con carta di credito si concentra esclusivamente sulla pianificazione e delega la gestione dei pagamenti al gruppo Pagamento con carta di credito istanza. L'assenza di ereditarietà implica l'assenza di un forte accoppiamento con l'implementazione della classe padre. Le modifiche alla gestione 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
Utilizza la composizione per separare i livelli di astrazione, anziché mescolarli tramite l'ereditarietà. Quando una classe necessita delle funzionalità di un'altra classe, considerala come una dipendenza e delegale il compito, anziché ereditare da essa. Ciò crea un accoppiamento debole, semplifica i test e impedisce che le modifiche apportate a una classe compromettano il funzionamento di un'altra.

