Aikido

保守性と柔軟性の高いコードのために、継承よりもコンポジションを優先する方法

保守性

ルール
継承よりも コンポジションを優先する
深い 継承階層は密結合を生み出し、
コードの理解と保守を困難にします。

対応言語: 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 インスタンス。継承がないということは、親クラスの実装との密な結合がないことを意味します。クレジットカード処理の変更はスケジューリングロジックに影響を与えません。支払いインスタンスは、スケジューリングコードを変更することなく、任意の支払い方法に置き換えることができます。

まとめ

継承によって懸念事項を混在させるのではなく、コンポジションを使用して分離してください。あるクラスが別のクラスの機能を必要とする場合、それを依存関係として受け入れ、継承するのではなく、それに委譲してください。これにより、疎結合が生まれ、テストが容易になり、あるクラスの変更が別のクラスを壊すのを防ぎます。

よくある質問

ご質問がありますか?

継承とコンポジションは、いつ使い分けるべきですか?

継承は、サブクラスが親の真に特殊化されたバージョンである、真の「is-a」関係にのみ使用してください。ドメインにおいて正方形が長方形である場合、Rectangleを拡張するSquareは理にかなっています。「has-a」または「uses-a」関係にはコンポジションを使用してください。定期的な支払いは支払いプロセッサを使用しますが、支払いプロセッサの一種ではありません。迷った場合は、コンポジションを優先してください。

複数のソースからコードを再利用する必要がある場合はどうなりますか?

コンポジションは、複数の依存関係を通じてこれを自然に処理します。クラスは、多重継承の制約と競合することなく、支払いプロセッサ、スケジューラ、および通知機能を構成できます。継承は、単一継承言語または複雑な多重継承階層を強制します。コンポジションはより明確です。各依存関係はコンストラクタで明示されます。

継承をコンポジションへ、どのようにリファクタリングしますか?

サブクラスが実際に何を行うか、そして何を継承しているかを特定します。例として、RecurringCreditCardPaymentは支払いをスケジュールしますが、処理ロジックを継承しています。親の機能を別のクラスに抽出し、それを依存関係として渡します。extends Parentをコンストラクタパラメータに置き換えます。super.method()呼び出しをthis.dependency.method()に置き換えます。各ステップをテストします。

コンポジションはより多くのボイラープレートを作成しませんか?

初期設定では明示的な依存関係が必要ですが、この明確さは価値があります。親階層を辿ることなく、各クラスが必要とするものを正確に把握できます。最新の依存性注入フレームワークは、定型コードを削減します。明示性により、暗黙的に継承された動作によるバグを防ぎます。数行の追加設定コードは、柔軟性と保守性の価値があります。

抽象基底クラスとインターフェースについてはどうですか?

インターフェースは、実装を結合することなく契約を定義するのに優れています。インターフェースを使用してクラスが必要とする振る舞いを指定し、その後具体的な実装を注入します。抽象クラスは、未実装のメソッドを持つ単なる継承であり、同じ結合の問題を抱えています。継承を伴う抽象クラスよりも、コンポジションを伴うインターフェースを推奨します。

共有ユーティリティメソッドをどのように処理しますか?

それらを個別のユーティリティクラスまたはサービスに抽出します。共有の検証ロジックを継承する代わりに、Validatorサービスを注入します。例として、単発支払いと定期支払いの両方が同じ検証を必要とする場合、両方がコンポジションを通じて使用できる共有のPaymentValidatorを作成します。これにより、共有ロジックが親クラスに隠されたメソッドよりも発見しやすく、テストしやすくなります。

今すぐ、安全な環境へ。

コード、クラウド、ランタイムを1つの中央システムでセキュアに。
脆弱性を迅速に発見し、自動的に修正。

クレジットカードは不要です | スキャン結果は32秒で表示されます。