ルール
好意 構成 優先 継承
深い 継承 階層 作成 緊密な 結合
そして 作り出す コードを より困難に 理解 理解 し 保守
サポート言語: 45+はじめに
継承は親クラスと子クラス間の緊密な結合を生み出し、コードを脆弱で変更しにくくします。クラスが動作を継承すると、その親クラスの実装詳細に依存するようになります。メソッドをオーバーライドしながら依然として親クラスのメソッドを呼び出すサブクラスは、 超 特に問題となるのは、親が変更されると機能しなくなる方法で、独自のロジックと継承された動作を混在させる点である。コンポジションはこの問題を解決し、オブジェクトが他のオブジェクトに委譲することを可能にすることで、疎結合と明確な関心事の分離を実現する。
なぜそれが重要なのか
懸念事項と緊密な結合: 継承は、無関係な処理を同じクラス階層に強制的に組み込みます。支払い処理クラスから継承した定期支払いクラスは、スケジュール処理ロジックと支払い処理を混在させます。呼び出す必要がある場合 スーパープロセス() そして独自の動作を追加すると、親の実装に強く依存することになります。親の実装が process() メソッドの変更により、子クラスが予期せぬ形で動作しなくなる。
望ましくない行動の継承: サブクラスは親クラスから全てを継承します。不要なメソッドや異なる実装が必要なメソッドも含まれます。定期支払いは継承します 返金する() 単発支払いを想定したロジックだが、定期購読の返金は動作が異なる。メソッドを上書きして混乱を招くか、不適切な継承された動作をそのまま受け入れるかの選択となる。
脆弱な基底クラス問題: 親クラスの変更はすべてのサブクラスに波及する。変更の仕方を クレジットカード決済 支払いを処理する影響 定期クレジットカード決済 たとえその変更がスケジューリングに関係なくとも。これによりリファクタリングは危険となる。なぜなら、どのサブクラスが壊れるかを予測できないからだ。
テストの複雑性:継承階層の深い位置にあるクラスをテストするには、親クラスの動作を理解する必要があります。定期支払いのスケジュール設定をテストするには、クレジットカード処理ロジック、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
}なぜそれが間違っているのか: 定期クレジットカード決済 支払い処理ロジックを継承するが、その真の関心事は支払いではなくスケジューリングである。必ず呼び出す必要がある。 スーパープロセス() そしてスケジューリング動作でそれを包み込み、緊密な結合を生み出す。このクラスは継承する 返金する() 親からですが、定期購読の返金処理は単発購入とは異なるロジックが必要です。変更点は クレジットカード決済 影響を与える 定期クレジットカード決済 たとえそれらの変更がスケジューリングに関係がない場合でも。
✅ 準拠:
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(),
);なぜこれが重要なのか: 定期クレジットカード決済 スケジューリングのみに焦点を当て、支払い処理は構成されたものに委任する クレジットカード決済 インスタンス。継承がないということは、親クラスの実装との緊密な結合がないことを意味します。クレジットカード処理の変更はスケジューリングロジックに影響を与えません。支払いインスタンスは、スケジューリングコードを変更することなく、任意の支払い方法に置き換えることができます。
結論
継承によって関心を混在させるのではなく、コンポジションを用いて分離する。あるクラスが別のクラスの機能性を必要とする場合、それを依存関係として受け入れ、継承するのではなく委譲する。これにより疎結合が実現され、テストが容易になり、あるクラスの変更が別のクラスを壊すことを防ぐ。
.avif)
