Aikido

Wie man Komposition gegenüber Vererbung für wartbaren und flexiblen Code bevorzugt

Wartbarkeit

Regel
Komposition gegenüber Vererbung bevorzugen
Tiefe Vererbungshierarchien erzeugen eine starke Kopplung
und erschweren das Verständnis und die Wartung des Codes.

Unterstützte Sprachen: 45+

Einleitung

Vererbung erzeugt eine starke Kopplung zwischen Eltern- und Kindklassen, was den Code fragil und schwer änderbar macht. Wenn eine Klasse Verhalten erbt, wird sie von den Implementierungsdetails ihrer Elternklasse abhängig. Unterklassen, die Methoden überschreiben, aber immer noch aufrufen super sind besonders problematisch, da sie ihre eigene Logik mit geerbtem Verhalten auf eine Weise vermischen, die bei Änderungen des übergeordneten Elements zu Fehlern führt. Komposition löst dies, indem Objekte an andere Objekte delegieren, wodurch eine lose Kopplung und eine klare Trennung der Belange entsteht.

Warum es wichtig ist

Vermischte Anliegen und enge Kopplung: Vererbung zwingt unabhängige Belange in dieselbe Klassenhierarchie. Eine Klasse für wiederkehrende Zahlungen, die von einem Zahlungsabwickler erbt, vermischt Planungslogik mit Zahlungsabwicklung. Wenn Sie aufrufen müssen super.process() und dann Ihr eigenes Verhalten hinzufügen, sind Sie eng an die Implementierung des übergeordneten Elements gekoppelt. Wenn das übergeordnete Element process() Methodenänderungen führen dazu, dass die Kindklasse auf unerwartete Weise bricht.

Unerwünschtes Verhalten erben: Unterklassen erben alles von ihren Eltern, einschließlich Methoden, die sie nicht benötigen oder die andere Implementierungen erfordern. Eine wiederkehrende Zahlung erbt refund() Logik, die für einmalige Zahlungen konzipiert wurde, aber Abonnement-Rückerstattungen funktionieren anders. Sie überschreiben entweder Methoden und stiften Verwirrung, oder Sie leben mit unangemessenem geerbten Verhalten.

Fragiles Basisklassenproblem: Änderungen an Elternklassen wirken sich auf alle Unterklassen aus. Die Änderung der Art und Weise, wie Kreditkartenzahlung betrifft Zahlungsabwicklungen Wiederkehrende Kreditkartenzahlung obwohl die Änderung für die Zeitplanung irrelevant ist. Dies macht Refactoring gefährlich, da Sie nicht vorhersagen können, welche Unterklassen brechen werden.

Testkomplexität: Das Testen von Klassen tief in einer Vererbungshierarchie erfordert das Verständnis des Verhaltens der übergeordneten Klasse. Um die Planung wiederkehrender Zahlungen zu testen, müssen Sie sich auch mit der Logik der Kreditkartenverarbeitung, Stripe-API-Aufrufen und der Validierung befassen. Komposition ermöglicht es Ihnen, die Planung mit einem einfachen Mock-Zahlungsobjekt zu testen.

Code-Beispiele

❌ Nicht konform:

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
}

Warum es falsch ist: Wiederkehrende Kreditkartenzahlung erbt die Zahlungsabwicklungslogik, aber ihr eigentliches Anliegen ist die Terminplanung, nicht die Zahlungen. Sie muss aufrufen super.process() und es mit Planungsverhalten umwickeln, wodurch eine enge Kopplung entsteht. Die Klasse erbt refund() vom übergeordneten Element, aber die Rückerstattung von Abonnements erfordert eine andere Logik als einmalige Zahlungen. Änderungen an Kreditkartenzahlung betreffen Wiederkehrende Kreditkartenzahlung selbst wenn diese Änderungen für die Zeitplanung irrelevant sind.

✅ Konform:

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(),
);

Warum dies wichtig ist: Wiederkehrende Kreditkartenzahlung konzentriert sich ausschließlich auf die Terminplanung und delegiert die Zahlungsabwicklung an die komponierte Einheit Kreditkartenzahlung Instanz. Keine Vererbung bedeutet keine enge Kopplung an die Implementierung der Elternklasse. Änderungen an der Kreditkartenverarbeitung beeinflussen die Planungslogik nicht. Die Zahlungsinstanz kann durch jede Zahlungsmethode ersetzt werden, ohne den Planungscode zu ändern.

