Das Factory-Methodenmuster und seine Implementierung in Python

Das Factory-Methodenmuster und seine Implementierung in Python

In diesem Artikel werden das Entwurfsmuster der Factory-Methode und seine Implementierung in Python erläutert. Designmuster wurden Ende der 90er Jahre zu einem beliebten Thema, nachdem die sogenannte Viererbande (GoF: Gamma, Helm, Johson und Vlissides) ihr BuchDesign Patterns: Elements of Reusable Object-Oriented Software veröffentlicht hatte.

Das Buch beschreibt Entwurfsmuster als zentrale Entwurfslösung für wiederkehrende Probleme in der Software und klassifiziert jedes Entwurfsmuster entsprechend der Art des Problems incategories. Jedes Muster erhält einen Namen, eine Problembeschreibung, eine Entwurfslösung und eine Erklärung der Konsequenzen seiner Verwendung.

Das GoF-Buch beschreibt die Factory-Methode als kreatives Entwurfsmuster. Erstellungsentwurfsmuster beziehen sich auf die Erstellung von Objekten, und die Factory-Methode ist ein Entwurfsmuster, mit dem Objekte mit einer gemeinsamen Schnittstelle erstellt werden.

Dies ist ein wiederkehrendes Problem, dasmakes Factory Method one of the most widely used design patternsist, und es ist sehr wichtig, es zu verstehen und zu wissen, wie es angewendet wird.

By the end of this article, you will:

  • Verstehen Sie die Komponenten der Factory-Methode

  • Erkennen Sie Möglichkeiten zur Verwendung der Factory-Methode in Ihren Anwendungen

  • Erfahren Sie, wie Sie vorhandenen Code ändern und sein Design mithilfe des Musters verbessern

  • Erfahren Sie, wie Sie Möglichkeiten identifizieren, bei denen die Factory-Methode das geeignete Entwurfsmuster ist

  • Wählen Sie eine geeignete Implementierung der Factory-Methode

  • Wissen, wie eine wiederverwendbare Allzwecklösung der Factory-Methode implementiert wird

Free Bonus:5 Thoughts On Python Mastery, ein kostenloser Kurs für Python-Entwickler, der Ihnen die Roadmap und die Denkweise zeigt, die Sie benötigen, um Ihre Python-Fähigkeiten auf die nächste Stufe zu bringen.

Einführung in die Factory-Methode

Die Factory-Methode ist ein kreatives Entwurfsmuster, mit dem konkrete Implementierungen einer gemeinsamen Schnittstelle erstellt werden.

Es trennt den Prozess des Erstellens eines Objekts vom Code, der von der Schnittstelle des Objekts abhängt.

Beispielsweise benötigt eine Anwendung ein Objekt mit einer bestimmten Schnittstelle, um ihre Aufgaben auszuführen. Die konkrete Implementierung der Schnittstelle wird durch einige Parameter identifiziert.

Anstatt die bedingte Struktur eines komplexenif/elif/elsezur Bestimmung der konkreten Implementierung zu verwenden, delegiert die Anwendung diese Entscheidung an eine separate Komponente, die das konkrete Objekt erstellt. Mit diesem Ansatz wird der Anwendungscode vereinfacht, wodurch er wiederverwendbarer und einfacher zu warten ist.

Stellen Sie sich eine Anwendung vor, die einSong-Objekt in einem bestimmten Format in seinestring-Darstellung konvertieren muss. Das Konvertieren eines Objekts in eine andere Darstellung wird häufig als Serialisierung bezeichnet. Diese Anforderungen werden häufig in einer einzelnen Funktion oder Methode implementiert, die die gesamte Logik und Implementierung enthält, wie im folgenden Code:

# In serializer_demo.py

import json
import xml.etree.ElementTree as et

class Song:
    def __init__(self, song_id, title, artist):
        self.song_id = song_id
        self.title = title
        self.artist = artist


class SongSerializer:
    def serialize(self, song, format):
        if format == 'JSON':
            song_info = {
                'id': song.song_id,
                'title': song.title,
                'artist': song.artist
            }
            return json.dumps(song_info)
        elif format == 'XML':
            song_info = et.Element('song', attrib={'id': song.song_id})
            title = et.SubElement(song_info, 'title')
            title.text = song.title
            artist = et.SubElement(song_info, 'artist')
            artist.text = song.artist
            return et.tostring(song_info, encoding='unicode')
        else:
            raise ValueError(format)

Im obigen Beispiel haben Sie eine einfacheSong-Klasse, um ein Lied darzustellen, und eineSongSerializer-Klasse, die einsong-Objekt gemäß dem Wert von in seinestring-Darstellung konvertieren kann derformat Parameter.

Die Methode.serialize() unterstützt zwei verschiedene Formate:JSON undXML. Alle anderen angegebenenformatwerden nicht unterstützt, daher wird eineValueError-Ausnahme ausgelöst.

Verwenden Sie die interaktive Python-Shell, um zu sehen, wie der Code funktioniert:

>>>

>>> import serializer_demo as sd
>>> song = sd.Song('1', 'Water of Love', 'Dire Straits')
>>> serializer = sd.SongSerializer()

>>> serializer.serialize(song, 'JSON')
'{"id": "1", "title": "Water of Love", "artist": "Dire Straits"}'

>>> serializer.serialize(song, 'XML')
'Water of LoveDire Straits'

>>> serializer.serialize(song, 'YAML')
Traceback (most recent call last):
  File "", line 1, in 
  File "./serializer_demo.py", line 30, in serialize
    raise ValueError(format)
ValueError: YAML

Sie erstellen einsong-Objekt und einserializer-Objekt und konvertieren das Lied mithilfe der.serialize()-Methode in seine Zeichenfolgendarstellung. Die Methode verwendet das Objektsongals Parameter sowie einen Zeichenfolgenwert, der das gewünschte Format darstellt. Der letzte Aufruf verwendetYAML als Format, das vonserializer nicht unterstützt wird, sodass eineValueError-Ausnahme ausgelöst wird.

Dieses Beispiel ist kurz und vereinfacht, aber es ist immer noch sehr komplex. Abhängig vom Wert des Parametersformat gibt es drei logische Pfade oder Ausführungspfade. Dies scheint keine große Sache zu sein, und Sie haben wahrscheinlich Code mit größerer Komplexität gesehen, aber das obige Beispiel ist immer noch ziemlich schwer zu pflegen.

Die Probleme mit komplexem Bedingungscode

Das obige Beispiel zeigt alle Probleme, die bei komplexem logischem Code auftreten. Komplexer logischer Code verwendet die Strukturen vonif/elif/else, um das Verhalten einer Anwendung zu ändern. Die Verwendung der bedingten Strukturen vonif/elif/elseerschwert das Lesen, das Verstehen und das Verwalten des Codes.

