Python überzeugt mit schneller Erlernbarkeit und einem breiten Einsatzgebiet. Genau das sind auch die Gründe, weswegen Python gerne in wissenschaftlichen Fachgebieten wie Data Science oder Machine Learning eingesetzt wird.

Wir Pythonentwickler sind dafür bekannt schnell etwas zum Laufen zu bringen. Nichtsdestotrotz sind viele von uns auch dafür bekannt Code zu schreiben der nicht Ideal auf große Projekte skaliert und früher oder später schwer zu warten wird. Dieses Problem haben im Grunde genommen alle Programmiersprachen.

Zum Glück haben die Programmierer dieser Welt dafür eine Lösung geschaffen: Die sogenannten Software Design Patterns (dt. Entwurfsmuster). Das sind bewährte Lösungsschablonen für wiederkehrende Probleme in der Softwarearchitektur.

Eine Kategorie von Design Patterns sind Creational Design Pattern (dt. Erzeugungsmuster). Sie dienen der Erzeugung von Objekten. In diesem Beitrag wirst du die 4 meiner Meinung nach wichtigsten von ihnen kennen lernen.

In diesem Beitrag zeige ich diese 4 Design Patterns:

  • Builder Pattern
  • Factory Method Pattern
  • Prototype Pattern
  • Singleton Pattern

1. Builder Pattern

“When piecewise construction is complicated, provide an API for doing it succinctly.” - Dmitri Nesteruk

Manche Objekte kannst du mit einem einzigen Initialisierungsaufruf erstellen. Andere benötigen eine Vielzahl an Konfigurationen bevor sie einsetzbar sind. Du solltest keine Klassen schreiben die mehr als 3 Argumente im Constructor (also in der __init__() Funktion) haben. Das wäre nicht sehr übersichtlich.

Besser wäre es, wenn du für komplexe Objekte das Builder Pattern (dt. Erbauer) verwendest. Die Idee dabei ist, dass du das Objekt in mehreren Schritten konfigurierst. Das funktioniert über Funktionsaufrufe einer speziellen Builderklasse. Den Builder kannst du dir als eine Art API vorstellen die das gewünschte Objekt Stück für Stück erzeugt.

In diesem Beispiel siehst du einen Builder, der einer HTML Element Klasse hilft, weitere Elemente als Unterelemente zu verketten und anzuzeigen.

class HtmlElement:
    indent_size = 2

    def __init__(self, name="", text=""):
        self.name = name
        self.text = text
        self.elements = []

    def __makestring(self, indent):
        lines = []
        i = ' ' * (indent * self.indent_size)
        lines.append(f'{i}<{self.name}>')

        if self.text:
            i1 = ' ' * ((indent + 1) * self.indent_size)
            lines.append(f'{i1}{self.text}')

        for e in self.elements:
            lines.append(e.__makestring(indent + 1))

        lines.append(f'{i}</{self.name}>')
        return '\n'.join(lines)

    def __str__(self):
        return self.__makestring(0)

    @staticmethod
    def create(name):
        return HtmlBuilder(name)

class HtmlBuilder:
    __root = HtmlElement()

    def __init__(self, root_name):
        self.root_name = root_name
        self.__root.name = root_name

    def add_child(self, child_name, child_text):
        self.__root.elements.append(
            HtmlElement(child_name, child_text)
        )

    def clear(self):
        self.__root = HtmlElement(name=self.root_name)

    def __str__(self):
        return str(self.__root)

if __name__ == "__main__":
    htmlbuilder = HtmlElement.create("ol")
    htmlbuilder.add_child("li", "Kaffee")
    htmlbuilder.add_child("li", "Tee")
    htmlbuilder.add_child("li", "Milch")
    print(htmlbuilder)
<ol>
  <li>
    Kaffee
  </li>
  <li>
    Tee
  </li>
  <li>
    Milch
  </li>
</ol>

Hier wurde der Builder von der Klasse selbst aufgerufen. Das muss natürlich nicht immer so sein, genau so gut kannst du einen Builder schreiben, den du direkt aufruft.

2. Factory Method Pattern

“A component responsible solely for the wholesale (not piecewise) creation of objects.” - Dmitri Nesteruk

Der Gegenentwurf zum Builder Pattern ist das Factory Method Pattern (dt. Fabrikmethode). Du setzt es ein, wenn du vermutest, dass eine bestimmte Objektkonfiguration öfters vorkommen wird. Üblicherweise ist eine Fabrikmethode eine normale Methode die ein fertig konfiguriertes Objekt zurückliefert. Das kann auch mit einem Builder zusammen funktionieren.

Indem du mehrere Fabrikmethoden bereitstellst, kannst du das Objekt mit mehreren unterschiedlichen Konfigurationen erstellen, ohne einen komplizierten Konstruktor zu haben.

Mit folgender Klasse kannst du einen Punkt in einem Koordinatensystem beschreiben. Die zwei meistgenutzten Systeme sind die kartesischen Koordinaten und die Polarkoordinaten.

Wenn du versuchst beide Systeme mit dem Konstruktor abzudecken wirst du mit den Parameternamen durcheinander kommen: Beim kartesischen System benutzt man x für die x-Achse und y für die y-Achse. Beim Polarsystem benutzt man hingegen Rho für die Entfernung zur Mitte und Theta für den Winkel. Im __init__() musst du dich allerdings für eines der beiden Benennungssysteme entscheiden.

