Schöpfungsmuster: Abstrakte Fabrik

Dec 04 2022
Hier ist ein Artikel, der ein kreatives Designmuster beschreibt, das es uns ermöglicht, eine Produktfamilie auf flexible und erweiterbare Weise zu erstellen. Was ist es? Im letzten Artikel haben wir etwas über das Factory-Methodenmuster gelernt.

Hier ist ein Artikel, der ein kreatives Designmuster beschreibt, das es uns ermöglicht, eine Produktfamilie auf flexible und erweiterbare Weise zu erstellen.

Was ist es?

Im letzten Artikel haben wir etwas über die Factory-Methode pattern gelernt . Dann haben wir gesehen, wie wir die Varianten des F-16 mit der Fabrikmethode modellieren können. In diesem Artikel wird jedoch behandelt, wie zahlreiche andere Flugzeuge als die F-16 dargestellt werden. Angenommen, ein Kunde kauft eine Boeing 747 für seinen CEO und möchte, dass Ihre Software diesen neuen Flugzeugtyp unterstützt.

Das abstrakte Fabrikmuster löst das Problem der Erstellung von Familien verwandter Produkte. Zum Beispiel braucht F-16 einen Motor, ein Cockpit und Flügel. Die Boeing-747 würde die gleichen Teile erfordern, aber sie wären spezifisch für Boeing. Jedes Flugzeug würde diese drei verwandten Teile benötigen; Die Teile sind jedoch flugzeugspezifisch. Können Sie hier ein Muster erkennen? Zunächst benötigen wir ein Framework zum Erstellen der zugehörigen Teile für jedes Flugzeug.

Das abstrakte Fabrikmuster ist ein Entwurfsmuster, mit dem Sie Familien verwandter oder abhängiger Objekte erstellen können, ohne ihre konkreten Klassen anzugeben.

Klassen Diagramm

Das Klassendiagramm enthält die folgenden Entitäten:

  • Abstrakte Fabrik
  • Betonfabrik
  • Abstraktes Produkt
  • Konkretes Produkt
  • Klient

Mit dem abstrakten Fabrikmuster können Sie eine Familie verwandter Objekte erstellen, ohne ihre konkreten Klassen anzugeben. Betrachten wir ein Beispiel. Angenommen, Sie erstellen Simulationssoftware für die Luftfahrtindustrie und müssen verschiedene Flugzeuge als Objekte darstellen. Aber bevor Sie ein Flugzeug darstellen, müssen Sie auch die anderen Teile eines Flugzeugs als Objekte definieren. Wählen wir drei aus: Flügel, Cockpit und Motor. Angenommen, das erste Flugzeug, das Sie darstellen möchten, ist die mächtige F-16. Sie werden wahrscheinlich drei Klassen schreiben, eine für jedes Stück, das für die F-16 spezifisch ist. In Ihrem Code werden Sie wahrscheinlich diese gerade erstellten drei Klassen wie folgt verwenden:

Dieser Codeschnipsel sieht harmlos aus, kann aber starke Kopfschmerzen verursachen, wenn Ihre Simulationssoftware abhebt und Sie sie auf andere Flugzeuge erweitern müssen. Dies sind einige der Probleme mit dem obigen Code:

  • Die konkreten Klassen für die drei Teile wurden dem Verbraucher direkt ausgesetzt.
  • Die F-16 hat mehrere Varianten mit unterschiedlichen Motoren. Wenn Sie ein Engine-Objekt zurückgeben möchten, das der Variante entspricht, müssen Sie die F-16Engine-Klasse ableiten, was auch eine Änderung des Consumer-Snippets erfordern würde.
  • Die Liste im Codeschnipsel ist mit einer konkreten Klasse parametrisiert deklariert; Wenn Sie Ihrem Programm ein weiteres Flugzeugtriebwerk hinzufügen, wird es von der Liste nicht erkannt, obwohl die Triebwerke für alle Flugzeuge etwas ähnlich sind.

Behandeln Sie eine Schnittstelle eher als abstraktes Konzept denn als Implementierung

Gutes objektorientiertes Design bedeutet, die konkreten Klassen zu verbergen, die in Ihrer Anwendung verwendet werden, und Schnittstellen für Clients offenzulegen. Ein Objekt antwortet auf eine Reihe von Anforderungen, die von einer Schnittstelle erfasst werden können, die von der Klasse des Objekts implementiert wird. Kunden sollten eher wissen, auf welche Anforderungen ein Objekt reagiert, als auf die Implementierung.