Der obige Code scheint nicht schwer zu lesen oder zu verstehen, aber warten Sie, bis Sie den endgültigen Code in diesem Abschnitt sehen!

Trotzdem ist der obige Code schwer zu pflegen, weil er zu viel bewirkt. Diesingle responsibility principle geben an, dass ein Modul, eine Klasse oder sogar eine Methode eine einzige, genau definierte Verantwortung haben sollte. Es sollte nur eine Sache tun und nur einen Grund haben, sich zu ändern.

Die.serialize()-Methode inSongSerializer erfordert aus vielen verschiedenen Gründen Änderungen. Dies erhöht das Risiko, neue Fehler einzuführen oder vorhandene Funktionen zu beschädigen, wenn Änderungen vorgenommen werden. Werfen wir einen Blick auf alle Situationen, in denen Änderungen an der Implementierung erforderlich sind:

  • When a new format is introduced: Die Methode muss geändert werden, um die Serialisierung in dieses Format zu implementieren.

  • When the Song object changes: Zum Hinzufügen oder Entfernen von Eigenschaften zur KlasseSong muss die Implementierung geändert werden, um die neue Struktur zu berücksichtigen.

  • When the string representation for a format changes (plain JSON vs JSON API): Die Methode.serialize() muss sich ändern, wenn sich die gewünschte Zeichenfolgendarstellung für ein Format ändert, da die Darstellung in der Methodenimplementierung von.serialize()fest codiert ist.

Die ideale Situation wäre, wenn eine dieser Änderungen der Anforderungen implementiert werden könnte, ohne die.serialize()-Methode zu ändern. In den folgenden Abschnitten erfahren Sie, wie Sie dies tun können.

Auf der Suche nach einer gemeinsamen Schnittstelle

Der erste Schritt, wenn Sie komplexen bedingten Code in einer Anwendung sehen, besteht darin, das gemeinsame Ziel jedes der Ausführungspfade (oder logischen Pfade) zu identifizieren.

Code, derif/elif/else verwendet, hat normalerweise ein gemeinsames Ziel, das in jedem logischen Pfad auf unterschiedliche Weise implementiert wird. Der obige Code konvertiert das Objekt einessongin seine Darstellung desstringunter Verwendung eines anderen Formats in jedem logischen Pfad.

Basierend auf dem Ziel suchen Sie nach einer gemeinsamen Schnittstelle, über die jeder der Pfade ersetzt werden kann. Das obige Beispiel erfordert eine Schnittstelle, die einsong-Objekt nimmt und einstring zurückgibt.

Sobald Sie eine gemeinsame Schnittstelle haben, stellen Sie separate Implementierungen für jeden logischen Pfad bereit. Im obigen Beispiel stellen Sie eine Implementierung zur Serialisierung in JSON und eine andere für XML bereit.

Anschließend stellen Sie eine separate Komponente bereit, die anhand der angegebenenformat über die zu verwendende konkrete Implementierung entscheidet. Diese Komponente wertet den Wert vonformat aus und gibt die durch ihren Wert identifizierte konkrete Implementierung zurück.

In den folgenden Abschnitten erfahren Sie, wie Sie Änderungen an vorhandenem Code vornehmen, ohne das Verhalten zu ändern. Dies wird alsrefactoringdes Codes bezeichnet.

Martin Fowler definiert in seinem BuchRefactoring: Improving the Design of Existing Code Refactoring als "den Prozess, ein Softwaresystem so zu ändern, dass es das externe Verhalten des Codes nicht verändert, aber seine interne Struktur verbessert".

Beginnen wir mit der Überarbeitung des Codes, um die gewünschte Struktur zu erhalten, die das Entwurfsmuster der Factory-Methode verwendet.

Refactoring von Code in die gewünschte Schnittstelle

Die gewünschte Schnittstelle ist ein Objekt oder eine Funktion, die einSong-Objekt nimmt und einestring-Darstellung zurückgibt.

Der erste Schritt besteht darin, einen der logischen Pfade in diese Schnittstelle umzugestalten. Dazu fügen Sie eine neue Methode._serialize_to_json() hinzu und verschieben den JSON-Serialisierungscode dorthin. Anschließend ändern Sie den Client so, dass er aufgerufen wird, anstatt die Implementierung im Hauptteil der Anweisungifzu haben:

class SongSerializer:
    def serialize(self, song, format):
        if format == 'JSON':
            return self._serialize_to_json(song)
        # The rest of the code remains the same

    def _serialize_to_json(self, song):
        payload = {
            'id': song.song_id,
            'title': song.title,
            'artist': song.artist
        }
        return json.dumps(payload)

Sobald Sie diese Änderung vorgenommen haben, können Sie überprüfen, ob sich das Verhalten nicht geändert hat. Anschließend tun Sie dasselbe für die XML-Option, indem Sie eine neue Methode._serialize_to_xml() einführen, die Implementierung dorthin verschieben und den Pfad vonelifändern, um sie aufzurufen.

Das folgende Beispiel zeigt den überarbeiteten Code:

class SongSerializer:
    def serialize(self, song, format):
        if format == 'JSON':
            return self._serialize_to_json(song)
        elif format == 'XML':
            return self._serialize_to_xml(song)
        else:
            raise ValueError(format)

    def _serialize_to_json(self, song):
        payload = {
            'id': song.song_id,
            'title': song.title,
            'artist': song.artist
        }
        return json.dumps(payload)

    def _serialize_to_xml(self, song):
        song_element = et.Element('song', attrib={'id': song.song_id})
        title = et.SubElement(song_element, 'title')
        title.text = song.title
        artist = et.SubElement(song_element, 'artist')
        artist.text = song.artist
        return et.tostring(song_element, encoding='unicode')

Die neue Version des Codes ist leichter zu lesen und zu verstehen, kann jedoch mit einer grundlegenden Implementierung der Factory-Methode noch verbessert werden.

Grundlegende Implementierung der Factory-Methode

Die zentrale Idee in der Factory-Methode besteht darin, eine separate Komponente mit der Verantwortung bereitzustellen, zu entscheiden, welche konkrete Implementierung basierend auf einem bestimmten Parameter verwendet werden soll. Dieser Parameter in unserem Beispiel istformat.

Um die Implementierung der Factory-Methode abzuschließen, fügen Sie eine neue Methode._get_serializer() hinzu, die die gewünschtenformat verwendet. Diese Methode wertet den Wert vonformat aus und gibt die übereinstimmende Serialisierungsfunktion zurück:

class SongSerializer:
    def _get_serializer(self, format):
        if format == 'JSON':
            return self._serialize_to_json
        elif format == 'XML':
            return self._serialize_to_xml
        else:
            raise ValueError(format)

