2 Structural Design Patterns die du als Pythonentwickler kennen solltest – Nützliche Strukturmuster für Python: Façade & Flyweight

You are currently viewing 2 Structural Design Patterns die du als Pythonentwickler kennen solltest – Nützliche Strukturmuster für Python: Façade & Flyweight

Structural Design Patterns (dt. Strukturmuster) sind Design Patterns (dt. Entwurfsmuster), die das Design von Beziehungen zwischen Softwareteilen (wie Klassen, Funktionen oder Objekten) vereinfachen.

Anders als bei den Creational Pattern geht es dabei nicht darum Objekte zu erstellen. Sondern eher dazu Verknüpfungen zwischen bestehenden Strukturen zu beschreiben und zu vereinfachen.

In diesem Beitrag lernst du diese 2 Design Pattern:

  • Façade Pattern
  • Flyweight Pattern

Façade Pattern

“A Façade provides a simple, easy to understand user interface over a large and sophisticated body of code.”
– Dmitri Nesteruk

Bei einem Haus ist die Fassade der ansehnlich gestaltete Teil der Außenwand. Meistens bezieht sich der Begriff auf die Seite des Gebäudes, die der Straße zugewandt ist. Das Wort kommt aus dem Lateinischen facies und bedeutet Angesicht bzw. Gesicht.

Das Mauerwerk hinter der Fassade ist der eigentliche Zweck der Mauer. Die Fassade ist aber das, was die anderen Menschen sehen sollen.

Auch in der Programmierung gibt es Teile deines Codes, die so komplex sind, dass du sie anderen Entwicklern nicht als API bereitstellen willst. Da diese Teile allerdings oft unverzichtbar für die Funktion der Software sind, kannst du das Façade Pattern einsetzen. Dieses Pattern hilft dir, dieselbe Funktionalität in schön bereitzustellen.

Dabei versteckst du komplexen Code hinter einen einfachen Aufruf. Dieser Aufruf ist zum einen einfacher zu verstehen. Zum anderen ist er in der Regel auch einfacher zu schreiben da du durch ihn mehrere Aufrufe kombinieren kannst.

Andere Design Patterns basieren auf einer ähnlichen Idee. So könntest du zum Beispiel das Factory Method Pattern als eine spezielle Variante des Façade Pattern sehen. Mit dem Unterschied, dass die Factory Method immer ein Objekt erzeugt während das Façade Pattern einfach nur komplexen Code versteckt.

Für das Pythonbeispiel bleiben wir beim Hausbau: Ein Haus besteht grob aus einem Fundament, einer Mauer und einem Dach. Wenn du es programmierst, sieht es etwa wie folgt aus.

class Fundament:
    def __init__(self, farbe):
        print(f"Baue Fundament in {farbe} ...")
        self.farbe = farbe
    def __str__(self):
        return f"Fundament in {self.farbe}"

class Mauern:
    def __init__(self, farbe, fundament):
        print(f"Baue Mauern in {farbe} auf {fundament} ...")
        self.farbe = farbe
    def __str__(self):
        return f"Mauern in {self.farbe}"

class Dach:
    def __init__(self, farbe, mauern):
        print(f"Baue Dach in {farbe} auf {mauern} ...")
        self.farbe = farbe
    def __str__(self):
        return f"Dach in {self.farbe}"

class Haus: # Dies ist unsere Facade Pattern Klasse
    def __init__(self, fundamentfarbe, mauerfarbe, dachfarbe):
        self.f = Fundament(fundamentfarbe)
        self.m = Mauern(mauerfarbe,self.f)
        self.d = Dach(dachfarbe,self.m)
    def __str__(self):
        return f"Mein Haus besteht aus einem {self.f}, {self.m} und einem {self.d}!"

Für ein Haus benötigst du das Fundament, die Mauern und das Dach. Da du diese drei Klassen in der Regel immer zusammen benötigst, kannst du anderen Entwicklern etwas Schreibarbeit einsparen, indem du das Facade Pattern einsetzt. Die Fassade ist in diesem Fall die Klasse Haus.

if __name__ == "__main__":
    print("Variante ohne Fassade:")
    print("==================")
    f = Fundament("Grau")
    m = Mauern("Gelb",f)
    d = Dach("Rot",m)
    print(f"Mein Haus besteht aus einem {f}, {m} und einem {d}!") 
    print("==================")
    print("")
    print("==================")
    print("Variante mit Fassade:")
    print("==================")
    h = Haus("Grau", "Gelb", "Rot")
    print(h)
    print("==================")
Variante ohne Fassade:
==================
Baue Fundament in Grau ...
Baue Mauern in Gelb auf Fundament in Grau ...
Baue Dach in Rot auf Mauern in Gelb ...
Mein Haus besteht aus einem Fundament in Grau, Mauern in Gelb und einem Dach in Rot!
==================

==================
Variante mit Fassade:
==================
Baue Fundament in Grau ...
Baue Mauern in Gelb auf Fundament in Grau ...
Baue Dach in Rot auf Mauern in Gelb ...
Mein Haus besteht aus einem Fundament in Grau, Mauern in Gelb und einem Dach in Rot!
==================

