Warum Muster
Kurz gesagt: Ein Entwurfsmuster ist eine benannte, bewährte Lösung für ein wiederkehrendes Problem im Code; die klassischen 23 Muster gehören zu drei Familien: erzeugende, strukturelle und Verhaltensmuster.
Problem
Zwei Absätze für ein einziges Wort
Ein Code-Review in einem Online-Shop. Jonas liest die Kasse und hinterlässt einen Kommentar an der markierten Zeile:
class Checkout: RULES = { "Karte": lambda amount: amount, "Nachnahme": lambda amount: amount + 2.50, "Gutschein": lambda amount: max(amount - 10, 0), }
def pay(self, method, amount): return self.RULES[method](amount)checkout = Checkout()print(f"{checkout.pay("Nachnahme", 80):.2f} €")print(f"{checkout.pay("Gutschein", 80):.2f} €")82.50 €70.00 €Jonas: Ich verstehe nicht, was das ist. Die Kasse hält eine Tabelle, in der jede Zahlungsart eine eigene Regel ist. Alle Regeln haben dieselbe Form: Sie nehmen einen Betrag und geben einen Betrag zurück. Während das Programm läuft, nimmt die Kasse die Regel unter dem Namen, den der Kunde gewählt hat, und ruft sie auf, ohne zu wissen, welche es ist. Ist das Absicht? Wenn morgen die Ratenzahlung kommt, muss ich dann
payanfassen?Maria: Das ist Absicht. Du fasst
paynicht an, du fügst eine Zeile in die Tabelle ein. Die Regel rechnet nur und merkt sich nichts, deshalb kann die Kasse sie mehrmals fragen: für die Vorschau, für die Rechnung, für die Zahlung. Sonst würde jede Frage einen anderen Betrag ergeben…
Zwei Leute, zwei Absätze, und das Wichtigste ist immer noch nicht gesagt: Was passiert, wenn der Name nicht in der Tabelle steht?
Maria hätte ein einziges Wort schreiben können: Strategy. Jonas hätte gewusst, was er vor sich hat und was er prüfen muss. Die Regel rechnet nur. Eine neue Zahlungsart ist eine neue Zeile, und die Kasse wird nicht angefasst. Ein unbekannter Name wird abgefangen, bevor er die Tabelle erreicht. Viele Leute haben diese Fragen schon gelöst, und sie sind unter diesem Namen festgehalten.
Die Idee
Ein Muster ist der Name einer Lösung
Ein Entwurfsmuster ist eine bewährte Lösung für ein Problem, das in vielen Projekten auftaucht, mit einem Namen, den alle kennen. Es ist keine Bibliothek und kein Code zum Kopieren: Jedes Projekt schreibt es auf seine Weise. Ein Muster trägt vier Dinge:
- einen Namen: ein Wort statt zwei Absätzen;
- das Problem, das es löst: zum Beispiel „die Regel ändert sich, und die Kasse darf nicht angefasst werden“;
- die Rollen: wer was hält und wer wen aufruft;
- die Kosten: was du dafür zahlst (mehr Funktionen oder Klassen, ein Sprung mehr beim Lesen) und wann es sich nicht lohnt.
Die meisten Namen stammen aus dem Buch „Design Patterns“ (1994) von vier Autoren, die Entwickler die „Gang of Four“ nennen. Es beschreibt 23 Muster in drei Familien. Das Modul geht die am häufigsten benutzten durch:
- Erzeugende Muster: wie und wo Objekte entstehen. Factory Method und Simple Factory, Builder, Singleton und Dependency Injection.
- Strukturelle Muster: wie Objekte zu etwas Größerem zusammengesetzt werden. Adapter und Facade, Decorator, Composite.
- Verhaltensmuster: wer wen aufruft und wer was entscheidet. Strategy, Observer, Command, State, Template Method und Iterator.
Repository und Schichten stammt nicht aus dem Buch. Es ist ein Architekturmuster: wo im Programm die Datenbank lebt.
Das Modul endet mit einer Übung, Der Shop: welches Muster wo: ein unordentlicher Shop, den du selbst aufräumst.
In Python verschwinden manche Muster fast, weil die Sprache sie schon hat. Eine Funktion ist ein Wert, deshalb ist Strategy oder Command oft nur eine Funktion. Das Durchlaufen einer Collection (Iterator) steckt infor. Jede Lektion im Modul sagt, wo das so ist, statt Java mit anderer Syntax zu schreiben.Das Zeichen @ über einer Funktion heißt in Python Dekorator. Es hängt mit dem Muster Decorator zusammen, ist aber nicht dasselbe; die Lektion zu Decorator zeigt beides.Sieh es dir an
Die Falle: ein Muster um des Musters willen
Die Aufgabe ist eine einzige: 10 % Rabatt für Bestellungen über 100 €. Jemand hat gerade etwas über Muster gelesen:
from abc import ABC, abstractmethod
class DiscountStrategy(ABC): @abstractmethod def apply(self, amount): pass
class PercentOverThresholdDiscountStrategy(DiscountStrategy): def __init__(self, percent, threshold): self.percent = percent self.threshold = threshold
def apply(self, amount): if amount > self.threshold: return amount * (1 - self.percent / 100) return amount
class AbstractDiscountStrategyFactory(ABC): @abstractmethod def create_strategy(self): pass
class DefaultDiscountStrategyFactory(AbstractDiscountStrategyFactory): def create_strategy(self): return PercentOverThresholdDiscountStrategy(10, 100)
class DiscountService: def __init__(self, factory): self.factory = factory
def discounted(self, amount): return self.factory.create_strategy().apply(amount)service = DiscountService(DefaultDiscountStrategyFactory())print(f"Summe: {service.discounted(120):.2f} €")print(f"Summe: {service.discounted(80):.2f} €")Summe: 108.00 €Summe: 80.00 €Fünf Klassen, zwei Muster (Strategy und eine Fabrik) und ein Name wie AbstractDiscountStrategyFactory. Dieselbe Aufgabe ohne Muster:
def discounted(amount): return amount * 0.9 if amount > 100 else amountprint(f"Summe: {discounted(120):.2f} €")print(f"Summe: {discounted(80):.2f} €")Summe: 108.00 €Summe: 80.00 €Die Ausgabe ist gleich. Der Unterschied liegt im Lesen: Um herauszufinden, wie groß der Rabatt ist, gehst du in der ersten Version durch fünf Klassen, in der zweiten liest du eine Zeile.
Die Muster in der ersten Version lösen überhaupt kein Problem. Es gibt einen einzigen Rabatt, niemand ändert ihn, während das Programm läuft, und niemand wählt zwischen Rabatten. Wenn der zweite Rabatt kommt und der Kunde wählen muss, wird aus der Funktion eine Strategy. Nicht früher.
In Python ist das noch klarer: Eine abstrakte Klasse mitABC für eine einzeilige Regel ist Java, in Python geschrieben. Eine Funktion ist schon ein Wert, den du weiterreichen und aufbewahren kannst.Probier es aus
Welche Familie, und braucht es ein Muster
Die Idee
Wann ein Muster schadet
Ein Muster ist die Antwort auf ein Problem. Ohne das Problem bleiben nur die Kosten:
- „Wir könnten es brauchen.“ Eine Klasse mit einer einzigen Methode, wo eine Funktion genügt, eine Fabrik für eine einzige Klasse, eine Tabelle mit einer einzigen Regel. Alle, die den Code lesen, zahlen dafür, und niemand benutzt es.
- Der Name nennt das Muster, nicht die Aufgabe.
AbstractDiscountStrategyFactorysagt, wie es gebaut ist,discountedsagt, was es tut. Wenn du die Klasse nicht ohne die Wörter des Musters beschreiben kannst, brauchst du sie wahrscheinlich nicht. - Ein Muster aus einer anderen Sprache. Ein Java-Muster, Klasse für Klasse abgeschrieben: eine abstrakte Klasse, eine Fabrik und eine Klasse mit einer einzigen Methode.
- Das Muster vor dem Schmerz. Die gute Reihenfolge ist die umgekehrte. Du schreibst das Einfache. Wenn derselbe
switchan einer dritten Stelle auftaucht oder die Regeln fünf werden, baust du per Refactoring zum Muster um. Dann weißt du genau, was sich ändert.
Muster lernst du, um sie im Code anderer und in Reviews zu erkennen und zu benennen. Nicht, um sie überall einzubauen.
Übung
Erkenne das Muster
Löse das in deinem Editor.
Drei Ausschnitte aus einem Shop. Führe sie aus, und sage zu jedem: Gibt es ein Muster, welches ist es (Strategy, Observer, Factory Method, Singleton oder Adapter aus der Lektion „Entwurfsmuster“ im Modul Modernes Python), und in einem Satz: Welches Problem löst es. Einer der drei braucht kein Muster.
A.
class Order: def __init__(self): self.listeners = []
def on_shipped(self, listener): self.listeners.append(listener)
def ship(self, order_id): for listener in self.listeners: listener(order_id)order = Order()order.on_shipped(lambda order_id: print(f"E-Mail: Bestellung {order_id} ist versandt"))order.on_shipped(lambda order_id: print(f"Lager: {order_id} aus der Warteschlange nehmen"))order.ship("1042")E-Mail: Bestellung 1042 ist versandtLager: 1042 aus der Warteschlange nehmenB.
class CourierApi: def send_parcel(self, city, grams): return f"Paket für {city}, {grams} g"
class CourierShipping: def __init__(self): self.api = CourierApi()
def ship(self, city, kilograms): return self.api.send_parcel(city, round(kilograms * 1000))shipping = CourierShipping()print(shipping.ship("Köln", 1.25))Paket für Köln, 1250 gC.
def price_label(price, with_vat): if with_vat: return f"{price * 1.20:.2f} € inkl. MwSt." return f"{price:.2f} € ohne MwSt."print(price_label(25, True))print(price_label(25, False))30.00 € inkl. MwSt.25.00 € ohne MwSt.Die Antworten
- A ist Observer. Die Bestellung weiß nicht, wer ihr zuhört. Die E-Mail und das Lager abonnieren, und
shipruft sie in der Reihenfolge auf, in der sie sich angemeldet haben. Ein neuer Listener fasstOrdernicht an. - B ist Adapter.
CourierShippingübersetzt deinship(Kilogramm) in das fremdesend_parcel(Gramm), ohne eines von beiden anzufassen. - C braucht kein Muster. Zwei Varianten, an einer Stelle, niemand ändert sie. Strategy wäre hier zwei Funktionen und eine Tabelle statt eines einzigen
if.
Weitere Aufgaben
- Wann C ein Muster braucht. Der Shop beginnt, in fünf Ländern mit unterschiedlicher Mehrwertsteuer zu verkaufen, und der Kunde wählt das Land. Was ändert sich? Schreibe auf, welches Muster helfen würde und warum, ohne Code.
- Das Muster, das verschwindet. Nimm die fünf Klassen aus der Karte „Die Falle“ und ersetze sie durch eine einzige Funktion. Zähle die Zeilen davor und danach. Die Ausgabe muss gleich bleiben.
- Dein Code. Suche in einem deiner Projekte einen
switchoder eine Kette vonifs, die sich an zwei Stellen wiederholt. Mit welchem Wort würdest du ihn in einem Review benennen?
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Listen: append, remove und sort · dict und set · Lambdas · Entwurfsmuster
- Aufgaben zum Thema: Entwurfsmuster