Note: Die Methode._get_serializer() ruft die konkrete Implementierung nicht auf und gibt nur das Funktionsobjekt selbst zurück.

Jetzt können Sie die.serialize()-Methode vonSongSerializer ändern, um._get_serializer() zu verwenden, um die Implementierung der Factory-Methode abzuschließen. Das nächste Beispiel zeigt den vollständigen Code:

class SongSerializer:
    def serialize(self, song, format):
        serializer = self._get_serializer(format)
        return serializer(song)

    def _get_serializer(self, format):
        if format == 'JSON':
            return self._serialize_to_json
        elif format == 'XML':
            return self._serialize_to_xml
        else:
            raise ValueError(format)

    def _serialize_to_json(self, song):
        payload = {
            'id': song.song_id,
            'title': song.title,
            'artist': song.artist
        }
        return json.dumps(payload)

    def _serialize_to_xml(self, song):
        song_element = et.Element('song', attrib={'id': song.song_id})
        title = et.SubElement(song_element, 'title')
        title.text = song.title
        artist = et.SubElement(song_element, 'artist')
        artist.text = song.artist
        return et.tostring(song_element, encoding='unicode')

Die endgültige Implementierung zeigt die verschiedenen Komponenten der Factory-Methode. Die Methode.serialize()ist der Anwendungscode, der von einer Schnittstelle abhängt, um ihre Aufgabe abzuschließen.

Dies wird alsclient-Komponente des Musters bezeichnet. Die definierte Schnittstelle wird alsproduct-Komponente bezeichnet. In unserem Fall ist das Produkt eine Funktion, dieSong benötigt und eine Zeichenfolgendarstellung zurückgibt.

Die Methoden._serialize_to_json() und._serialize_to_xml() sind konkrete Implementierungen des Produkts. Schließlich ist die._get_serializer()-Methode diecreator-Komponente. Der Ersteller entscheidet, welche konkrete Implementierung verwendet werden soll.

Da Sie mit vorhandenem Code begonnen haben, sind alle Komponenten der Factory-Methode Mitglieder derselben KlasseSongSerializer.

Normalerweise ist dies nicht der Fall, und wie Sie sehen, verwendet keine der hinzugefügten Methoden den Parameterself. Dies ist ein guter Hinweis darauf, dass sie keine Methoden der KlasseSongSerializerein sollten und zu externen Funktionen werden können:

class SongSerializer:
    def serialize(self, song, format):
        serializer = get_serializer(format)
        return serializer(song)


def get_serializer(format):
    if format == 'JSON':
        return _serialize_to_json
    elif format == 'XML':
        return _serialize_to_xml
    else:
        raise ValueError(format)


def _serialize_to_json(song):
    payload = {
        'id': song.song_id,
        'title': song.title,
        'artist': song.artist
    }
    return json.dumps(payload)


def _serialize_to_xml(song):
    song_element = et.Element('song', attrib={'id': song.song_id})
    title = et.SubElement(song_element, 'title')
    title.text = song.title
    artist = et.SubElement(song_element, 'artist')
    artist.text = song.artist
    return et.tostring(song_element, encoding='unicode')

Note: Die Methode.serialize() inSongSerializer verwendet nicht den Parameterself.

Die obige Regel besagt, dass es nicht Teil der Klasse sein sollte. Dies ist korrekt, aber Sie haben es mit vorhandenem Code zu tun.

Wenn SieSongSerializer entfernen und die Methode.serialize() in eine Funktion ändern, müssen Sie alle Speicherorte in der Anwendung ändern, dieSongSerializer verwenden, und die Aufrufe der neuen Funktion ersetzen.

Dies ist keine Änderung, die Sie vornehmen sollten, es sei denn, Sie haben bei Ihren Komponententests einen sehr hohen Prozentsatz an Codeabdeckung.

Die Mechanik der Factory-Methode ist immer dieselbe. Ein Client (SongSerializer.serialize()) hängt von einer konkreten Implementierung einer Schnittstelle ab. Es fordert die Implementierung von einer Erstellerkomponente (get_serializer()) unter Verwendung einer Art Bezeichner (format) an.

Der Ersteller gibt die konkrete Implementierung gemäß dem Wert des Parameters an den Client zurück, und der Client verwendet das bereitgestellte Objekt, um seine Aufgabe abzuschließen.

Sie können dieselben Anweisungen im interaktiven Python-Interpreter ausführen, um zu überprüfen, ob sich das Anwendungsverhalten nicht geändert hat:

>>>

>>> import serializer_demo as sd
>>> song = sd.Song('1', 'Water of Love', 'Dire Straits')
>>> serializer = sd.SongSerializer()

>>> serializer.serialize(song, 'JSON')
'{"id": "1", "title": "Water of Love", "artist": "Dire Straits"}'

>>> serializer.serialize(song, 'XML')
'Water of LoveDire Straits'

>>> serializer.serialize(song, 'YAML')
Traceback (most recent call last):
  File "", line 1, in 
  File "./serializer_demo.py", line 13, in serialize
    serializer = get_serializer(format)
  File "./serializer_demo.py", line 23, in get_serializer
    raise ValueError(format)
ValueError: YAML

Sie erstellen einsong und einserializer und verwenden dasserializer, um das Lied in seinestring-Darstellung zu konvertieren, wobei einformat angegeben wird. DaYAML kein unterstütztes Format ist, wirdValueError ausgelöst.

Erkennen von Möglichkeiten zur Verwendung der Factory-Methode

Die Factory-Methode sollte in jeder Situation verwendet werden, in der eine Anwendung (Client) von einer Schnittstelle (Produkt) abhängt, um eine Aufgabe auszuführen, und es gibt mehrere konkrete Implementierungen dieser Schnittstelle. Sie müssen einen Parameter angeben, der die konkrete Implementierung identifizieren und im Ersteller verwenden kann, um die konkrete Implementierung zu bestimmen.

Es gibt eine Vielzahl von Problemen, die zu dieser Beschreibung passen. Schauen wir uns also einige konkrete Beispiele an.

Replacing complex logical code: Komplexe logische Strukturen im Formatif/elif/else sind schwer zu pflegen, da neue logische Pfade erforderlich sind, wenn sich die Anforderungen ändern.

Die Factory-Methode ist ein guter Ersatz, da Sie den Hauptteil jedes logischen Pfads mit einer gemeinsamen Schnittstelle in separate Funktionen oder Klassen unterteilen können und der Ersteller die konkrete Implementierung bereitstellen kann.

Der in den Bedingungen ausgewertete Parameter wird zum Parameter zur Identifizierung der konkreten Implementierung. Das obige Beispiel zeigt diese Situation.

Constructing related objects from external data: Stellen Sie sich eine Anwendung vor, die Mitarbeiterinformationen aus einer Datenbank oder einer anderen externen Quelle abrufen muss.