Du kannst schnell erkennen, dass beide Varianten dasselbe Ergebnis produzieren. Die Variante mit der Fassade ist allerdings deutlich verständlicher: Jeder weiß was ein Haus ist. Aber nicht jeder weiß aus welchen Teilen ein Haus zusammengesetzt wird.

Darüber hinaus wird dein Code deutlich schlanker so fern du die Fassade nutzt. In diesem Fall brauchst du nur 2 anstelle von 4 Zeilen Code.

Flyweight Pattern

Normalerweise skaliert der Speicherplatz linear mit der Größe der Liste. Je größer eine Liste, desto höher ist der Speicherverbrauch.

Das muss nicht unbedingt so sein!

Das Flyweight Pattern kannst du einsetzen, um bei besonders großen Datenmengen Speicherplatz zu sparen. Die Idee dieses Pattern ist:

  • ähnliche Daten zusammenzufassen
  • diese extern abzuspeichern
  • die Einträge entsprechend zu verlinken

Das Prinzip kannst du ganz einfach anhand einer Telefonbuchdatenbank verstehen. Sie enthält alle Telefonnummern und speichert zu jeder Telefonnummer einen Namen.

Der Trick ist: Namen sind keine zufälligen Strings. Manche Namen kommen in Deutschland sehr häufig vor. So gab es im Jahr 1996 über 320.000 Telefonbucheinträge mit dem Namen "Müller" in Deutschland. "Schmidt" gab es 235.000 Mal.

Damit du einen Namen im ISO 8859-1 (Latin-1) Format speichern kannst benötigst du mindestens ein Byte pro Buchstaben. Bei Schmidt wären es also schon 7 Byte. Zeichenkodierungsstandards die auch asiatische Namen speichern können benötigen oft noch mehr Speicherplatz pro Buchstaben.

Das Flyweight Pattern lehrt dir, die Namen in einer eigenen Tabelle zu speichern und anschließend von deiner Telefonbuchadresse auf diese Tabelle zu verlinken. Verlinkungen sind nämlich deutlich Speicher freundlicher als Kopien:

print(sys.getsizeof("Müller"))
print(sys.getsizeof(1))
79
28

Im Folgenden siehst du nun zweimal dieselbe Implementation eines Telefonbuchs. Einmal mit und einmal ohne Flyweight Pattern.

Ohne Flyweight

from typing import NamedTuple

class Eintrag_alt(NamedTuple):
    name: str
    telefonnummer: str

class Telefonbuch_alt(list):
    def __str__(self):
        output = ""
        for eintrag in self:
            output += (
                f"{eintrag.name} hat die Nummer {eintrag.telefonnummer}\n"
            )
        return output

telefonbuch_alt = Telefonbuch_alt()
telefonbuch_alt.append(Eintrag_alt("Müller", "555 001"))
telefonbuch_alt.append(Eintrag_alt("Müller", "555 002"))
telefonbuch_alt.append(Eintrag_alt("Müller", "555 003"))
telefonbuch_alt.append(Eintrag_alt("Schmidt", "555 004"))
telefonbuch_alt.append(Eintrag_alt("Schmidt", "555 005"))
telefonbuch_alt.append(Eintrag_alt("Schneider", "555 006"))
telefonbuch_alt.append(Eintrag_alt("Fischer", "555 007"))

print(telefonbuch_alt)
Müller hat die Nummer 555 001
Müller hat die Nummer 555 002
Müller hat die Nummer 555 003
Schmidt hat die Nummer 555 004
Schmidt hat die Nummer 555 005
Schneider hat die Nummer 555 006
Fischer hat die Nummer 555 007

Mit Flyweight

class Eintrag_neu(NamedTuple):
    nameId: int
    telefonnummer: str

class Telefonbuch_neu(list):
    nachnamen = {0: "Müller", 1: "Schmidt", 2: "Schneider", 3: "Fischer"}

    def __str__(self):
        output = ""
        for eintrag in self:
            output += f"{self.nachnamen[eintrag.nameId]} hat die Nummer {eintrag.telefonnummer}\n"
        return output

telefonbuch_neu = Telefonbuch_neu()
telefonbuch_neu.append(Eintrag_neu(0, "555 001"))
telefonbuch_neu.append(Eintrag_neu(0, "555 002"))
telefonbuch_neu.append(Eintrag_neu(0, "555 003"))
telefonbuch_neu.append(Eintrag_neu(1, "555 004"))
telefonbuch_neu.append(Eintrag_neu(1, "555 005"))
telefonbuch_neu.append(Eintrag_neu(2, "555 006"))
telefonbuch_neu.append(Eintrag_neu(3, "555 007"))
Müller hat die Nummer 555 001
Müller hat die Nummer 555 002
Müller hat die Nummer 555 003
Schmidt hat die Nummer 555 004
Schmidt hat die Nummer 555 005
Schneider hat die Nummer 555 006
Fischer hat die Nummer 555 007

Ein ähnliches Prinzip verwenden übrigens auch Kompressionsalgorithmen.

Schreibe einen Kommentar