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 die Creational Pattern vom letzten Beitrag geht es dabei nicht darum Objekte zu erstellen. Sondern eher dazu Verknüpfungen zwischen bestehenden Strukturen zu beschreiben und zu vereinfachen.
In diesem Beitrag zeige ich diese 2 Design Patterns:
- Adapter Pattern
- Bridge Pattern
Adapter Pattern
“A Construct which adapts an existing interface X to conform the required interface Y” - Dmitri Nesteruk
Das Adapter Pattern brauchst du, wenn ein bestehendes, unveränderbares Interface oder eine API nicht genau zu deinen speziellen Anforderungen passt.
Der Adapter ist ein Stück Software das den Output des Interfaces/der API so verändert, dass er zu deinem Projekt passt.
Du kannst dir das wie bei Reiseadaptern vorstellen. Das sind diese Plastikaufsätze, die du auf Reisen in andere Länder zwischen der Steckdose und dem Stecker deines Föhns steckst. Sie bewirken dass die Stecker zu den Steckdosen passen.
Aber auch die elektrische Spannung ist wichtig. Während wir in Europa meist 230 Volt verwenden, sind es in Amerika meist nur 110 Volt. Damit kommen viele unserer Geräte nicht zurecht. Dieses Problem wird durch einen aktiven Adapter gelöst. Dieser macht nicht nur die Anschlüsse passend, sondern wandelt auch die Spannung um.
Genauso funktioniert auch das Adapter Pattern in der Softwareentwicklung. Nehmen wir an du bist in Amerika und dein Hotelzimmer hat eine amerikanische Steckdose.
class AmerikanischeSteckdose:
def __init__(self):
print("Nutze amerikanische Steckdose mit 110 Volt und 20 Ampere.")
def spannung(self):
return 110 # Volt
def maximaler_strom(self):
return 20 # Ampere
Dein Föhn braucht aber 230 Volt in der Stromquelle um zu funktionieren.
class Foehn:
benoetigte_spannung = 230 # Volt
benoetigte_leistung = 1800 # Watt
def __init__(self, stromquelle):
self.__stromquelle = stromquelle
self.benoetigter_strom = (
self.benoetigte_leistung / self.benoetigte_spannung
)
def _check_leistung(self):
maximal_moeglicher_strom = self.__stromquelle.maximaler_strom()
if maximal_moeglicher_strom >= self.benoetigter_strom:
return True
return False
def _check_spannung(self):
if self.__stromquelle.spannung() >= self.benoetigte_spannung:
return True
return False
def einschalten(self):
if self._check_leistung() and self._check_spannung():
print(
f"Funktioniert ! \nNutze \t\t{self.__stromquelle.spannung()} V "
f"und {self.benoetigter_strom} A"
)
else:
print(
"Funktioniert nicht !\nBenoetige mindestens \t"
f"{self.benoetigte_spannung} V und "
f"{self.benoetigter_strom} A\n"
"Bekomme nur \t\t"
f"{self.__stromquelle.spannung()} V und "
f"{self.__stromquelle.maximaler_strom()} A"
)
if __name__ == "__main__":
foehn = Foehn(AmerikanischeSteckdose())
foehn.einschalten()
Nutze amerikanische Steckdose mit 110 Volt und 20 Ampere.
Funktioniert nicht !
Benoetige mindestens 230 V und 7.826086956521739 A
Bekomme nur 110 V und 20 A
Dieses Problem löst du mit einem Adapter der die Spannung in 230 Volt umwandelt. Dabei wird natürlich der Strom reduziert. An dieser Steckdose ist nichtsdestotrotz genug Stromstärke vorhanden!
class Adapter:
def __init__(self, stromquelle):
print("Nutze Adapter für Umwandlung auf 230 Volt.")
self.__stromquelle = stromquelle
def spannung(self):
return 230 # Volt
def maximaler_strom(self):
faktor = self.__stromquelle.spannung() / self.spannung()
I = faktor * self.__stromquelle.maximaler_strom()
return I # Ampere
if __name__ == "__main__":
foehn = Foehn(Adapter(AmerikanischeSteckdose()))
foehn.einschalten()
Nutze amerikanische Steckdose mit 110 Volt und 20 Ampere.
Nutze Adapter für Umwandlung auf 230 Volt.
Funktioniert !
Nutze 230 V und 7.826086956521739 A
Eigentlich ganz einfach! Beim Hotel steckst du den Adapter zwischen Steckdose und Gerät. Bei Python steckst du den Adapter zwischen Client und API.
Bridge Pattern
“A mechanism that decouples an interface (hierarchy) from an implementation (hierarchy).” - Dmitri Nesteruk
Mit dem Bridge Pattern kannst du ein Konzept in zwei unabhängige Klassenhierarchien trennen: die Abstraktion und die Implementation. Das brauchst du oft, wenn ein Prozess auf unterschiedliche Weisen durchführbar ist.
Gucken wir uns als Beispiel das Speichern von geometrischen Formen auf der Festplatte an.
Mögliche Formen wären:
- Dreiecke
- Rechtecke
- Kreise
- Achtecke
Und dein Auftrag ist diese Formen auf unterschiedliche Weisen zu speichern. Zum Beispiel als:
- BMP
- JPG
- SVG
Falls es nur wenige Grafiken und Speicherformate gibt, kannst du versuchen für jedes Speicherformat und für jede Form eine Klasse zu schreiben.
Übel wird es, wenn es viele gibt:
class DreieckBMP:
def __init__(self): pass
def anzeigen(self): pass
def speichern(self): pass
class RechteckBMP:
def __init__(self): pass
def anzeigen(self): pass
def speichern(self): pass
class AchteckBMP:
def __init__(self): pass
def anzeigen(self): pass
def speichern(self): pass
class KreisBMP:
def __init__(self): pass
def anzeigen(self): pass
def speichern(self): pass
class DreieckJPG:
def __init__(self): pass
def anzeigen(self): pass
def speichern(self): pass
class RechteckJPG:
def __init__(self): pass
def anzeigen(self): pass
def speichern(self): pass
class KreisJPG:
def __init__(self): pass
def anzeigen(self): pass
def speichern(self): pass
class AchteckJPG:
def __init__(self): pass
def anzeigen(self): pass
def speichern(self): pass
class DreieckSVG:
def __init__(self): pass
def anzeigen(self): pass
def speichern(self): pass
class RechteckSVG:
def __init__(self): pass
def anzeigen(self): pass
def speichern(self): pass
class KreisSVG:
def __init__(self): pass
def anzeigen(self): pass
def speichern(self): pass
class AchteckSVG:
def __init__(self): pass
def anzeigen(self): pass
def speichern(self): pass
Dass es nicht lustig wird diesen Code zu Pflegen wirst du spätestens merken, wenn du noch mehr Formen oder Speicherformate hinzufügen musst. Dann musst du nämlich für nur eine Speicherart, eine zusätzliche Klasse für jede Form schreiben.
Das nennt man eine Cartesian Product Complexity Explosion. Also ist ein Problem, das die Codebasis exponentiell wachsen lässt, wenn die Funktionalität der Software wächst.
Ein eleganterer Ansatz ist das Bridge Pattern.
Bridge Pattern als Klassendiagramm
Die Idee ist Oberklassen (Abstraktion und Implementation) zu bilden die du dann miteinander verknüpfst. Zum Beispiel die Oberklassen Form (Abstraktion) für Kreis, Dreieck, Rechteck und Achteck (Spezielle Abstraktionen) und Serializer (Implementation) für BMP, SVG und JPG (Konkrete Implementation).
Die Abstraktion hat eine Implementation. Diese musst du zuerst erzeugen und sie dann bei der Erstellung der speziellen Abstraktion an diese weitergeben.
class Serializer:
def __init__(self): pass
def speichern(self): pass
class JPGSerializer(Serializer):
def __init__(self): pass
def speichern(self):
print("als JPG gespeichert!")
class BMPSerializer(Serializer):
def __init__(self): pass
def speichern(self):
print("als BMP gespeichert!")
class SVGSerializer(Serializer):
def __init__(self): pass
def speichern(self):
print("als SVG gespeichert!")
class Form:
def __init__(self, serializer):
self.serializer = serializer
def speichern(self): pass
def anzeigen(self): pass
class Kreis(Form):
def __init__(self, serializer):
print("Kreis erstellen")
super().__init__(serializer)
def speichern(self):
self.serializer.speichern()
def anzeigen(self):
print("Kreis ausgeben")
class Dreieck(Form):
def __init__(self, serializer):
print("Dreieck erstellen")
super().__init__(serializer)
def speichern(self):
self.serializer.speichern()
def anzeigen(self):
print("Dreieck ausgeben")
class Rechteck(Form):
def __init__(self, serializer):
print("Rechteck erstellen")
super().__init__(serializer)
def speichern(self):
self.serializer.speichern()
def anzeigen(self):
print("Rechteck ausgeben")
class Achteck(Form):
def __init__(self, serializer):
print("Achteck erstellen")
super().__init__(serializer)
def speichern(self):
self.serializer.speichern()
def anzeigen(self):
print("Achteck ausgeben")
So wurden aus 12 Klassen und 36 Methoden nur noch 9 Klassen und 23 Methoden. Das Ganze wird noch sichtbarer, wenn der Code noch weiter wächst.
if __name__ == '__main__':
JPG = JPGSerializer()
SVG = SVGSerializer()
kreis = Kreis(JPG)
kreis.anzeigen()
kreis.speichern()
rechteck = Rechteck(SVG)
rechteck.anzeigen()
rechteck.speichern()
Kreis erstellen
Kreis ausgeben
als JPG gespeichert!
Rechteck erstellen
Rechteck ausgeben
als SVG gespeichert!
Diese Idee stellt zum einen sicher, dass das Single Responsibility Prinzip (SRP) eingehalten wird. Zum anderen verhindert es eine Cartesian Product Complexity Explosion.
Konnte ich helfen? Ich freue mich über einen Drink!
💙