Die Datensätze stellen Mitarbeiter mit unterschiedlichen Rollen oder Typen dar: Manager, Büroangestellte, Vertriebsmitarbeiter usw. Die Anwendung kann eine Kennung speichern, die den Mitarbeitertyp im Datensatz darstellt, und dann mithilfe der Factory-Methode jedes konkreteEmployee-Objekt aus den restlichen Informationen im Datensatz erstellen.

Supporting multiple implementations of the same feature: Eine Bildverarbeitungsanwendung muss ein Satellitenbild von einem Koordinatensystem in ein anderes transformieren. Es gibt jedoch mehrere Algorithmen mit unterschiedlichen Genauigkeitsstufen, um die Transformation durchzuführen.

Die Anwendung kann es dem Benutzer ermöglichen, eine Option auszuwählen, die den konkreten Algorithmus identifiziert. Die Factory-Methode kann die konkrete Implementierung des Algorithmus basierend auf dieser Option bereitstellen.

Combining similar features under a common interface: Nach dem Bildverarbeitungsbeispiel muss eine Anwendung einen Filter auf ein Bild anwenden. Der zu verwendende spezifische Filter kann durch Benutzereingaben identifiziert werden, und die Factory-Methode kann die konkrete Filterimplementierung bereitstellen.

Integrating related external services: Eine Musik-Player-Anwendung möchte in mehrere externe Dienste integriert werden und Benutzern die Auswahl der Herkunft ihrer Musik ermöglichen. Die Anwendung kann eine gemeinsame Schnittstelle für einen Musikdienst definieren und mithilfe der Factory-Methode die richtige Integration basierend auf den Benutzereinstellungen erstellen.

Alle diese Situationen sind ähnlich. Sie alle definieren einen Client, der von einer gemeinsamen Schnittstelle abhängt, die als Produkt bezeichnet wird. Sie alle bieten die Möglichkeit, die konkrete Implementierung des Produkts zu identifizieren, sodass sie alle die Factory-Methode in ihrem Design verwenden können.

Sie können nun das Serialisierungsproblem aus den vorherigen Beispielen betrachten und ein besseres Design bereitstellen, indem Sie das Entwurfsmuster der Factory-Methode berücksichtigen.

Ein Beispiel für die Objektserialisierung

Die Grundvoraussetzungen für das obige Beispiel sind, dass SieSong-Objekte in ihrestring-Darstellung serialisieren möchten. Es scheint, dass die Anwendung Funktionen im Zusammenhang mit Musik bietet. Daher ist es plausibel, dass die Anwendung andere Objekttypen wiePlaylist oderAlbum serialisieren muss.

Im Idealfall sollte das Design das Hinzufügen von Serialisierung für neue Objekte unterstützen, indem neue Klassen implementiert werden, ohne dass Änderungen an der vorhandenen Implementierung erforderlich sind. Die Anwendung erfordert, dass Objekte in mehrere Formate wie JSON und XML serialisiert werden. Daher erscheint es naheliegend, eine SchnittstelleSerializer zu definieren, die mehrere Implementierungen haben kann, eine pro Format.

Die Schnittstellenimplementierung könnte ungefähr so ​​aussehen:

# In serializers.py

import json
import xml.etree.ElementTree as et

class JsonSerializer:
    def __init__(self):
        self._current_object = None

    def start_object(self, object_name, object_id):
        self._current_object = {
            'id': object_id
        }

    def add_property(self, name, value):
        self._current_object[name] = value

    def to_str(self):
        return json.dumps(self._current_object)


class XmlSerializer:
    def __init__(self):
        self._element = None

    def start_object(self, object_name, object_id):
        self._element = et.Element(object_name, attrib={'id': object_id})

    def add_property(self, name, value):
        prop = et.SubElement(self._element, name)
        prop.text = value

    def to_str(self):
        return et.tostring(self._element, encoding='unicode')

Note: Das obige Beispiel implementiert keine vollständigeSerializer-Schnittstelle, sollte jedoch für unsere Zwecke und zur Demonstration der Factory-Methode ausreichend sein.

DieSerializer-Schnittstelle ist aufgrund der Dynamik derPython-Sprache ein abstraktes Konzept. Für statische Sprachen wie Java oder C # müssen Schnittstellen explizit definiert werden. In Python soll jedes Objekt, das die gewünschten Methoden oder Funktionen bereitstellt, die Schnittstelle implementieren. Das Beispiel definiert dieSerializer-Schnittstelle als ein Objekt, das die folgenden Methoden oder Funktionen implementiert:

  • .start_object(object_name, object_id)

  • .add_property(name, value)

  • .to_str()

Diese Schnittstelle wird durch die konkreten KlassenJsonSerializer undXmlSerializer implementiert.

Im ursprünglichen Beispiel wurde eineSongSerializer-Klasse verwendet. Für die neue Anwendung implementieren Sie etwas Allgemeineres wieObjectSerializer:

# In serializers.py

class ObjectSerializer:
    def serialize(self, serializable, format):
        serializer = factory.get_serializer(format)
        serializable.serialize(serializer)
        return serializer.to_str()

Die Implementierung vonObjectSerializer ist vollständig generisch und nennt nurserializable undformat als Parameter.

Dasformat wird verwendet, um die konkrete Implementierung desSerializer zu identifizieren, und wird durch dasfactory-Objekt aufgelöst. Der Parameterserializable bezieht sich auf eine andere abstrakte Schnittstelle, die für jeden Objekttyp implementiert werden sollte, den Sie serialisieren möchten.

Schauen wir uns eine konkrete Implementierung derserializable-Schnittstelle in derSong-Klasse an:

# In songs.py

class Song:
    def __init__(self, song_id, title, artist):
        self.song_id = song_id
        self.title = title
        self.artist = artist

    def serialize(self, serializer):
        serializer.start_object('song', self.song_id)
        serializer.add_property('title', self.title)
        serializer.add_property('artist', self.artist)

Die KlasseSong implementiert die SchnittstelleSerializable, indem sie eine Methode.serialize(serializer) bereitstellt. In der Methode verwendet die KlasseSongdas Objektserializer, um eigene Informationen zu schreiben, ohne das Format zu kennen.

Tatsächlich weiß dieSong-Klasse nicht einmal, dass das Ziel darin besteht, die Daten in eine Zeichenfolge zu konvertieren. Dies ist wichtig, da Sie diese Schnittstelle verwenden können, um eine andere Art vonserializer bereitzustellen, die dieSong-Informationen bei Bedarf in eine völlig andere Darstellung konvertiert. Beispielsweise muss Ihre Anwendung möglicherweise in Zukunft dasSong-Objekt in ein Binärformat konvertieren.

