Das Wort "Fabrik" ist bei weitem eines der am häufigsten von Programmierern verwendeten, wenn sie über ihre (oder andere) Programme diskutieren. Die darin eingebettete Bedeutung kann jedoch sehr unterschiedlich sein: Es kann sich um eine Klasse handeln, die Objekte generiert (polymorph oder nicht). und eine Methode, die Instanzen eines beliebigen Typs (statisch oder nicht) erstellt; es passiert und sogar jede Generierungsmethode (einschließlich
Konstruktoren ).
Natürlich kann nicht alles, was Instanzen von irgendetwas erzeugt, als "Fabrik" bezeichnet werden. Darüber hinaus können unter diesem Wort zwei verschiedene generative Muster aus dem Arsenal der Viererbande verborgen werden - die
„Fabrikmethode“ und die
„abstrakte Fabrik“ , deren Details ich gerne etwas näher erläutern möchte, wobei ich besonders auf ihr klassisches Verständnis und ihre Umsetzung achte.
Und ich wurde inspiriert, diesen Aufsatz von
Joshua Kerivsky (Leiter von
„Industrial Logic“ ) oder vielmehr seinem Buch
„Refactoring to Patterns“ zu schreiben, das zu Beginn des Jahrhunderts als Teil einer Reihe von Büchern veröffentlicht wurde, die von
Martin Fowler (einem berühmten Autor des modernen Programmierklassikers - dem Buch
„ Refactoring " ). Wenn jemand das erste nicht gelesen oder gar gehört hat (und ich kenne viele davon), fügen Sie es unbedingt Ihrer Leseliste hinzu. Dies ist eine würdige Fortsetzung von Refactoring und einem noch klassischeren Buch,
Objective Design Techniques. Entwurfsmuster .
"Das Buch enthält unter anderem Dutzende von Rezepten, um verschiedene
"Gerüche" im Code mithilfe von
Designmustern zu entfernen . Einschließlich drei (mindestens) „Rezepte“ zum diskutierten Thema.
Abstrakte Fabrik
Kerivsky gibt in seinem Buch zwei Fälle an, in denen die Verwendung dieser Vorlage nützlich sein wird.
Die erste ist die
Kapselung von Wissen über bestimmte Klassen, die durch eine gemeinsame Schnittstelle verbunden sind. In diesem Fall verfügt nur der Typ, der das Werk ist, über dieses Wissen.
Die öffentliche
API der Factory besteht aus einer Reihe von Methoden (statisch oder nicht), die Instanzen eines gemeinsamen Schnittstellentyps zurückgeben und einige "sprechende" Namen haben (um zu verstehen, welche Methode für einen bestimmten Zweck aufgerufen werden muss).
Das zweite Beispiel ist dem ersten sehr ähnlich (und im Allgemeinen sind alle Szenarien für die Verwendung des Musters einander mehr oder weniger ähnlich). Dies ist der Fall, wenn Instanzen eines oder mehrerer Typen derselben Gruppe an verschiedenen Stellen im Programm erstellt werden. In diesem Fall kapselt die Factory erneut das Wissen über den Code, der die Instanzen erstellt, jedoch mit einer etwas anderen Motivation. Dies gilt beispielsweise insbesondere dann, wenn das Erstellen von Instanzen dieser Typen komplex ist und nicht auf den Aufruf des Konstruktors beschränkt ist.
Um dem Thema Entwicklung unter
"iOS" näher zu kommen, ist es zweckmäßig, Unterklassen von
UIViewController
zu üben. In der Tat ist dies definitiv einer der häufigsten Typen in der "iOS" -Entwicklung, es wird fast immer vor der Verwendung "vererbt", und eine bestimmte Unterklasse ist für Client-Code oft nicht einmal wichtig.
Ich werde versuchen, die Codebeispiele so nah wie möglich an der klassischen Implementierung aus dem Gang of Four-Buch zu halten, aber im wirklichen Leben wird der Code oft auf die eine oder andere Weise vereinfacht. Und nur ein ausreichendes Verständnis der Vorlage öffnet die Tür zu einer freieren Nutzung.
Detailliertes Beispiel
Angenommen, wir handeln mit Fahrzeugen in einer Anwendung, und die Zuordnung hängt vom Typ des jeweiligen Fahrzeugs ab: Wir verwenden verschiedene Unterklassen von
UIViewController
für verschiedene Fahrzeuge. Außerdem unterscheiden sich alle Fahrzeuge im Zustand (neu und gebraucht):
enum VehicleCondition{ case new case used } final class BicycleViewController: UIViewController { private let condition: VehicleCondition init(condition: VehicleCondition) { self.condition = condition super.init(nibName: nil, bundle: nil) } required init?(coder aDecoder: NSCoder) { fatalError("BicycleViewController: init(coder:) has not been implemented.") } } final class ScooterViewController: UIViewController { private let condition: VehicleCondition init(condition: VehicleCondition) { self.condition = condition super.init(nibName: nil, bundle: nil) } required init?(coder aDecoder: NSCoder) { fatalError("ScooterViewController: init(coder:) has not been implemented.") } }
Wir haben also eine Familie von Objekten derselben Gruppe, deren Instanzen je nach Bedingung an denselben Stellen erstellt werden (z. B. wenn der Benutzer auf ein Produkt in der Liste geklickt hat und je nachdem, ob es sich um einen Roller oder ein Fahrrad handelt) Erstellen Sie den entsprechenden Controller. Controller-Konstruktoren verfügen über einige Parameter, die ebenfalls jedes Mal festgelegt werden müssen. Sind diese beiden Argumente für die Schaffung einer "Fabrik", die allein die Logik zur Schaffung des richtigen Controllers kennt?
Natürlich ist das Beispiel recht einfach, und in einem realen Projekt in einem ähnlichen Fall wird die Einführung einer „Fabrik“ explizit als
„Überentwicklung“ bezeichnet . Wenn wir uns jedoch vorstellen, dass wir nicht zwei Fahrzeugtypen haben und die Konstrukteure mehr als einen Parameter haben, werden die Vorteile der „Fabrik“ offensichtlicher.
Deklarieren wir also eine Schnittstelle, die die Rolle einer "abstrakten Fabrik" spielt:
protocol VehicleViewControllerFactory { func makeBicycleViewController() -> UIViewController func makeScooterViewController() -> UIViewController }
(Eine ziemlich kurze
„Richtlinie“ zum Entwerfen einer „API“ in
Swift empfiehlt, Factory-Methoden aufzurufen, die mit dem Wort make beginnen.)
(Ein Beispiel im Buch der vierköpfigen Banden findet sich in
„C ++“ und basiert auf
Vererbung und
„virtuellen“ Funktionen . Mit „Swift“ sind wir dem protokollorientierten Programmierparadigma natürlich näher gekommen.)
Die abstrakte Factory-Oberfläche enthält nur zwei Methoden: Erstellen von Controllern für den Verkauf von Fahrrädern und Rollern. Methoden geben Instanzen nicht bestimmter Unterklassen zurück, sondern einer gemeinsamen Basisklasse. Daher ist der Wissensumfang über bestimmte Typen auf den Bereich beschränkt, in dem er wirklich notwendig ist.
Als „konkrete Fabriken“ werden wir zwei Implementierungen der abstrakten Factory-Schnittstelle verwenden:
struct NewVehicleViewControllerFactory: VehicleViewControllerFactory { func makeBicycleViewController() -> UIViewController { return BicycleViewController(condition: .new) } func makeScooterViewController() -> UIViewController { return ScooterViewController(condition: .new) } } struct UsedVehicleViewControllerFactory: VehicleViewControllerFactory { func makeBicycleViewController() -> UIViewController { return BicycleViewController(condition: .used) } func makeScooterViewController() -> UIViewController { return ScooterViewController(condition: .used) } }
In diesem Fall sind, wie aus dem Code hervorgeht, bestimmte Fabriken für Fahrzeuge mit unterschiedlichen Bedingungen (neu und gebraucht) verantwortlich.
Das Erstellen des richtigen Controllers sieht nun ungefähr so aus:
let factory: VehicleViewControllerFactory = NewVehicleViewControllerFactory() let vc = factory.makeBicycleViewController()
Werkseitig gekapselte Klassen
Gehen Sie nun kurz auf die Anwendungsfälle ein, die Kerivsky in seinem Buch anbietet.
Der erste Fall bezieht sich auf die
Kapselung bestimmter Klassen . Nehmen Sie zum Beispiel dieselben Steuerungen zum Anzeigen von Daten über Fahrzeuge:
final class BicycleViewController: UIViewController { } final class ScooterViewController: UIViewController { }
Angenommen, es handelt sich um ein separates Modul, beispielsweise eine Plug-In-Bibliothek. In diesem Fall bleiben die oben deklarierten Klassen (standardmäßig)
internal
, und die Factory
internal
als öffentliche „API“ der Bibliothek, die in ihren Methoden die Basisklassen der Controller zurückgibt und so Kenntnisse über bestimmte Unterklassen in der Bibliothek hinterlässt:
public struct VehicleViewControllerFactory { func makeBicycleViewController() -> UIViewController { return BicycleViewController() } func makeScooterViewController() -> UIViewController { return ScooterViewController() } }
Bewegen von Wissen über das Erstellen eines Objekts innerhalb einer Fabrik
Der zweite „Fall“ beschreibt die
komplexe Initialisierung des Objekts , und Kerivsky schlägt als eine der Möglichkeiten zur Vereinfachung des Codes und zum Schutz der Kapselungsprinzipien vor, die Verbreitung von Wissen über den Initialisierungsprozess außerhalb der Fabrik einzuschränken.
Angenommen, wir wollten gleichzeitig Autos verkaufen. Und dies ist zweifellos eine komplexere Technik mit einer größeren Anzahl von Merkmalen. Zum Beispiel beschränken wir uns auf die Art des verwendeten Kraftstoffs, die Art des Getriebes und die Größe der Felge:
enum Condition { case new case used } enum EngineType { case diesel case gas } struct Engine { let type: EngineType } enum TransmissionType { case automatic case manual } final class CarViewController: UIViewController { private let condition: Condition private let engine: Engine private let transmission: TransmissionType private let wheelDiameter: Int init(engine: Engine, transmission: TransmissionType, wheelDiameter: Int = 16, condition: Condition = .new) { self.engine = engine self.transmission = transmission self.wheelDiameter = wheelDiameter self.condition = condition super.init(nibName: nil, bundle: nil) } required init?(coder aDecoder: NSCoder) { fatalError("CarViewController: init(coder:) has not been implemented.") } }
Ein Beispiel für die Initialisierung des entsprechenden Controllers:
let engineType = EngineType.diesel let engine = Engine(type: engineType) let transmission = TransmissionType.automatic let wheelDiameter = 18 let vc = CarViewController(engine: engine, transmission: transmission, wheelDiameter: wheelDiameter)
Wir können die Verantwortung für all diese "kleinen Dinge" auf die "Schultern" einer spezialisierten Fabrik legen:
struct UsedCarViewControllerFactory { let engineType: EngineType let transmissionType: TransmissionType let wheelDiameter: Int func makeCarViewController() -> UIViewController { let engine = Engine(type: engineType) return CarViewController(engine: engine, transmission: transmissionType, wheelDiameter: wheelDiameter, condition: .used) } }
Und erstellen Sie den Controller folgendermaßen:
let factory = UsedCarViewControllerFactory(engineType: .gas, transmissionType: .manual, wheelDiameter: 17) let vc = factory.makeCarViewController()
Fabrikmethode
Die zweite "Single-Root" -Vorlage kapselt auch Wissen über bestimmte generierte Typen, jedoch nicht durch Verstecken dieses Wissens innerhalb einer speziellen Klasse, sondern durch Polymorphismus. Kerivsky gibt in seinem Buch Beispiele in
Java und schlägt vor,
abstrakte Klassen zu verwenden , aber die Bewohner des Swift-Universums sind mit diesem Konzept nicht vertraut. Wir haben hier unsere eigene Atmosphäre ... und Protokolle.
Das Buch "Gangs of Four" berichtet, dass die Vorlage auch als "virtueller Konstruktor" bekannt ist, und dies ist nicht umsonst. In "C ++" ist virtuell eine Funktion, die in abgeleiteten Klassen neu definiert wird. Die Sprache gibt dem Designer nicht die Möglichkeit, virtuell zu deklarieren, und es ist möglich, dass es ein Versuch war, das gewünschte Verhalten nachzuahmen, das zur Erfindung dieses Musters führte.
Polymorphe Objekterstellung
Als klassisches Beispiel für die Nützlichkeit der Vorlage betrachten wir den Fall, dass
in der Hierarchie verschiedene Typen die identische Implementierung einer Methode haben, mit Ausnahme des Objekts, das in dieser Methode erstellt und verwendet wird . Als Lösung wird vorgeschlagen, dieses Objekt in einer separaten Methode zu erstellen und separat zu implementieren und die allgemeine Methode in der Hierarchie höher anzuheben. Daher verwenden verschiedene Typen die allgemeine Implementierung der Methode, und das für diese Methode erforderliche Objekt wird polymorph erstellt.
Kehren wir zum Beispiel zu unseren Steuerungen zurück, um Fahrzeuge anzuzeigen:
final class BicycleViewController: UIViewController { } final class ScooterViewController: UIViewController { }
Angenommen, eine bestimmte Entität wird verwendet, um sie anzuzeigen, z. B. ein
Koordinator , der diese Controller modal von einem anderen Controller darstellt:
protocol Coordinator { var presentingViewController: UIViewController? { get set } func start() }
Die
start()
-Methode wird immer auf die gleiche Weise verwendet, außer dass unterschiedliche Controller erstellt werden:
final class BicycleCoordinator: Coordinator { weak var presentingViewController: UIViewController? func start() { let vc = BicycleViewController() presentingViewController?.present(vc, animated: true) } } final class ScooterCoordinator: Coordinator { weak var presentingViewController: UIViewController? func start() { let vc = ScooterViewController() presentingViewController?.present(vc, animated: true) } }
Die vorgeschlagene Lösung besteht darin, das verwendete Objekt in einer separaten Methode zu erstellen:
protocol Coordinator { var presentingViewController: UIViewController? { get set } func start() func makeViewController() -> UIViewController }
Die Hauptmethode besteht darin, die grundlegende Implementierung bereitzustellen:
extension Coordinator { func start() { let vc = makeViewController() presentingViewController?.present(vc, animated: true) } }
Bestimmte Typen haben in diesem Fall die Form:
final class BicycleCoordinator: Coordinator { weak var presentingViewController: UIViewController? func makeViewController() -> UIViewController { return BicycleViewController() } } final class ScooterCoordinator: Coordinator { weak var presentingViewController: UIViewController? func makeViewController() -> UIViewController { return ScooterViewController() } }
Fazit
Ich habe versucht, dieses einfache Thema zu behandeln, indem ich drei Ansätze kombiniert habe:
- die klassische Erklärung der Existenz der Rezeption, inspiriert vom Buch „Gangs of Four“;
- Gebrauchsmotivation, offen inspiriert von Kerivskys Buch;
- Angewandte Anwendung als Beispiel für eine Programmierbranche in meiner Nähe.
Gleichzeitig habe ich versucht, der Lehrbuchstruktur der Vorlagen so nahe wie möglich zu kommen, ohne die Prinzipien des modernen Entwicklungsansatzes für das iOS-System zu zerstören und die Funktionen der Swift-Sprache (anstelle des allgemeineren C ++ und Java) zu nutzen.
Wie sich herausstellte, ist es ziemlich schwierig, detaillierte Materialien zu diesem Thema zu finden, die angewandte Beispiele enthalten. Die meisten vorhandenen Artikel und Handbücher enthalten nur oberflächliche Rezensionen und gekürzte Beispiele, die im Vergleich zu den Lehrbuchversionen von Implementierungen bereits ziemlich abgeschnitten sind.
Ich hoffe, dass ich zumindest teilweise meine Ziele erreichen konnte und der Leser - zumindest teilweise - interessiert oder zumindest neugierig war, mein Wissen zu diesem Thema zu lernen oder aufzufrischen.
Meine anderen Materialien zu Designmustern:Und dies ist ein Link zu meinem „Twitter“, wo ich Links zu meinen Aufsätzen und etwas mehr veröffentliche.