ルール
継承よりも コンポジションを優先する
深い 継承階層は密結合を生み出し、
コードの理解と保守を困難にします。
対応言語: 45+はじめに
継承は、親クラスと子クラスの間に密な結合を生み出し、コードを脆弱にし、変更を困難にします。クラスが振る舞いを継承すると、親の実装詳細に依存するようになります。メソッドをオーバーライドするが、依然として呼び出すサブクラスは super は特に問題が多く、親が変更されたときに壊れるような方法で、自身のロジックと継承された振る舞いを混在させます。コンポジションは、オブジェクトが他のオブジェクトに委譲できるようにすることでこれを解決し、疎結合と明確な関心の分離を生み出します。
なぜ重要なのか
懸念の混在と密結合: 継承は、無関係な関心事を同じクラス階層に強制します。支払い処理クラスから継承する定期支払いクラスは、スケジューリングロジックと支払い処理を混在させます。呼び出す必要がある場合、 super.process() そして独自の振る舞いを追加すると、親の実装に密結合になります。もし親の process() メソッドが変更されると、子クラスが予期せぬ形で壊れます。
不要な動作の継承: サブクラスは、不要なメソッドや異なる実装が必要なメソッドを含め、親クラスからすべてを継承します。定期的な支払いは継承します。 refund() ワンタイム決済用に設計されたロジックですが、サブスクリプションの払い戻しは異なる動作をします。メソッドをオーバーライドして混乱を招くか、不適切な継承された動作を受け入れるかのどちらかになります。
脆弱な基底クラス問題: 親クラスへの変更は、すべてのサブクラスに波及します。変更方法 CreditCardPayment 支払処理が影響を及ぼす 定期的なクレジットカード決済 変更がスケジューリングとは無関係であるにもかかわらず、です。これにより、どのサブクラスが壊れるか予測できないため、リファクタリングが危険になります。
テストの複雑さ: 継承階層の深い場所にあるクラスをテストするには、親クラスの動作を理解する必要があります。定期的な支払いスケジューリングをテストするには、クレジットカード処理ロジック、Stripe API呼び出し、および検証も処理する必要があります。コンポジションを使用すると、シンプルなモック支払いオブジェクトでスケジューリングをテストできます。
コード例
❌ 非準拠:
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
}誤っている理由: 定期的なクレジットカード決済 支払い処理ロジックを継承しますが、その本来の関心事は支払いではなくスケジューリングです。呼び出す必要があります。 super.process() そしてスケジューリングの振る舞いでラップすると、密結合を生み出します。そのクラスは継承します refund() 親からですが、サブスクリプションの返金には一回限りの支払いとは異なるロジックが必要です。変更点: CreditCardPayment 影響 定期的なクレジットカード決済 たとえそれらの変更がスケジューリングとは無関係な場合でもです。
✅ 準拠済み:
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(),
);これが重要である理由: 定期的なクレジットカード決済 スケジューリングのみに特化し、支払い処理は構成されたものに委任します。 CreditCardPayment インスタンス。継承がないということは、親クラスの実装との密な結合がないことを意味します。クレジットカード処理の変更はスケジューリングロジックに影響を与えません。支払いインスタンスは、スケジューリングコードを変更することなく、任意の支払い方法に置き換えることができます。
まとめ
継承によって懸念事項を混在させるのではなく、コンポジションを使用して分離してください。あるクラスが別のクラスの機能を必要とする場合、それを依存関係として受け入れ、継承するのではなく、それに委譲してください。これにより、疎結合が生まれ、テストが容易になり、あるクラスの変更が別のクラスを壊すのを防ぎます。