Die Fabrikmethode schafft Abhilfe, indem einfach eine neue statische Methode für jede der Varianten bereitgestellt wird die das neue Objekt erzeugt und zurückgibt.

from math import sin, cos

class Point:
    def __str__(self):
        return f"x: {self.x}, y: {self.y}"

    def __init__(self, a, b):
        self.x = a
        self.y = b

    @staticmethod
    def new_cartesian_point(x, y):
        return Point(x, y)

    @staticmethod
    def new_polar_point(rho, theta):
        return Point(rho * sin(theta), rho * cos(theta))

if __name__ == "__main__":
    p1 = Point.new_cartesian_point(1, 2)
    p2 = Point.new_polar_point(5, 7)
    print(p1)
    print(p2)
x: 1, y: 2
x: 3.2849329935939453, y: 3.769511271716523

Das Factory Method Pattern ist schnell umgesetzt. Aber vor allem mit wachsender Anzahl an Fabrikmethoden verstößt es gegen das Single Responsibility Principle.

Deswegen solltest du überlegen, ob du deine Fabrikmethoden auch in eine eigene Klasse schreiben kannst. Im Zweifelsfall kannst du einfach all deine Fabrikmethoden in einer Fabrikklasse bündeln.

import Point

class PointFactory:
    @staticmethod
    def new_cartesian_point(x, y):
        return Point(x, y)

    @staticmethod
    def new_polar_point(rho, theta):
        return Point(rho * sin(theta), rho * cos(theta))

if __name__ == "__main__":
    p = PointFactory.new_cartesian_point(1, 2)
    print(p)
x: 1, y: 2

3. Prototype Pattern

“A partially or fully initialized object that you copy (clone) and make use of.” - Dmitri Nesteruk

Das Prototype Pattern (dt. Prototyp) kannst du verwenden, um Objekte zu bauen die anderen, bereits existierenden Objekten ähneln. Das hat vor allem dann einen Vorteil, wenn du Objekte benutzt die bei der Erstellung sehr Rechen- oder Code intensiv sind.

In Python kannst du das umsetzen, indem du das gewünschte Quellobjekt mithilfe des copy Moduls kopierst.

import copy

class Address:
    def __init__(self, street_address, city, country):
        self.country = country
        self.city = city
        self.street_address = street_address

    def __str__(self):
        return f"{self.street_address}, {self.city}, {self.country}"

class Person:
    def __init__(self, name, address):
        self.name = name
        self.address = address

    def __str__(self):
        return f"{self.name} lives at {self.address}"

john = Person("John", Address("123 London Road", "London", "UK"))
print(john)
jane = copy.deepcopy(john)
jane.name = "Jane"
jane.address.street_address = "124 London Road"
print(john)
print(jane)
John lives at 123 London Road, London, UK
John lives at 123 London Road, London, UK
Jane lives at 124 London Road, London, UK

In der Praxis kannst du diese Technik einsetzen, um Objekte zu erstellen, schon bevor du die genauen Spezifikationen des Objekts kennst. Zum Beispiel kann ein Webserver den Prototyp einer Seite bauen während er auf die Anfrage wartet. Sobald dann die entsprechende Anfrage mit den genauen Spezifikationen kommt, macht er einfach eine Kopie des Prototyps, ändert die zuvor unbekannten Daten ab und liefert die geänderte Kopie aus. Das kannst du übrigens auch gut mit dem Factory Method Pattern kombinieren und eine Fabrik erstellen die einen fertigen Prototypen zurückliefert.

4. Singleton Pattern

“A component which is instantiated only once.” - Dmitri Nesteruk

Das Singleton Pattern ist sehr umstritten. Laut Erich Gamma bedeutet der Einsatz dieses Pattern, dass deine Architektur an irgendeiner Stelle stinkt. Aber trotzdem - oder gerade deswegen - ist es sinnvoll zumindest zu wissen wie das Singleton Pattern aussieht.

Wann wird das Singleton Pattern eingesetzt?

Für manche Klassen kann es Sinn ergeben, dass sie nur ein einziges Mal initialisiert werden. Das können zum einen Klassen sein, die du nicht mehrmals im System haben willst (Datenbanken, Objektfabriken etc.). Zum anderen können das Klassen sein, bei denen der Initialisierungsprozess teuer ist.

Das Singleton hat das Ziel sicherzustellen, dass die jeweilige Klasse nur ein einziges Mal initialisiert werden kann. Das kannst du zum Beispiel mit einem Python Decorator umsetzen.

def singleton(class_):
    instances = {}

    def get_instance(*args, **kwargs):
        if class_ not in instances:
            instances[class_] = class_(*args, **kwargs)
        return instances[class_]

    return get_instance

@singleton
class Database:
    def __init__(self):
        print("Loading database")

if __name__ == "__main__":
    d1 = Database()
    d2 = Database()
    print(d1 == d2)
    print(d1 is d2)
Loading database
True
True

Alternativ zur Implementation mithilfe des Decorators kannst du natürlich auch eine Metaklasse schreiben und Database davon erben lassen. Das Prinzip bleibt gleich.


Konnte ich helfen? Ich freue mich über einen Drink! 💙