Fazit

Nutzen Sie Komposition, um Belange zu trennen, anstatt sie durch Vererbung zu vermischen. Wenn eine Klasse die Funktionalität einer anderen Klasse benötigt, akzeptieren Sie diese als Abhängigkeit und delegieren Sie an sie, anstatt von ihr zu erben. Dies führt zu einer losen Kopplung, erleichtert das Testen und verhindert, dass Änderungen in einer Klasse eine andere beeinträchtigen.

FAQs

Haben Sie Fragen?

Wann sollte ich Vererbung vs. Komposition verwenden?

Verwenden Sie Vererbung nur für echte „ist-ein“-Beziehungen, bei denen die Unterklasse tatsächlich eine spezialisierte Version der Elternklasse ist. Ein Quadrat, das ein Rechteck erweitert, ist sinnvoll, wenn Quadrate in Ihrer Domäne Rechtecke sind. Verwenden Sie Komposition für „hat-ein“- oder „nutzt-ein“-Beziehungen. Eine wiederkehrende Zahlung nutzt einen Zahlungsdienstleister, sie ist kein Typ eines Zahlungsdienstleisters. Im Zweifelsfall bevorzugen Sie Komposition.

Was, wenn ich Code aus mehreren Quellen wiederverwenden muss?

Komposition handhabt dies natürlich durch mehrere Abhängigkeiten. Eine Klasse kann einen Zahlungsabwickler, einen Scheduler und einen Benachrichtiger zusammensetzen, ohne mit Einschränkungen der Mehrfachvererbung zu kämpfen. Vererbung zwingt Sie in Sprachen mit einfacher Vererbung oder komplexe Mehrfachvererbungshierarchien. Komposition ist klarer: Jede Abhängigkeit ist explizit im Konstruktor.

Wie refaktorisere ich Vererbung zu Komposition?

Identifizieren Sie, was die Unterklasse tatsächlich tut, im Vergleich zu dem, was sie erbt. Im Beispiel plant RecurringCreditCardPayment Zahlungen, erbt aber die Verarbeitungslogik. Extrahieren Sie die Elternfunktionalität in eine separate Klasse und übergeben Sie sie dann als Abhängigkeit. Ersetzen Sie extends Parent durch einen Konstruktorparameter. Ersetzen Sie super.method()-Aufrufe durch this.dependency.method(). Testen Sie jeden Schritt.

Erzeugt Komposition nicht mehr Boilerplate?

Die initiale Einrichtung erfordert explizite Abhängigkeiten, aber diese Klarheit ist wertvoll. Sie sehen genau, was jede Klasse benötigt, ohne sich durch Elternhierarchien wühlen zu müssen. Moderne Dependency-Injection-Frameworks reduzieren Boilerplate-Code. Die Explizitheit verhindert Fehler durch implizit geerbtes Verhalten. Ein paar zusätzliche Zeilen Setup-Code sind die Flexibilität und Wartbarkeit wert.

Was ist mit abstrakten Basisklassen und Interfaces?

Interfaces eignen sich hervorragend, um Verträge zu definieren, ohne Implementierungen zu koppeln. Verwenden Sie Interfaces, um zu spezifizieren, welches Verhalten eine Klasse benötigt, und injizieren Sie dann konkrete Implementierungen. Abstrakte Klassen sind lediglich Vererbung mit einigen nicht implementierten Methoden; sie haben die gleichen Kopplungsprobleme. Bevorzugen Sie Interfaces mit Komposition gegenüber abstrakten Klassen mit Vererbung.

Wie gehe ich mit gemeinsam genutzten Utility-Methoden um?

Lagern Sie sie in separate Utility-Klassen oder Services aus. Anstatt gemeinsame Validierungslogik zu erben, injizieren Sie einen Validator-Service. Im Beispiel, wenn sowohl einmalige als auch wiederkehrende Zahlungen dieselbe Validierung benötigen, erstellen Sie einen gemeinsamen PaymentValidator, den beide durch Komposition verwenden können. Dies macht die gemeinsame Logik leichter auffindbar und testbar als Methoden, die in übergeordneten Klassen versteckt sind.

Werden Sie jetzt sicher.

Sichern Sie Ihren Code, Ihre Cloud und Ihre Laufzeit in einem zentralen System.
Finden und beheben Sie Schwachstellen schnell und automatisch.

Keine Kreditkarte erforderlich | Scan-Ergebnisse in 32 Sek.