Python-Kurs / Vererbung und Komposition für austauschbare Regeln vergleichen

Vererbung erweitert eine Klassenbeziehung und ihr Verhalten; Komposition verwendet andere Objekte als Bestandteile oder Mitarbeiter. Entscheiden Sie nach der ersetzbaren Beziehung und der gewünschten Änderungsrichtung, statt allein ähnlichen Code als Grund für eine Hierarchie zu verwenden.

Untertypbeziehung von verwendeter Zusammenarbeit unterscheiden

Eine Unterklasse sollte dort einsetzbar sein, wo die übergeordnete Schnittstelle erwartet wird, und deren wesentliche Zusagen erhalten. Ein Buchungsobjekt, das eine Gebührenregel benutzt, ist dagegen nicht selbst eine Art Gebührenregel. Eine gespeicherte und aufgerufene Regel beschreibt diese Zusammenarbeit oft direkter.

Vererbung und Komposition schließen sich nicht aus. Im ersten Beispiel erweitert FixedFee die Gebührenregel FeePolicy. Ledger speichert hingegen ein Regelobjekt als Attribut und delegiert an dessen apply-Methode. Die Hierarchie beschreibt damit Gebührenregeln, die Komposition beschreibt ihre Verwendung durch das Buchungsobjekt. Die Python-Einführung zur Vererbung erläutert Unterklassen und überschriebene Methoden.

super() ruft die nächste passende Implementierung in der Methodenauflösungsreihenfolge auf. In dieser einfachen Einzelvererbung ist das die gezeigte Basisklassenimplementierung. Lesen Sie es trotzdem nicht als dauerhaft fest codierten Namen genau einer Elternklasse; bei mehreren kooperierenden Basisklassen ist die Reihenfolge relevant. Die Referenz zu super erklärt diese Delegation.

Eine austauschbare Gebührenregel ausführen

Speichern Sie den folgenden vollständigen Code als composition.py in UTF-8 im Übungsordner. Führen Sie python composition.py mit dem Projektinterpreter aus; unter macOS oder Ubuntu kann außerhalb einer aktivierten Umgebung python3 passen. Zusatzpakete und externe Daten sind nicht erforderlich.

class FeePolicy:
    def apply(self, amount):
        return amount

class FixedFee(FeePolicy):
    def __init__(self, fee):
        self.fee = fee

    def apply(self, amount):
        return super().apply(amount) + self.fee

class Ledger:
    def __init__(self, policy):
        self.policy = policy

    def charge(self, amount):
        return self.policy.apply(amount)

print(Ledger(FeePolicy()).charge(8000))
print(Ledger(FixedFee(500)).charge(8000))

Erwartete Ausgabe:

8000
8500

Die Grundregel gibt den Betrag unverändert zurück. FixedFee(500) ruft zunächst diese Regel auf und ergänzt 500. Ledger kennt die Einzelheiten der Gebührenberechnung nicht: Es ruft lediglich self.policy.apply(amount) auf. Die Ergebnisse sind deshalb 8000 und 8500.

Ein anderes Objekt kann ebenfalls mit Ledger zusammenarbeiten, wenn es ein fachlich passendes apply bereitstellt. Die gemeinsame Basisklasse ist dafür in diesem dynamischen Beispiel keine zwingende Laufzeitvoraussetzung. Ein passender Methodenname allein verspricht aber noch keinen passenden Eingabe- und Ergebnisvertrag. Das Objekt muss die vereinbarte Bedeutung der Berechnung tatsächlich erfüllen.

Zwei Alternativen unter gleichen Bedingungen vergleichen

Das folgende zweite vollständige Beispiel isoliert dieselbe Aufgabe in zwei Varianten. Speichern Sie es als policies.py und starten Sie python policies.py im selben Übungsordner:

class InheritedLedger:
    def fee(self):
        return 0
    def charge(self, amount):
        return amount + self.fee()
class FixedLedger(InheritedLedger):
    def fee(self):
        return 500
class ComposedLedger:
    def __init__(self, fee_policy):
        self.fee_policy = fee_policy
    def charge(self, amount):
        return amount + self.fee_policy()
def fixed_fee():
    return 500
print(FixedLedger().charge(8000))
ledger = ComposedLedger(fixed_fee)
print(ledger.charge(8000))
ledger.fee_policy = lambda: 0
print(ledger.charge(8000))

Erwartete Ausgabe:

8500
8500
8000

FixedLedger überschreibt die geerbte Methode fee. Der geerbte Ablauf von charge verwendet dieses veränderte Verhalten. Die zusammengesetzte Variante erhält stattdessen eine aufrufbare Funktion fixed_fee und fragt sie nach dem Aufschlag. Beide berechnen unter denselben Bedingungen 8500.

Durch die spätere Zuweisung einer neuen parameterlosen Funktion mit Ergebnis 0 ändert sich nur die Regel des zusammengesetzten Objekts. Derselbe Aufruf ledger.charge(8000) ergibt danach 8000. lambda: 0 ist hier lediglich eine kurze Funktion ohne Argumente. Die Buchungsberechnung selbst bleibt unverändert.

Die zwei Beispiele verwenden unterschiedliche Schnittstellen für ihre Komponenten: Im ersten erwartet Ledger eine Methode apply(amount), im zweiten erwartet ComposedLedger eine parameterlose aufrufbare Gebührenfunktion. Vertauschen Sie diese nicht ohne Anpassung. Die Grundlagen von Instanzattributen und Methoden stehen in der Übung zu Klassen und Instanzen.

Verträge und fehlgeschlagene Zusammenarbeit prüfen

  1. Ändern Sie im ersten Beispiel nur FixedFee(500) zu FixedFee(0). Beide Ausgaben müssen nun 8000 sein. Stellen Sie 500 anschließend wieder her.
  2. Erzeugen Sie ein Buchungsobjekt mit einer Gebührenregel und ersetzen Sie dessen policy anschließend durch FeePolicy(). Der nächste Aufruf soll wieder den unveränderten Betrag liefern. Prüfen Sie den Wechsel bei gleichbleibendem Eingabebetrag.
  3. Übergeben Sie getrennt ein gewöhnliches Objekt ohne apply. Ein AttributeError beim Berechnungsaufruf zeigt die fehlende Schnittstelle. Eine apply-Methode mit unpassender Argumentliste kann stattdessen einen TypeError auslösen.
  4. Im zweiten Beispiel muss fee_policy aufrufbar sein. Ein direkt übergebener Wert 500 statt einer Funktion führt beim Aufruf zu TypeError. Übergeben Sie die Funktion selbst, nicht vorschnell ihr numerisches Ergebnis.
  5. Stellen Sie beide Originaldateien wieder her und vergleichen Sie jeweils sämtliche Ausgabezeilen.

Die gezeigten Klassen erzwingen noch keine vollständige Zahlen- oder Gebührenregel. Eine negative feste Gebühr kann das Ergebnis verkleinern; ob dies erlaubt ist, muss der Vertrag entscheiden. Testen Sie bei einer Unterklasse insbesondere, ob sie zulässige Eingaben der Basisklasse weiterhin akzeptiert und ob die Bedeutung des Ergebnisses erhalten bleibt.

Eine austauschbare Funktion kann für eine kleine Regel bereits genügen. Eine Klassenhierarchie ist eher dann hilfreich, wenn eine stabile Untertypbeziehung und mehrere zusammengehörige Operationen sie rechtfertigen. Vermeiden Sie die Vererbung umfangreicher fachfremder Funktionen nur zur Wiederverwendung einer einzelnen Methode; eine ausdrücklich übergebene Komponente kann Abhängigkeiten und Tests einfacher halten.