In unserem Beispiel erstellen wir eine Schnittstelle, die eine Methode namens start() verfügbar macht . Die Klasse F16Engine ändert sich dann wie folgt:

Sehen Sie, wie sich der entsprechende Verbrauchercode mit der obigen Änderung ändert.

Der Verbrauchercode enthält jetzt keine Implementierungsdetails darüber, wie die F-16-Engine funktioniert und welche Klasse sie implementiert. Allerdings wollen wir den F16Engine()- Teil des Codes immer noch nicht preisgeben . Wir möchten, dass unsere Verbraucher im Unklaren darüber bleiben, welche Klasse wir instanziieren. Dies wird als nächstes diskutiert.

Erstellen einer Fabrik

Anstatt Objekte im Client-Code zu erstellen, haben wir eine Factory-Klasse, die die angeforderten Objekte erstellt und sie an den Client zurückgibt. Wir nennen diese Klasse F16Factory , weil sie verschiedene Teile des F16-Militärflugzeugs produzieren und an den anfordernden Kunden liefern kann. Die Klasse würde die folgende Form annehmen.

Nehmen wir nun an, dass wir das F16Factory- Objekt als Konstruktorargument an den Clientcode übergeben , und dieser kann dann Objekte wie folgt erstellen:

Beachten Sie, wie dieses Setup es uns ermöglicht, die konkrete Klasse zu ändern, die die F16Engine darstellt, solange sie an die IEngine- Schnittstelle gebunden ist. Wir können unsere Klasse umbenennen, erweitern oder modifizieren, ohne eine Breaking Change im Client zu verursachen. Beachten Sie auch, dass wir dem Kunden verschiedene Teile für ein völlig neues Flugzeug liefern können, indem wir nur die an den Kundenhersteller übergebene Werksklasse ändern.

Fabrik der Fabriken: Große Fabrik, die andere Fabriken herstellt

Wäre es nicht großartig, dasselbe Client-Snippet für verschiedene Arten von Flugzeugen zu verwenden, z. B. eine Boeing 747 oder eine russische MiG-29 ? Wenn sich alle Fabriken, die createEngine() implementieren , auf eine Standardschnittstelle einigen, wird unser Client-Code weiterhin für alle Flugzeugfabriken funktionieren. Aber alle Fabriken müssten sich auf diese gemeinsame Schnittstelle festlegen, deren Methoden sie implementieren werden, und diese vertraute Schnittstelle ist eine abstrakte Fabrik .

Implementierung

Beginnen wir mit einer Schnittstelle, die die Methoden definiert, die verschiedene Flugzeugfabriken implementieren müssen. Der Client-Code wird für diese Schnittstelle geschrieben, aber zur Laufzeit mit einer konkreten Factory komponiert.

Wenn wir uns auf „ Schnittstelle “ beziehen, meinen wir eine Java-Schnittstelle oder eine abstrakte Klasse. In diesem Fall hätten wir eine abstrakte Klasse verwenden können, wenn es Standardimplementierungen für eines der Produkte gegeben hätte. Die create-Methoden geben keine konkreten Produkte zurück – sie geben Schnittstellen zurück, um die Factory-Consumer von der konkreten Implementierung von Teilen zu entkoppeln.

Die formale Definition des abstrakten Fabrikmusters besagt, dass es sich um ein Entwurfsmuster handelt, das eine Schnittstelle zum Erstellen von Familien verwandter Produkte definiert, ohne die konkreten Klassen anzugeben. In diesem Fall ist die IAircraftFactory diese Schnittstelle, und ihre Erstellungsmethoden geben keine wesentlichen Teile zurück, sondern Schnittstellen, die von den Klassen der konkreten Teile implementiert werden.

Als nächstes richten wir die Fabriken für unsere beiden Flugzeuge ein

Eine Betonfabrik wird einen F-16- oder Boeing-spezifischen Motor, ein Cockpit und Flügel produzieren. Jeder Teil hat eine entsprechende Produktschnittstelle, die wir hier der Kürze halber nicht aufführen. Die Produktschnittstellen, die die Teile darstellen, wären:

  • IEngine
  • ICockpit
  • IWings