Bisher haben wir die Implementierung des Clients (ObjectSerializer) und des Produkts (serializer) gesehen. Es ist Zeit, die Implementierung der Factory-Methode abzuschließen und den Ersteller bereitzustellen. Der Ersteller im Beispiel ist die Variablefactory inObjectSerializer.serialize().

Factory-Methode als Objektfabrik

Im ursprünglichen Beispiel haben Sie den Ersteller als Funktion implementiert. Funktionen sind für sehr einfache Beispiele in Ordnung, bieten jedoch nicht zu viel Flexibilität, wenn sich die Anforderungen ändern.

Klassen können zusätzliche Schnittstellen bereitstellen, um Funktionen hinzuzufügen, und sie können abgeleitet werden, um das Verhalten anzupassen. Sofern Sie keinen sehr einfachen Ersteller haben, der sich in Zukunft nie ändern wird, möchten Sie ihn als Klasse und nicht als Funktion implementieren. Diese Arten von Klassen werden als Objektfabriken bezeichnet.

Sie können die grundlegende Schnittstelle vonSerializerFactory in der Implementierung vonObjectSerializer.serialize() sehen. Die Methode verwendetfactory.get_serializer(format), umserializer aus der Objektfactory abzurufen.

Sie werden jetztSerializerFactory implementieren, um diese Schnittstelle zu erfüllen:

# In serializers.py

class SerializerFactory:
    def get_serializer(self, format):
        if format == 'JSON':
            return JsonSerializer()
        elif format == 'XML':
            return XmlSerializer()
        else:
            raise ValueError(format)


factory = SerializerFactory()

Die aktuelle Implementierung von.get_serializer() entspricht der im ursprünglichen Beispiel verwendeten. Die Methode wertet den Wert vonformat aus und entscheidet über die konkrete Implementierung, die erstellt und zurückgegeben werden soll. Es ist eine relativ einfache Lösung, mit der wir die Funktionalität aller Komponenten der Factory-Methode überprüfen können.

Gehen wir zum interaktiven Python-Interpreter und sehen, wie es funktioniert:

>>>

>>> import songs
>>> import serializers
>>> song = songs.Song('1', 'Water of Love', 'Dire Straits')
>>> serializer = serializers.ObjectSerializer()

>>> serializer.serialize(song, 'JSON')
'{"id": "1", "title": "Water of Love", "artist": "Dire Straits"}'

>>> serializer.serialize(song, 'XML')
'Water of LoveDire Straits'

>>> serializer.serialize(song, 'YAML')
Traceback (most recent call last):
  File "", line 1, in 
  File "./serializers.py", line 39, in serialize
    serializer = factory.get_serializer(format)
  File "./serializers.py", line 52, in get_serializer
    raise ValueError(format)
ValueError: YAML

Das neue Design der Factory-Methode ermöglicht es der Anwendung, neue Funktionen durch Hinzufügen neuer Klassen einzuführen, anstatt vorhandene zu ändern. Sie können andere Objekte serialisieren, indem Sie dieSerializable-Schnittstelle auf ihnen implementieren. Sie können neue Formate unterstützen, indem Sie die SchnittstelleSerializerin einer anderen Klasse implementieren.

Das fehlende Teil ist, dassSerializerFactory geändert werden muss, um die Unterstützung für neue Formate einzuschließen. Dieses Problem lässt sich mit dem neuen Design leicht lösen, daSerializerFactory eine Klasse ist.

Unterstützung zusätzlicher Formate

Die aktuelle Implementierung vonSerializerFactory muss geändert werden, wenn ein neues Format eingeführt wird. Ihre Anwendung muss möglicherweise nie zusätzliche Formate unterstützen, aber Sie wissen es nie.

Sie möchten, dass Ihre Designs flexibel sind, und wie Sie sehen werden, ist es relativ einfach, zusätzliche Formate zu unterstützen, ohneSerializerFactory zu ändern.

Die Idee ist, eine Methode inSerializerFactory bereitzustellen, die eine neueSerializer-Implementierung für das Format registriert, das wir unterstützen möchten:

# In serializers.py

class SerializerFactory:

    def __init__(self):
        self._creators = {}

    def register_format(self, format, creator):
        self._creators[format] = creator

    def get_serializer(self, format):
        creator = self._creators.get(format)
        if not creator:
            raise ValueError(format)
        return creator()


factory = SerializerFactory()
factory.register_format('JSON', JsonSerializer)
factory.register_format('XML', XmlSerializer)

Mit der Methode.register_format(format, creator) können neue Formate registriert werden, indem ein Wert vonformatangegeben wird, mit dem das Format und das Objektcreatoridentifiziert werden. Das Erstellerobjekt ist zufällig der Klassenname der konkretenSerializer. Dies ist möglich, da alle Klassen vonSerializereinen Standardwert von.__init__() zum Initialisieren der Instanzen bereitstellen.

Die Registrierungsinformationen werden im_creators-Wörterbuch gespeichert. Die Methode.get_serializer() ruft den registrierten Ersteller ab und erstellt das gewünschte Objekt. Wenn das angeforderteformat nicht registriert wurde, wirdValueError ausgelöst.

Sie können jetzt die Flexibilität des Entwurfs überprüfen, indem SieYamlSerializer implementieren und die störendenValueError entfernen, die Sie zuvor gesehen haben:

# In yaml_serializer.py

import yaml
import serializers

class YamlSerializer(serializers.JsonSerializer):
    def to_str(self):
        return yaml.dump(self._current_object)


serializers.factory.register_format('YAML', YamlSerializer)

Note: Um das Beispiel zu implementieren, müssen SiePyYAML mitpip install PyYAML in Ihrer Umgebung installieren.

JSON und YAML sind sehr ähnliche Formate, sodass Sie den größten Teil der Implementierung vonJsonSerializer wiederverwenden und.to_str() überschreiben können, um die Implementierung abzuschließen. Das Format wird dann mit dem Objektfactoryregistriert, um es verfügbar zu machen.

Verwenden Sie den interaktiven Python-Interpreter, um die Ergebnisse anzuzeigen:

>>>

>>> import serializers
>>> import songs
>>> import yaml_serializer
>>> song = songs.Song('1', 'Water of Love', 'Dire Straits')
>>> serializer = serializers.ObjectSerializer()

>>> print(serializer.serialize(song, 'JSON'))
{"id": "1", "title": "Water of Love", "artist": "Dire Straits"}

>>> print(serializer.serialize(song, 'XML'))
Water of LoveDire Straits

>>> print(serializer.serialize(song, 'YAML'))
{artist: Dire Straits, id: '1', title: Water of Love}

Durch die Implementierung der Factory-Methode mithilfe einer Object Factory und die Bereitstellung einer Registrierungsschnittstelle können Sie neue Formate unterstützen, ohne den vorhandenen Anwendungscode zu ändern. Dies minimiert das Risiko, vorhandene Funktionen zu beschädigen oder subtile Fehler einzuführen.

