Ich habe die Geschichte mehrerer C # - und Java-Klassenbibliotheksprojekte auf GitHub und CodePlex gesehen und sehe einen Trend zum Umstieg auf Factory-Klassen im Gegensatz zur direkten Objektinstanziierung.
Warum sollte ich viele Factory-Klassen verwenden? Ich habe eine ziemlich gute Bibliothek, in der Objekte auf altmodische Weise erstellt werden - durch Aufruf öffentlicher Konstruktoren von Klassen. In den letzten Commits haben die Autoren schnell alle öffentlichen Konstruktoren von Tausenden von Klassen in interne geändert und außerdem eine große Factory-Klasse mit Tausenden von CreateXXX
statischen Methoden erstellt, die nur neue Objekte zurückgeben, indem sie die internen Konstruktoren der Klassen aufrufen. Die externe Projekt-API ist fehlerhaft, gut gemacht.
Warum wäre eine solche Änderung sinnvoll? Was bringt es, auf diese Weise umzugestalten? Welche Vorteile bietet es, Aufrufe an Konstruktoren öffentlicher Klassen durch statische Factory-Methodenaufrufe zu ersetzen?
Wann sollte ich öffentliche Konstruktoren verwenden und wann sollten Fabriken verwendet werden?
Antworten:
Factory-Klassen werden häufig implementiert, weil sie es dem Projekt ermöglichen, die SOLID- Prinzipien genauer zu befolgen . Insbesondere die Prinzipien der Grenzflächensegregation und der Abhängigkeitsinversion.
Fabriken und Schnittstellen ermöglichen eine viel größere langfristige Flexibilität. Es ermöglicht ein entkoppeltes - und damit überprüfbareres - Design. Hier ist eine nicht vollständige Liste, warum Sie diesen Weg gehen könnten:
Betrachten Sie diese Situation.
Assembly A (-> bedeutet abhängig von):
Ich möchte Klasse B in Assembly B verschieben, die von Assembly A abhängig ist. Mit diesen konkreten Abhängigkeiten muss ich den größten Teil meiner gesamten Klassenhierarchie verschieben. Wenn ich Schnittstellen benutze, kann ich viel Schmerz vermeiden.
Versammlung A:
Ich kann jetzt die Klasse B ohne jegliche Schmerzen auf die Baugruppe B verschieben. Es kommt immer noch auf die Schnittstellen in Assembly A an.
Die Verwendung eines IoC-Containers zum Auflösen Ihrer Abhängigkeiten bietet Ihnen noch mehr Flexibilität. Es ist nicht erforderlich, jeden Aufruf des Konstruktors zu aktualisieren, wenn Sie die Abhängigkeiten der Klasse ändern.
Nach dem Prinzip der Schnittstellentrennung und dem Prinzip der Abhängigkeitsinversion können hochflexible, entkoppelte Anwendungen erstellt werden. Sobald Sie an einer dieser Arten von Anwendungen gearbeitet haben, werden Sie nie mehr auf die Verwendung des
new
Schlüsselworts zurückgreifen wollen .quelle
Wie der Name schon sagt, glaube ich, dass dies ein Fall von Frachtkult- Softwaredesign ist. Factories, insbesondere die abstrakte, können nur verwendet werden, wenn Ihr Modul mehrere Instanzen einer Klasse erstellt und Sie dem Benutzer dieses Moduls die Möglichkeit geben möchten, den zu erstellenden Typ anzugeben. Diese Anforderung ist eigentlich recht selten, da Sie die meiste Zeit nur eine Instanz benötigen und diese Instanz auch direkt übergeben können, anstatt eine explizite Factory zu erstellen.
Fakt ist, dass Fabriken (und Singletons) extrem einfach zu implementieren sind und die Leute sie daher häufig verwenden, auch an Orten, an denen sie nicht benötigt werden. Also, wenn Programmierer denkt "Welche Entwurfsmuster sollte ich in diesem Code verwenden?" Die Fabrik ist die erste, die ihm in den Sinn kommt.
Viele Fabriken entstehen, weil "Vielleicht muss ich diese Klassen eines Tages anders erstellen". Welches ist eine klare Verletzung von YAGNI .
Und Fabriken werden obsolet, wenn Sie das IoC-Framework einführen, da IoC nur eine Art Fabrik ist. Viele IoC-Frameworks sind in der Lage, Implementierungen bestimmter Fabriken zu erstellen.
Außerdem gibt es kein Entwurfsmuster, das besagt, dass riesige statische Klassen mit
CreateXXX
Methoden erstellt werden sollen, die nur Konstruktoren aufrufen. Und es wird vor allem keine Fabrik (oder abstrakte Fabrik) genannt.quelle
Die Fabrikmuster Mode stammt von einem fast dogmatischen Glauben unter den Programmierer in „C-Stil“ Sprachen (C / C ++, C #, Java), dass die Verwendung der „neuen“ Schlüsselwort ist schlecht, und sollte unter allen Umständen vermieden werden (oder zumin dest zentralisiert). Dies wiederum kommt aus einer ultra-strengen Auslegung der einheitlichen Prinzip Verantwortung (die „S“ von SOLID) und auch der Dependency Inversion Principle ( „D“). Einfach gesagt, sagt der SRP, dass im Idealfall ein Codeobjekt einen „Grund zu ändern“ haben sollte, und man nur; dass „Grund zu ändern“ ist der zentrale Zweck dieses Objekts, seine „Verantwortung“ in der Codebasis, und alles andere, was eine Änderung Code erfordert nicht Öffnung benötigen bis diese Klassendatei. Das DIP ist noch einfacher; ein Codeobjekt soll nie auf ein anderes konkretes Objekt abhängig sein,
Typischer Fall, durch „neue“ und einen öffentlichen Konstruktor verwenden, Sie koppeln den anrufenden Code auf eine bestimmte Bauweise einer bestimmten konkreten Klasse. Ihr Code muss jetzt wissen, dass eine Klasse MyFooObject existiert und hat einen Konstruktor, der einen String und einen int nimmt. Wenn das Konstruktor immer mehr Informationen benötigt, müssen alle Verwendungen des Konstrukteurs aktualisiert werden in diesen Informationen weitergeben, einschließlich der man jetzt Sie schreiben, und deshalb sind sie verpflichtet, etwas Gültigkeit zu haben, in übergeben, und so müssen sie entweder es oder geändert werden, um es (mehr Verantwortung an die raubend Objekte hinzufügen). Darüber hinaus müssen, wenn MyFooObject jemals in der Codebasis von BetterFooObject ersetzt wird, werden alle Verwendungen der alten Klasse ändern das neue Objekt anstelle der alten zu errichten.
Stattdessen sollten alle Benutzer von MyFooObject direkt von "IFooObject" abhängig sein, das das Verhalten der Implementierung von Klassen einschließlich MyFooObject definiert. Jetzt können Benutzer von IFooObjects nicht nur ein IFooObject erstellen (ohne zu wissen, dass eine bestimmte konkrete Klasse ein IFooObject ist, das sie nicht benötigen), sondern müssen stattdessen eine Instanz einer IFooObject-implementierenden Klasse oder Methode erhalten von außen durch ein anderes Objekt, das dafür verantwortlich ist, das richtige IFooObject für den Umstand zu erstellen, der in unserer Sprache normalerweise als Factory bezeichnet wird.
Hier trifft Theorie auf Realität. Ein Objekt kann niemals für alle Arten von Änderungen geschlossen werden. Beispiel: IFooObject ist jetzt ein zusätzliches Codeobjekt in der Codebasis, das geändert werden muss, wenn sich die von Verbrauchern oder Implementierungen von IFooObjects benötigte Schnittstelle ändert. Dies führt zu einer neuen Komplexität, die die Art und Weise beeinflusst, wie Objekte in dieser Abstraktion miteinander interagieren. Darüber hinaus müssen die Verbraucher noch tiefgreifender Änderungen vornehmen, wenn die Schnittstelle selbst durch eine neue ersetzt wird.
Ein guter Programmierer weiß, wie er YAGNI ("Du wirst es nicht brauchen") mit SOLID in Einklang bringt, indem er das Design analysiert und Orte findet, bei denen es sehr wahrscheinlich ist, dass sie sich auf eine bestimmte Weise ändern, und sie überarbeitet, um toleranter zu sein diese Art von Veränderung, denn in diesem Fall "wirst du es brauchen".
quelle
Foo
, einen öffentlichen Konstruktor anzugeben, der zum Erstellen vonFoo
Instanzen oder zum Erstellen anderer Typen innerhalb desselben Pakets / derselben Assembly verwendet werden kann , der jedoch nicht zum Erstellen von verwendet werden kann anderswo abgeleitete Typen. Ich kenne keinen besonders zwingenden Grund, warum eine Sprache / ein Framework keine separaten Konstruktoren zur Verwendung innew
Ausdrücken definieren kann, im Gegensatz zum Aufruf von Subtypkonstruktoren, aber ich kenne keine Sprachen, die diese Unterscheidung treffen.Konstruktoren sind in Ordnung, wenn sie kurzen, einfachen Code enthalten.
Wenn die Initialisierung mehr als die Zuweisung einiger Variablen zu den Feldern ist, ist eine Factory sinnvoll. Hier sind einige der Vorteile:
Langer, komplizierter Code ist in einer dedizierten Klasse (einer Factory) sinnvoller. Wenn derselbe Code in einem Konstruktor abgelegt wird, der eine Reihe statischer Methoden aufruft, wird die Hauptklasse verschmutzt.
In einigen Sprachen und in einigen Fällen ist das Auslösen von Ausnahmen in Konstruktoren eine wirklich schlechte Idee , da dies zu Fehlern führen kann.
Wenn Sie einen Konstruktor aufrufen, müssen Sie als Aufrufer den genauen Typ der zu erstellenden Instanz kennen. Dies ist nicht immer der Fall (als
Feeder
muss ich nur das konstruieren,Animal
um es zu füttern; es ist mir egal, ob es einDog
oder ein istCat
).quelle
Feeder
möglicherweise keine der beiden Methoden und ruft stattdessenKennel
diegetHungryAnimal
Methode des Objekts auf .Builder Pattern
. Ist es nichtWenn Sie mit Schnittstellen arbeiten, können Sie unabhängig von der tatsächlichen Implementierung bleiben. Eine Factory kann konfiguriert werden (über Eigenschaften, Parameter oder eine andere Methode), um eine von mehreren verschiedenen Implementierungen zu instanziieren.
Ein einfaches Beispiel: Sie möchten mit einem Gerät kommunizieren, wissen aber nicht, ob dies über Ethernet, COM oder USB erfolgen soll. Sie definieren eine Schnittstelle und 3 Implementierungen. Zur Laufzeit können Sie dann die gewünschte Methode auswählen und die Factory gibt Ihnen die entsprechende Implementierung.
Benutze es oft ...
quelle
Dies ist ein Symptom für eine Einschränkung in Java / C # -Modulsystemen.
Grundsätzlich gibt es keinen Grund, eine Implementierung einer Klasse mit denselben Konstruktor- und Methodensignaturen gegen eine andere auszutauschen. Es gibt Sprachen, die dies erlauben. Java und C # bestehen jedoch darauf, dass jede Klasse einen eindeutigen Bezeichner (den vollständig qualifizierten Namen) hat und der Clientcode eine fest codierte Abhängigkeit davon aufweist.
Sie können dies irgendwie umgehen, indem Sie mit den Dateisystem- und Compileroptionen experimentieren, sodass eine
com.example.Foo
Zuordnung zu einer anderen Datei möglich ist. Dies ist jedoch überraschend und nicht intuitiv. Selbst wenn Sie dies tun, ist Ihr Code immer noch an nur eine Implementierung der Klasse gebunden. Dh, wenn Sie eine Klasse schreiben, die von einer KlasseFoo
abhängtMySet
, können SieMySet
zur Kompilierungszeit eine Implementierung von auswählen , aber Sie könnenFoo
s immer noch nicht mit zwei verschiedenen Implementierungen von instanziierenMySet
.Diese unglückliche Designentscheidung zwingt die Leute dazu,
interface
s unnötig zu verwenden, um ihren Code gegen die Möglichkeit, dass sie später eine andere Implementierung von etwas benötigen, zukunftssicher zu machen oder Unit-Tests zu ermöglichen. Das ist nicht immer machbar; Wenn Sie Methoden haben, die die privaten Felder von zwei Instanzen der Klasse untersuchen, können Sie diese nicht in einer Schnittstelle implementieren. Das ist der Grund, warum Sie zum Beispiel nichtunion
in JavasSet
Benutzeroberfläche sehen. Abgesehen von numerischen Typen und Auflistungen sind binäre Methoden jedoch nicht üblich, sodass Sie in der Regel davonkommen können.Wenn Sie aufrufen, haben
new Foo(...)
Sie natürlich immer noch eine Abhängigkeit von der Klasse . Sie benötigen also eine Factory, wenn eine Klasse eine Schnittstelle direkt instanziieren soll. In der Regel ist es jedoch besser, die Instanz im Konstruktor zu akzeptieren und eine andere Person entscheiden zu lassen, welche Implementierung verwendet werden soll.Es liegt an Ihnen, zu entscheiden, ob es sich lohnt, Ihre Codebasis mit Schnittstellen und Fabriken aufzublähen. Wenn die betreffende Klasse in Ihrer Codebasis enthalten ist, ist es zum einen trivial, den Code so umzugestalten, dass er in Zukunft eine andere Klasse oder ein anderes Interface verwendet. Sie können YAGNI später aufrufen und umgestalten, wenn die Situation eintritt. Wenn die Klasse jedoch Teil der öffentlichen API einer von Ihnen veröffentlichten Bibliothek ist, können Sie den Clientcode nicht korrigieren. Wenn Sie eine nicht verwenden
interface
und später mehrere Implementierungen benötigen, stecken Sie zwischen einem Stein und einem harten Ort fest.quelle
new
wären ansonsten einfach syntaktischer Zucker für den Aufruf einer speziell benannten statischen Methode (die automatisch generiert würde, wenn ein Typ einen "öffentlichen Konstruktor" hätte "aber die Methode nicht explizit einbinden). IMHO, wenn Code nur eine langweilige Standard-Implementierung haben möchteList
, sollte es der Schnittstelle möglich sein, eine solche zu erstellen, ohne dass der Client eine bestimmte Implementierung kennen muss (zArrayList
. B. ).Meiner Meinung nach verwenden sie nur die einfache Fabrik, die kein geeignetes Entwurfsmuster darstellt und nicht mit der abstrakten Fabrik oder der Fabrikmethode verwechselt werden sollte.
Und da sie eine "riesige Fabric-Klasse mit Tausenden von statischen CreateXXX-Methoden" erstellt haben, klingt dies nach einem Anti-Pattern (vielleicht nach einer God-Klasse?).
Ich denke, dass die Simple Factory und die statischen Erstellermethoden (für die keine externe Klasse erforderlich ist) in einigen Fällen nützlich sein können. Zum Beispiel, wenn für die Konstruktion eines Objekts verschiedene Schritte erforderlich sind, z. B. das Instanziieren anderer Objekte (z. B. Begünstigen der Komposition).
Ich würde das nicht einmal als Factory bezeichnen, sondern nur eine Reihe von Methoden, die in einer zufälligen Klasse mit dem Suffix "Factory" gekapselt sind.
quelle
int x
undIFooService fooService
. Sie möchten nichtfooService
überall herumlaufen, also erstellen Sie eine Fabrik mit einer MethodeCreate(int x)
und fügen den Service in die Fabrik ein.IFactory
überall rumlaufen anstattIFooService
.Wenn die Bibliothek als Benutzer einer Bibliothek über Factory-Methoden verfügt, sollten Sie diese verwenden. Sie gehen davon aus, dass die Factory-Methode dem Autor der Bibliothek die Flexibilität gibt, bestimmte Änderungen vorzunehmen, ohne dass sich dies auf Ihren Code auswirkt. Sie können beispielsweise eine Instanz einer Unterklasse in einer Factory-Methode zurückgeben, die mit einem einfachen Konstruktor nicht funktioniert.
Als Ersteller einer Bibliothek würden Sie Factory-Methoden verwenden, wenn Sie diese Flexibilität selbst nutzen möchten.
In dem Fall, den Sie beschreiben, scheinen Sie den Eindruck zu haben, dass das Ersetzen der Konstruktoren durch Factory-Methoden einfach sinnlos war. Es war sicherlich ein Schmerz für alle Beteiligten; Eine Bibliothek sollte nichts ohne guten Grund aus ihrer API entfernen. Wenn ich also Factory-Methoden hinzugefügt hätte, hätte ich vorhandene Konstruktoren verfügbar gelassen, möglicherweise veraltet, bis eine Factory-Methode diesen Konstruktor nicht mehr nur aufruft und Code mit dem einfachen Konstruktor weniger gut funktioniert, als er sollte. Ihr Eindruck könnte sehr gut richtig sein.
quelle
Dies scheint im Zeitalter von Scala und funktionaler Programmierung überholt zu sein. Ein solides Funktionsfundament ersetzt eine Unmenge von Klassen.
Auch zu beachten, dass Java Double
{{
nicht mehr funktioniert, wenn eine Fabrik verwendet wird, dhDamit können Sie eine anonyme Klasse erstellen und anpassen.
Im Zeit-Raum-Dilema ist der Zeitfaktor dank schneller CPUs jetzt so stark geschrumpft, dass eine funktionale Programmierung, die das Schrumpfen des Raums bzw. der Codegröße ermöglicht, jetzt praktisch ist.
quelle