Im vorherigen Artikel haben wir eine Klasse für F-16 erstellt, die eine fly()- Methode enthält. Diese Methode hat makeF16() aufgerufen , die eine Instanz von F-16 erstellt hat, und dann taxi() aufgerufen . Sobald es auf der Landebahn war, flog es dann weg. Wir können denselben Ansatz auf unser Boeing-747-Beispiel anwenden. Alle Flugzeuge folgen demselben Muster: hergestellt werden, auf der Landebahn rollen und dann wegfliegen. Wir können also eine Klasse für ein Flugzeug erstellen, das diese drei Aufgaben erfüllt. Beachten Sie, dass wir keine separaten Klassen erstellen, um die beiden Flugzeuge darzustellen (dh F-16 und Boeing-747), sondern eine einzige Flugzeugklasse , die beide darstellen kann.

Vorerst lassen wir die Methode makeAircraft leer. Sehen wir uns also zuerst an, wie ein Client F-16- und Boeing-747-Objekte anfordert.

Um unserer Aircraft- Klasse einen Konstruktor hinzuzufügen, müssen wir eine neue Instanzvariable namens factory erstellen. Die Fabrik speichert das Objekt, das für die Herstellung von Flugzeugteilen verantwortlich ist. Wir können ein anderes Flugzeug erhalten, indem wir das Flugzeugobjekt mit einer anderen Fabrik zusammenstellen. Die vollständige Version der Flugzeugklasse ist unten:

Der Client muss die richtige Factory instanziieren, die er an die Aircraft- Klasse übergibt. Die Aircraft-Klasse repräsentiert den Client. Wir hätten eine IAircraft- Schnittstelle erstellen können, um alle Flugzeuge darzustellen, die unsere Fabrik erstellen kann, aber für unser begrenztes Beispiel ist dies unnötig.

Der resultierende Code lässt sich leicht erweitern und modifizieren.

Um unser Beispiel aus dem Musterartikel der Fabrikmethode fortzusetzen , können wir entweder die Klasse F16Factory als Unterklasse verwenden , um Fabriken für die A- und B- Varianten von F-16 zu erstellen, oder sie mit einer Aufzählung parametrisieren, die das Variantenmodell angibt, und den richtigen Teil in einem Schalter zurückgeben Aussage.

Andere Beispiele

  • Das abstrakte Fabrikmuster hilft beim Aufbau von Familien verwandter Produkte. Wenn Ihre Bibliothek beispielsweise ausgefallene UI-Widgets bereitstellt und macOS und Windows unterstützt, benötigen Sie möglicherweise eine Produktfamilie, die auf diesen Plattformen funktioniert. In ähnlicher Weise können in IDEs verwendete Themen ein weiteres Beispiel sein. Wenn Ihre IDE beispielsweise helle und dunkle Designs unterstützt, kann sie das abstrakte Fabrikmuster verwenden, um Widgets zu erstellen, die zum hellen oder dunklen Design gehören, indem sie die konkrete Fabrik variiert, die die Widgets herstellt.
  • javax.xml.parsers.DocumentBuilderFactory.newInstance()wird Ihnen eine Fabrik zurückgeben.
  • javax.xml.transform.TransformerFactory.newInstance()wird Ihnen eine Fabrik zurückgeben.
  • Die Fabrikmethode und das abstrakte Fabrikmuster sind insofern ähnlich, als sie bei der Verwaltung der Objekterstellung helfen. Der Unterschied zwischen den beiden Mustern liegt in ihrer Motivation. Das Fabrikmethodenmuster ist normalerweise für die Erstellung eines einzelnen Produkts verantwortlich, während ein abstraktes Fabrikmuster ganze Familien verwandter Produkte erstellt. Darüber hinaus verwenden wir die Vererbung im Factory-Methodenmuster, um spezialisiertere Produkte zu konstruieren. Schließlich üben wir die Objektkomposition in einem abstrakten Fabrikmuster, indem wir Fabriken übergeben, die verbraucht werden, um die gewünschten Ergebnisse zu erzielen.
  • Wir schaffen eine konkrete Fabrik, wenn wir unsere Flotte um ein neues Flugzeug erweitern. Angenommen, ein Hubschrauber wird der Flotte hinzugefügt und benötigt ein Teil, das ein Flugzeug nicht hat. In diesem Fall müssen wir die IAircraftFactory-Schnittstelle mit einer anderen kreativen Methode für den Teil erweitern, der nur vom Hubschrauber benötigt wird. Dadurch wird die Änderung an vorhandene Fabriken weitergegeben, die null zurückgeben müssen, da die neue Komponente nicht Teil der Jets ist.
  • Betonfabriken werden am besten als Singleton-Objekte implementiert.