Eine Allzweck-Objektfabrik

Die Implementierung vonSerializerFactory ist eine enorme Verbesserung gegenüber dem ursprünglichen Beispiel. Es bietet große Flexibilität bei der Unterstützung neuer Formate und vermeidet das Ändern von vorhandenem Code.

Die aktuelle Implementierung ist jedoch speziell auf das oben genannte Serialisierungsproblem ausgerichtet und kann in anderen Kontexten nicht wiederverwendet werden.

Die Factory-Methode kann verwendet werden, um eine Vielzahl von Problemen zu lösen. Eine Objektfabrik bietet zusätzliche Flexibilität für das Design, wenn sich die Anforderungen ändern. Idealerweise möchten Sie eine Implementierung von Object Factory, die in jeder Situation wiederverwendet werden kann, ohne die Implementierung zu replizieren.

Die Bereitstellung einer allgemeinen Implementierung von Object Factory ist mit einigen Herausforderungen verbunden. In den folgenden Abschnitten werden Sie sich mit diesen Herausforderungen befassen und eine Lösung implementieren, die in jeder Situation wiederverwendet werden kann.

Nicht alle Objekte können gleich erstellt werden

Die größte Herausforderung bei der Implementierung einer Allzweck-Objektfabrik besteht darin, dass nicht alle Objekte auf dieselbe Weise erstellt werden.

Nicht in allen Situationen können wir zum Erstellen und Initialisieren der Objekte einen Standardwert von.__init__()verwenden. Es ist wichtig, dass der Ersteller, in diesem Fall die Object Factory, vollständig initialisierte Objekte zurückgibt.

Dies ist wichtig, da der Client die Initialisierung abschließen und komplexen bedingten Code verwenden muss, um die bereitgestellten Objekte vollständig zu initialisieren, wenn dies nicht der Fall ist. Dies macht den Zweck des Entwurfsmusters der Factory-Methode zunichte.

Um die Komplexität einer Allzwecklösung zu verstehen, werfen wir einen Blick auf ein anderes Problem. Angenommen, eine Anwendung möchte in verschiedene Musikdienste integriert werden. Diese Dienste können außerhalb der Anwendung oder intern sein, um eine lokale Musiksammlung zu unterstützen. Jeder der Dienste hat unterschiedliche Anforderungen.

Note: Die Anforderungen, die ich für das Beispiel definiere, dienen nur zur Veranschaulichung und spiegeln nicht die tatsächlichen Anforderungen wider, die Sie für die Integration in Dienste wiePandora oderSpotify implementieren müssen.

Ziel ist es, andere Anforderungen bereitzustellen, die die Herausforderungen bei der Implementierung einer Allzweck-Objektfabrik aufzeigen.

Stellen Sie sich vor, die Anwendung möchte in einen von Spotify bereitgestellten Dienst integriert werden. Dieser Dienst erfordert einen Autorisierungsprozess, bei dem ein Client-Schlüssel und ein Geheimnis zur Autorisierung bereitgestellt werden.

Der Dienst gibt einen Zugangscode zurück, der für jede weitere Kommunikation verwendet werden soll. Dieser Autorisierungsprozess ist sehr langsam und sollte nur einmal ausgeführt werden. Daher möchte die Anwendung das initialisierte Serviceobjekt behalten und jedes Mal verwenden, wenn sie mit Spotify kommunizieren muss.

Gleichzeitig möchten andere Benutzer in Pandora integrieren. Pandora verwendet möglicherweise einen völlig anderen Autorisierungsprozess. Es erfordert auch einen Client-Schlüssel und ein Geheimnis, gibt jedoch einen Verbraucherschlüssel und ein Geheimnis zurück, die für andere Kommunikationen verwendet werden sollten. Wie bei Spotify ist der Autorisierungsprozess langsam und sollte nur einmal ausgeführt werden.

Schließlich implementiert die Anwendung das Konzept eines lokalen Musikdienstes, bei dem die Musiksammlung lokal gespeichert wird. Für den Dienst muss der Speicherort der Musiksammlung im lokalen System angegeben werden. Das Erstellen einer neuen Dienstinstanz erfolgt sehr schnell, sodass jedes Mal, wenn der Benutzer auf die Musiksammlung zugreifen möchte, eine neue Instanz erstellt werden kann.

Dieses Beispiel bietet mehrere Herausforderungen. Jeder Dienst wird mit einem anderen Parametersatz initialisiert. Außerdem erfordern Spotify und Pandora einen Autorisierungsprozess, bevor die Dienstinstanz erstellt werden kann.

Sie möchten diese Instanz auch wiederverwenden, um zu vermeiden, dass die Anwendung mehrmals autorisiert wird. Der lokale Dienst ist einfacher, stimmt jedoch nicht mit der Initialisierungsschnittstelle der anderen überein.

In den folgenden Abschnitten lösen Sie dieses Problem, indem Sie die Erstellungsoberfläche verallgemeinern und eine Allzweck-Object Factory implementieren.

Separate Objekterstellung zur Bereitstellung einer gemeinsamen Schnittstelle

Die Erstellung jedes konkreten Musikdienstes hat seine eigenen Anforderungen. Dies bedeutet, dass eine gemeinsame Initialisierungsschnittstelle für jede Service-Implementierung nicht möglich oder empfohlen ist.

Der beste Ansatz besteht darin, einen neuen Objekttyp zu definieren, der eine allgemeine Schnittstelle bietet und für die Erstellung eines konkreten Dienstes verantwortlich ist. Dieser neue Objekttyp wird alsBuilder bezeichnet. Das%(t1)s Objekt verfügt über die gesamte Logik, um eine Dienstinstanz zu erstellen und zu initialisieren. Sie implementieren einBuilder-Objekt für jeden der unterstützten Dienste.

Schauen wir uns zunächst die Anwendungskonfiguration an:

# In program.py

config = {
    'spotify_client_key': 'THE_SPOTIFY_CLIENT_KEY',
    'spotify_client_secret': 'THE_SPOTIFY_CLIENT_SECRET',
    'pandora_client_key': 'THE_PANDORA_CLIENT_KEY',
    'pandora_client_secret': 'THE_PANDORA_CLIENT_SECRET',
    'local_music_location': '/usr/data/music'
}

Dasconfig-Wörterbuch enthält alle Werte, die zum Initialisieren der einzelnen Dienste erforderlich sind. Der nächste Schritt besteht darin, eine Schnittstelle zu definieren, die diese Werte verwendet, um eine konkrete Implementierung eines Musikdienstes zu erstellen. Diese Schnittstelle wird inBuilder implementiert.

