Regla
Priorizar la composición sobre la herencia
Las jerarquías de herencia profundas crean un acoplamiento fuerte
y dificultan la comprensión y el mantenimiento del código.
Lenguajes compatibles: 45+Introducción
La herencia crea un acoplamiento fuerte entre las clases padre e hijo, lo que hace que el código sea frágil y difícil de modificar. Cuando una clase hereda comportamiento, se vuelve dependiente de los detalles de implementación de su clase padre. Las subclases que sobrescriben métodos pero aún llaman super son particularmente problemáticos, mezclando su propia lógica con comportamientos heredados de formas que se rompen cuando el padre cambia. La composición resuelve esto permitiendo que los objetos deleguen en otros objetos, creando un acoplamiento débil y una clara separación de responsabilidades.
Por qué es importante
Preocupaciones mixtas y acoplamiento fuerte: La herencia fuerza la inclusión de preocupaciones no relacionadas en la misma jerarquía de clases. Una clase de pago recurrente que hereda de un procesador de pagos mezcla la lógica de programación con el procesamiento de pagos. Cuando necesita llamar super.process() y luego añades tu propio comportamiento, estás fuertemente acoplado a la implementación del padre. Si el del padre process() el método cambia, la clase hija falla de formas inesperadas.
Heredar comportamientos no deseados: Las subclases heredan todo de sus clases padre, incluyendo métodos que no necesitan o que requieren implementaciones diferentes. Un pago recurrente hereda refund() lógica diseñada para pagos únicos, pero los reembolsos de suscripciones funcionan de manera diferente. O bien sobrescribe métodos y crea confusión, o convive con un comportamiento heredado inapropiado.
Problema de la clase base frágil: Los cambios en las clases padre se propagan a todas las subclases. Modificar cómo Pago con tarjeta de crédito procesa pagos afecta PagoRecurrenteConTarjetaDeCrédito aunque el cambio sea irrelevante para la planificación. Esto hace que la refactorización sea peligrosa porque no se puede predecir qué subclases se romperán.
Complejidad de las pruebas: Probar clases en lo profundo de una jerarquía de herencia requiere comprender el comportamiento de la clase padre. Para probar la programación de pagos recurrentes, también debe lidiar con la lógica de procesamiento de tarjetas de crédito, las llamadas a la API de Stripe y la validación. La composición le permite probar la programación con un objeto de pago simulado simple.
Ejemplos de código
❌ No 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
}Por qué es incorrecto: PagoRecurrenteConTarjetaDeCrédito hereda la lógica de procesamiento de pagos, pero su verdadera preocupación es la programación, no los pagos. Debe llamar super.process() y envolverlo con un comportamiento de programación, creando un acoplamiento fuerte. La clase hereda refund() del principal, pero el reembolso de suscripciones requiere una lógica diferente a la de los pagos únicos. Cambios en Pago con tarjeta de crédito afectar PagoRecurrenteConTarjetaDeCrédito incluso cuando esos cambios son irrelevantes para la planificación.
✅ 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(),
);¿Por qué esto importa? PagoRecurrenteConTarjetaDeCrédito se centra únicamente en la programación y delega el procesamiento de pagos al compuesto Pago con tarjeta de crédito instancia. Sin herencia, no hay un acoplamiento estricto a la implementación de la clase padre. Los cambios en el procesamiento de tarjetas de crédito no afectan la lógica de planificación. La instancia de pago puede ser reemplazada por cualquier método de pago sin modificar el código de planificación.
Conclusión
Utilice la composición para separar responsabilidades en lugar de mezclarlas mediante herencia. Cuando una clase necesita la funcionalidad de otra clase, acéptela como una dependencia y delegue en ella en lugar de heredar. Esto crea un acoplamiento débil, facilita las pruebas y evita que los cambios en una clase rompan otra.
.avif)