Schauen wir uns die Implementierung derSpotifyService undSpotifyServiceBuilder an:

# In music.py

class SpotifyService:
    def __init__(self, access_code):
        self._access_code = access_code

    def test_connection(self):
        print(f'Accessing Spotify with {self._access_code}')


class SpotifyServiceBuilder:
    def __init__(self):
        self._instance = None

    def __call__(self, spotify_client_key, spotify_client_secret, **_ignored):
        if not self._instance:
            access_code = self.authorize(
                spotify_client_key, spotify_client_secret)
            self._instance = SpotifyService(access_code)
        return self._instance

    def authorize(self, key, secret):
        return 'SPOTIFY_ACCESS_CODE'

Note: Die Musikdienstschnittstelle definiert eine.test_connection()-Methode, die für Demonstrationszwecke ausreichen sollte.

Das Beispiel zeigt einSpotifyServiceBuilder, das.__call__(spotify_client_key, spotify_client_secret, **_ignored) implementiert.

Diese Methode wird verwendet, um die konkretenSpotifyService zu erstellen und zu initialisieren. Es gibt die erforderlichen Parameter an und ignoriert alle zusätzlichen Parameter, die durch**_ignored bereitgestellt werden. Sobaldaccess_code abgerufen wurde, wird die Instanz vonSpotifyServiceerstellt und zurückgegeben.

Beachten Sie, dassSpotifyServiceBuilder die Dienstinstanz beibehält und erst beim ersten Anfordern des Dienstes eine neue erstellt. Dadurch wird vermieden, dass der Autorisierungsprozess mehrmals durchlaufen wird, wie in den Anforderungen angegeben.

Machen wir dasselbe für Pandora:

# In music.py

class PandoraService:
    def __init__(self, consumer_key, consumer_secret):
        self._key = consumer_key
        self._secret = consumer_secret

    def test_connection(self):
        print(f'Accessing Pandora with {self._key} and {self._secret}')


class PandoraServiceBuilder:
    def __init__(self):
        self._instance = None

    def __call__(self, pandora_client_key, pandora_client_secret, **_ignored):
        if not self._instance:
            consumer_key, consumer_secret = self.authorize(
                pandora_client_key, pandora_client_secret)
            self._instance = PandoraService(consumer_key, consumer_secret)
        return self._instance

    def authorize(self, key, secret):
        return 'PANDORA_CONSUMER_KEY', 'PANDORA_CONSUMER_SECRET'

DasPandoraServiceBuilder implementiert dieselbe Schnittstelle, verwendet jedoch unterschiedliche Parameter und Prozesse, um dasPandoraService zu erstellen und zu initialisieren. Außerdem bleibt die Dienstinstanz erhalten, sodass die Autorisierung nur einmal erfolgt.

Schauen wir uns zum Schluss die Implementierung des lokalen Dienstes an:

# In music.py

class LocalService:
    def __init__(self, location):
        self._location = location

    def test_connection(self):
        print(f'Accessing Local music at {self._location}')


def create_local_music_service(local_music_location, **_ignored):
    return LocalService(local_music_location)

DieLocalService erfordern nur einen Speicherort, an dem die Sammlung gespeichert ist, um dieLocalService zu initialisieren.

Jedes Mal, wenn der Dienst angefordert wird, wird eine neue Instanz erstellt, da kein langsamer Autorisierungsprozess stattfindet. Die Anforderungen sind einfacher, sodass Sie keineBuilder-Klasse benötigen. Stattdessen wird eine Funktion verwendet, die ein initialisiertesLocalService zurückgibt. Diese Funktion entspricht der Schnittstelle der in den Builder-Klassen implementierten.__call__()-Methoden.

Eine generische Schnittstelle zur Object Factory

Eine Allzweck-Objektfabrik (ObjectFactory) kann die generischeBuilder-Schnittstelle nutzen, um alle Arten von Objekten zu erstellen. Es bietet eine Methode zum Registrieren vonBuilder basierend auf einemkey-Wert und eine Methode zum Erstellen der konkreten Objektinstanzen basierend aufkey.

Schauen wir uns die Implementierung unserer generischenObjectFactoryan:

# In object_factory.py

class ObjectFactory:
    def __init__(self):
        self._builders = {}

    def register_builder(self, key, builder):
        self._builders[key] = builder

    def create(self, key, **kwargs):
        builder = self._builders.get(key)
        if not builder:
            raise ValueError(key)
        return builder(**kwargs)

Die Implementierungsstruktur vonObjectFactory ist dieselbe wie inSerializerFactory.

Der Unterschied besteht in der Schnittstelle, die die Erstellung eines beliebigen Objekttyps unterstützt. Der Builder-Parameter kann ein beliebiges Objekt sein, das diecallable-Schnittstelle implementiert. Das bedeutet, dassBuilder eine Funktion, eine Klasse oder ein Objekt sein kann, das.__call__() implementiert.

Die Methode.create()erfordert, dass zusätzliche Argumente als Schlüsselwortargumente angegeben werden. Auf diese Weise können dieBuilder-Objekte die benötigten Parameter angeben und den Rest in keiner bestimmten Reihenfolge ignorieren. Sie können beispielsweise sehen, dasscreate_local_music_service() einenlocal_music_location-Parameter angibt und den Rest ignoriert.

Erstellen Sie die Factory-Instanz und registrieren Sie die Builder für die Services, die Sie unterstützen möchten:

# In music.py
import object_factory

# Omitting other implementation classes shown above

factory = object_factory.ObjectFactory()
factory.register_builder('SPOTIFY', SpotifyServiceBuilder())
factory.register_builder('PANDORA', PandoraServiceBuilder())
factory.register_builder('LOCAL', create_local_music_service)

Das Modulmusic macht die InstanzObjectFactoryüber das Attributfactory verfügbar. Anschließend werden die Builder bei der Instanz registriert. Für Spotify und Pandora registrieren Sie eine Instanz des entsprechenden Builders, aber für den lokalen Dienst übergeben Sie einfach die Funktion.

Schreiben wir ein kleines Programm, das die Funktionalität demonstriert:

# In program.py
import music

config = {
    'spotify_client_key': 'THE_SPOTIFY_CLIENT_KEY',
    'spotify_client_secret': 'THE_SPOTIFY_CLIENT_SECRET',
    'pandora_client_key': 'THE_PANDORA_CLIENT_KEY',
    'pandora_client_secret': 'THE_PANDORA_CLIENT_SECRET',
    'local_music_location': '/usr/data/music'
}

pandora = music.factory.create('PANDORA', **config)
pandora.test_connection()

spotify = music.factory.create('SPOTIFY', **config)
spotify.test_connection()

local = music.factory.create('LOCAL', **config)
local.test_connection()

pandora2 = music.services.get('PANDORA', **config)
print(f'id(pandora) == id(pandora2): {id(pandora) == id(pandora2)}')

spotify2 = music.services.get('SPOTIFY', **config)
print(f'id(spotify) == id(spotify2): {id(spotify) == id(spotify2)}')

Die Anwendung definiert einconfig-Wörterbuch, das die Anwendungskonfiguration darstellt. Die Konfiguration wird unabhängig vom Dienst, auf den Sie zugreifen möchten, als Schlüsselwortargument für die Factory verwendet. Die Factory erstellt die konkrete Implementierung des Musikdienstes basierend auf dem angegebenen Parameterkey.

Sie können jetzt unser Programm ausführen, um zu sehen, wie es funktioniert:

$ python program.py
Accessing Pandora with PANDORA_CONSUMER_KEY and PANDORA_CONSUMER_SECRET
Accessing Spotify with SPOTIFY_ACCESS_CODE
Accessing Local music at /usr/data/music
id(pandora) == id(pandora2): True
id(spotify) == id(spotify2): True

Sie können sehen, dass abhängig vom angegebenen Diensttyp die richtige Instanz erstellt wird. Sie können auch sehen, dass das Anfordern des Pandora- oder Spotify-Dienstes immer dieselbe Instanz zurückgibt.

Spezialisierte Object Factory zur Verbesserung der Lesbarkeit von Code

Allgemeine Lösungen sind wiederverwendbar und vermeiden Code-Duplikationen. Leider können sie den Code auch verdecken und weniger lesbar machen.

Das obige Beispiel zeigt, dass für den Zugriff auf einen Musikdienstmusic.factory.create() aufgerufen wird. Dies kann zu Verwirrung führen. Andere Entwickler glauben möglicherweise, dass jedes Mal eine neue Instanz erstellt wird, und entscheiden, dass sie die Dienstinstanz beibehalten sollen, um den langsamen Initialisierungsprozess zu vermeiden.

Sie wissen, dass dies nicht der Fall ist, da die KlasseBuilderdie initialisierte Instanz beibehält und für nachfolgende Aufrufe zurückgibt. Dies wird jedoch nicht durch einfaches Lesen des Codes deutlich.

Eine gute Lösung besteht darin, eine Allzweckimplementierung zu spezialisieren, um eine Schnittstelle bereitzustellen, die für den Anwendungskontext konkret ist. In diesem Abschnitt werden SieObjectFactory im Kontext unserer Musikdienste spezialisieren, damit der Anwendungscode die Absicht besser kommuniziert und besser lesbar wird.

Das folgende Beispiel zeigt, wie SieObjectFactory spezialisieren und eine explizite Schnittstelle zum Kontext der Anwendung bereitstellen:

# In music.py

class MusicServiceProvider(object_factory.ObjectFactory):
    def get(self, service_id, **kwargs):
        return self.create(service_id, **kwargs)


services = MusicServiceProvider()
services.register_builder('SPOTIFY', SpotifyServiceBuilder())
services.register_builder('PANDORA', PandoraServiceBuilder())
services.register_builder('LOCAL', create_local_music_service)

Sie leitenMusicServiceProvider vonObjectFactory ab und legen eine neue Methode.get(service_id, **kwargs) offen.

Diese Methode ruft das generische.create(key, **kwargs) auf, sodass das Verhalten gleich bleibt, der Code jedoch im Kontext unserer Anwendung besser gelesen werden kann. Sie haben auch die vorherige Variablefactoryinservices umbenannt und alsMusicServiceProvider initialisiert.

Wie Sie sehen können, liest sich der aktualisierte Anwendungscode jetzt viel besser:

import music

config = {
    'spotify_client_key': 'THE_SPOTIFY_CLIENT_KEY',
    'spotify_client_secret': 'THE_SPOTIFY_CLIENT_SECRET',
    'pandora_client_key': 'THE_PANDORA_CLIENT_KEY',
    'pandora_client_secret': 'THE_PANDORA_CLIENT_SECRET',
    'local_music_location': '/usr/data/music'
}

pandora = music.services.get('PANDORA', **config)
pandora.test_connection()
spotify = music.services.get('SPOTIFY', **config)
spotify.test_connection()
local = music.services.get('LOCAL', **config)
local.test_connection()

pandora2 = music.services.get('PANDORA', **config)
print(f'id(pandora) == id(pandora2): {id(pandora) == id(pandora2)}')

spotify2 = music.services.get('SPOTIFY', **config)
print(f'id(spotify) == id(spotify2): {id(spotify) == id(spotify2)}')

Das Ausführen des Programms zeigt, dass sich das Verhalten nicht geändert hat:

$ python program.py
Accessing Pandora with PANDORA_CONSUMER_KEY and PANDORA_CONSUMER_SECRET
Accessing Spotify with SPOTIFY_ACCESS_CODE
Accessing Local music at /usr/data/music
id(pandora) == id(pandora2): True
id(spotify) == id(spotify2): True

Fazit

Die Factory-Methode ist ein weit verbreitetes kreatives Entwurfsmuster, das in vielen Situationen verwendet werden kann, in denen mehrere konkrete Implementierungen einer Schnittstelle vorhanden sind.

Das Muster entfernt komplexen logischen Code, der schwer zu pflegen ist, und ersetzt ihn durch ein Design, das wiederverwendbar und erweiterbar ist. Das Muster vermeidet das Ändern von vorhandenem Code, um neue Anforderungen zu unterstützen.

Dies ist wichtig, da das Ändern von vorhandenem Code zu Verhaltensänderungen oder subtilen Fehlern führen kann.

In diesem Artikel haben Sie gelernt:

  • Was ist das Entwurfsmuster der Factory-Methode und was sind seine Komponenten?

  • So überarbeiten Sie vorhandenen Code, um die Factory-Methode zu nutzen

  • Situationen, in denen die Factory-Methode angewendet werden sollte

  • Wie Objektfabriken mehr Flexibilität bei der Implementierung der Factory-Methode bieten

  • Implementieren einer Allzweck-Objektfabrik und ihrer Herausforderungen

  • Spezialisierung einer allgemeinen Lösung zur Bereitstellung eines besseren Kontexts

Weitere Lektüre

Wenn Sie mehr über die Factory-Methode und andere Entwurfsmuster erfahren möchten, empfehle ichDesign Patterns: Elements of Reusable Object-Oriented Software von der GoF, eine hervorragende Referenz für weit verbreitete Entwurfsmuster.

Außerdem bietetHeads First Design Patterns: A Brain-Friendly Guide von Eric Freeman und Elisabeth Robson eine unterhaltsame, leicht zu lesende Erklärung für Entwurfsmuster.

Wikipedia hat einen guten Katalog vondesign patterns mit Links zu Seiten für die häufigsten und nützlichsten Muster.