Es gibt eine Sache in C ++, die mich lange Zeit unwohl gefühlt hat, weil ich ehrlich gesagt nicht weiß, wie ich das machen soll, obwohl es einfach klingt:
Wie implementiere ich die Factory-Methode in C ++ korrekt?
Ziel: Ermöglichen, dass der Client ein Objekt mithilfe von Factory-Methoden anstelle der Konstruktoren des Objekts instanziieren kann, ohne inakzeptable Konsequenzen und Leistungseinbußen.
Mit "Factory-Methodenmuster" meine ich sowohl statische Factory-Methoden innerhalb eines Objekts oder Methoden, die in einer anderen Klasse definiert sind, als auch globale Funktionen. Nur allgemein "das Konzept, die normale Art der Instanziierung der Klasse X an einen anderen Ort als den Konstruktor umzuleiten".
Lassen Sie mich einige mögliche Antworten durchgehen, an die ich gedacht habe.
0) Machen Sie keine Fabriken, machen Sie Konstruktoren.
Das klingt gut (und ist oft die beste Lösung), ist aber kein allgemeines Mittel. Erstens gibt es Fälle, in denen die Objektkonstruktion eine Aufgabe ist, die komplex genug ist, um ihre Extraktion in eine andere Klasse zu rechtfertigen. Aber selbst diese Tatsache beiseite zu legen, reicht selbst für einfache Objekte, die nur Konstruktoren verwenden, oft nicht aus.
Das einfachste Beispiel, das ich kenne, ist eine 2-D-Vektorklasse. So einfach und doch so knifflig. Ich möchte es sowohl aus kartesischen als auch aus Polarkoordinaten konstruieren können. Offensichtlich kann ich nicht tun:
struct Vec2 {
Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!
// ...
};
Meine natürliche Denkweise ist dann:
struct Vec2 {
static Vec2 fromLinear(float x, float y);
static Vec2 fromPolar(float angle, float magnitude);
// ...
};
Was mich anstelle von Konstruktoren zur Verwendung statischer Factory-Methoden führt ... was im Wesentlichen bedeutet, dass ich das Factory-Muster auf irgendeine Weise implementiere ("die Klasse wird zu einer eigenen Factory"). Das sieht gut aus (und würde zu diesem speziellen Fall passen), schlägt aber in einigen Fällen fehl, was ich in Punkt 2 beschreiben werde. Lesen Sie weiter.
Ein anderer Fall: Der Versuch, einige APIs (z. B. GUIDs nicht verwandter Domänen oder eine GUID und ein Bitfeld) durch zwei undurchsichtige Typedefs zu überladen, weist semantisch völlig unterschiedliche Typen auf (also theoretisch gültige Überladungen), die sich jedoch tatsächlich als die herausstellen das Gleiche - wie vorzeichenlose Ints oder leere Zeiger.
1) Der Java-Weg
Java hat es einfach, da wir nur dynamisch zugewiesene Objekte haben. Eine Fabrik zu bauen ist so trivial wie:
class FooFactory {
public Foo createFooInSomeWay() {
// can be a static method as well,
// if we don't need the factory to provide its own object semantics
// and just serve as a group of methods
return new Foo(some, args);
}
}
In C ++ bedeutet dies:
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
};
Cool? In der Tat oft. Dies zwingt den Benutzer jedoch dazu, nur die dynamische Zuordnung zu verwenden. Die statische Zuordnung macht C ++ komplex, macht es aber auch oft leistungsfähig. Ich glaube auch, dass es einige Ziele gibt (Schlüsselwort: eingebettet), die keine dynamische Zuordnung ermöglichen. Und das bedeutet nicht, dass die Benutzer dieser Plattformen gerne sauberes OOP schreiben.
Abgesehen von der Philosophie: Im Allgemeinen möchte ich die Benutzer der Fabrik nicht dazu zwingen, sich auf die dynamische Zuordnung zu beschränken.
2) Rückgabe nach Wert
OK, wir wissen also, dass 1) cool ist, wenn wir eine dynamische Zuordnung wünschen. Warum fügen wir nicht zusätzlich eine statische Zuordnung hinzu?
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooInSomeWay() {
return Foo(some, args);
}
};
Was? Wir können nicht durch den Rückgabetyp überladen? Oh, natürlich können wir nicht. Ändern wir also die Methodennamen, um dies widerzuspiegeln. Und ja, ich habe das obige ungültige Codebeispiel geschrieben, um zu betonen, wie sehr ich die Notwendigkeit, den Methodennamen zu ändern, nicht mag, zum Beispiel, weil wir ein sprachunabhängiges Factory-Design jetzt nicht richtig implementieren können, da wir Namen ändern müssen - und Jeder Benutzer dieses Codes muss sich an den Unterschied zwischen der Implementierung und der Spezifikation erinnern.
class FooFactory {
public:
Foo* createDynamicFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooObjectInSomeWay() {
return Foo(some, args);
}
};
OK ... da haben wir es. Es ist hässlich, da wir den Methodennamen ändern müssen. Es ist unvollkommen, da wir denselben Code zweimal schreiben müssen. Aber sobald es fertig ist, funktioniert es. Recht?
Normalerweise. Aber manchmal nicht. Beim Erstellen von Foo sind wir tatsächlich darauf angewiesen, dass der Compiler die Rückgabewertoptimierung für uns durchführt, da der C ++ - Standard so gut ist, dass die Compilerhersteller nicht angeben können, wann das Objekt an Ort und Stelle erstellt und wann es bei der Rückgabe von a kopiert wird temporäres Objekt nach Wert in C ++. Wenn das Kopieren von Foo teuer ist, ist dieser Ansatz riskant.
Und was ist, wenn Foo überhaupt nicht kopierbar ist? Nun, doh. ( Beachten Sie, dass in C ++ 17 mit garantierter Kopierentfernung das Nicht-Kopieren für den obigen Code kein Problem mehr darstellt. )
Schlussfolgerung: Die Herstellung einer Fabrik durch Rückgabe eines Objekts ist zwar in einigen Fällen eine Lösung (z. B. der zuvor erwähnte 2D-Vektor), jedoch noch kein allgemeiner Ersatz für Konstruktoren.
3) Zweiphasenaufbau
Eine andere Sache, die sich wahrscheinlich jemand einfallen lassen würde, ist die Trennung der Frage der Objektzuweisung und ihrer Initialisierung. Dies führt normalerweise zu folgendem Code:
class Foo {
public:
Foo() {
// empty or almost empty
}
// ...
};
class FooFactory {
public:
void createFooInSomeWay(Foo& foo, some, args);
};
void clientCode() {
Foo staticFoo;
auto_ptr<Foo> dynamicFoo = new Foo();
FooFactory factory;
factory.createFooInSomeWay(&staticFoo);
factory.createFooInSomeWay(&dynamicFoo.get());
// ...
}
Man könnte denken, es funktioniert wie ein Zauber. Der einzige Preis, für den wir in unserem Code zahlen ...
Da ich das alles geschrieben und als letztes belassen habe, muss ich es auch nicht mögen. :) Warum?
Zuallererst ... Ich mag das Konzept der zweiphasigen Konstruktion aufrichtig nicht und fühle mich schuldig, wenn ich es benutze. Wenn ich meine Objekte mit der Behauptung entwerfe, dass "wenn es existiert, ist es in einem gültigen Zustand", fühle ich, dass mein Code sicherer und weniger fehleranfällig ist. Ich mag es so.
Diese Konvention fallen zu lassen UND das Design meines Objekts nur zu ändern, um daraus eine Fabrik zu machen, ist ... nun, unhandlich.
Ich weiß, dass das oben Genannte nicht viele Menschen überzeugen wird, also lassen Sie mich einige fundiertere Argumente vorbringen. Bei zweiphasiger Konstruktion können Sie nicht:
- Elementvariablen initialisieren
const
oder referenzieren, - Übergeben Sie Argumente an Basisklassenkonstruktoren und Elementobjektkonstruktoren.
Und wahrscheinlich könnte es noch einige weitere Nachteile geben, an die ich momentan nicht denken kann, und ich fühle mich nicht einmal besonders dazu verpflichtet, da mich die oben genannten Punkte bereits überzeugen.
Also: nicht einmal in der Nähe einer guten allgemeinen Lösung für die Implementierung einer Fabrik.
Schlussfolgerungen:
Wir wollen eine Art der Objektinstanziierung, die:
- eine einheitliche Instanziierung unabhängig von der Zuordnung ermöglichen,
- Geben Sie den Konstruktionsmethoden unterschiedliche, aussagekräftige Namen (ohne sich auf die Überladung durch Argumente zu stützen).
- keinen signifikanten Leistungseinbruch und vorzugsweise einen signifikanten Code-Bloat-Treffer einführen, insbesondere auf Client-Seite,
- allgemein sein, wie in: für jede Klasse einführbar.
Ich glaube, ich habe bewiesen, dass die von mir genannten Methoden diese Anforderungen nicht erfüllen.
Irgendwelche Hinweise? Bitte geben Sie mir eine Lösung, ich möchte nicht glauben, dass diese Sprache es mir nicht erlaubt, ein so triviales Konzept richtig umzusetzen.
delete
. Diese Art von Methoden ist vollkommen in Ordnung, solange "dokumentiert" ist (Quellcode ist Dokumentation ;-)), dass der Aufrufer den Zeiger in Besitz nimmt (lesen Sie: ist dafür verantwortlich, ihn gegebenenfalls zu löschen).unique_ptr<T>
statt zurückgebenT*
.Antworten:
Ich glaube, dieser Punkt ist falsch. Die Komplexität spielt keine Rolle. Die Relevanz ist, was tut. Wenn ein Objekt in einem Schritt erstellt werden kann (nicht wie im Builder-Muster), ist der Konstruktor der richtige Ort dafür. Wenn Sie wirklich eine andere Klasse benötigen, um den Job auszuführen, sollte es sich um eine Hilfsklasse handeln, die ohnehin vom Konstruktor verwendet wird.
Hierfür gibt es eine einfache Problemumgehung:
Der einzige Nachteil ist, dass es etwas ausführlich aussieht:
Das Gute ist jedoch, dass Sie sofort sehen können, welchen Koordinatentyp Sie verwenden, und sich gleichzeitig keine Gedanken über das Kopieren machen müssen. Wenn Sie kopieren möchten und es teuer ist (wie natürlich durch Profilerstellung bewiesen), möchten Sie möglicherweise so etwas wie die gemeinsam genutzten Klassen von Qt verwenden , um das Kopieren von Overhead zu vermeiden.
Der Hauptgrund für die Verwendung des Factory-Musters ist normalerweise der Polymorphismus. Konstruktoren können nicht virtuell sein, und selbst wenn sie könnten, würde es nicht viel Sinn machen. Bei Verwendung der statischen Zuordnung oder der Stapelzuordnung können Sie Objekte nicht polymorph erstellen, da der Compiler die genaue Größe kennen muss. Es funktioniert also nur mit Zeigern und Referenzen. Und aus einer Fabrik einen Verweis Rückkehr nicht funktioniert auch, weil während ein Objekt technisch kann durch Verweis gelöscht werden, könnte es sein , eher verwirrend und fehleranfällig, siehe ist die Praxis eine C ++ Rückkehr Referenzgröße, das Böse?zum Beispiel. Zeiger sind also das einzige, was noch übrig ist, und dazu gehören auch intelligente Zeiger. Mit anderen Worten, Fabriken sind am nützlichsten, wenn sie mit dynamischer Zuordnung verwendet werden. Sie können also Folgendes tun:
In anderen Fällen helfen Fabriken nur bei der Lösung kleinerer Probleme wie der von Ihnen erwähnten mit Überlastungen. Es wäre schön, wenn es möglich wäre, sie einheitlich zu verwenden, aber es tut nicht viel weh, dass es wahrscheinlich unmöglich ist.
quelle
Einfaches Fabrikbeispiel:
quelle
unique_ptr
in diesem Beispiel hat keinen Leistungsaufwand. Das Verwalten von Ressourcen, einschließlich Speicher, ist einer der größten Vorteile von C ++ gegenüber jeder anderen Sprache, da Sie dies ohne Leistungseinbußen und deterministisch tun können, ohne die Kontrolle zu verlieren, aber Sie sagen genau das Gegenteil. Einige Leute mögen Dinge, die C ++ implizit tut, nicht, wie die Speicherverwaltung durch intelligente Zeiger, aber wenn Sie möchten, dass alles obligatorisch explizit ist, verwenden Sie C; Der Kompromiss ist um Größenordnungen weniger Probleme. Ich finde es unfair, dass Sie eine gute Empfehlung ablehnen.boost::ptr_vector<>
ist ein kleines bisschen effizienter, da es versteht, dass es den Zeiger besitzt, anstatt die Arbeit an eine Unterklasse zu delegieren. ABER der Hauptvorteil vonboost::ptr_vector<>
ist, dass es seine Mitglieder durch Referenz (nicht Zeiger) verfügbar macht, so dass es mit Algorithmen in der Standardbibliothek wirklich einfach zu verwenden ist.Haben Sie darüber nachgedacht, überhaupt keine Fabrik zu benutzen und stattdessen das Typensystem sinnvoll zu nutzen? Ich kann mir zwei verschiedene Ansätze vorstellen, die so etwas tun:
Option 1:
So können Sie Dinge schreiben wie:
Option 2:
Sie können "Tags" verwenden, wie es die STL mit Iteratoren und dergleichen tut. Zum Beispiel:
Mit diesem zweiten Ansatz können Sie Code schreiben, der folgendermaßen aussieht:
Das ist auch schön und ausdrucksstark, während Sie für jeden Konstruktor einzigartige Prototypen haben können.
quelle
Eine sehr gute Lösung finden Sie unter: http://www.codeproject.com/Articles/363338/Factory-Pattern-in-Cplusplus
Die beste Lösung sind die "Kommentare und Diskussionen", siehe "Keine Notwendigkeit für statische Erstellungsmethoden".
Aus dieser Idee habe ich eine Fabrik gemacht. Beachten Sie, dass ich Qt verwende, aber Sie können QMap und QString für Standardäquivalente ändern.
Beispielnutzung:
quelle
Ich stimme der akzeptierten Antwort größtenteils zu, aber es gibt eine C ++ 11-Option, die in vorhandenen Antworten nicht behandelt wurde:
Beispiel:
Dann können Sie Objekte auf dem Stapel erstellen:
Als Unterobjekte anderer Dinge:
Oder dynamisch zugeordnet:
Wann könnte ich das benutzen?
Wenn es auf einem öffentlichen Konstruktor nicht möglich ist, ohne eine vorläufige Berechnung aussagekräftige Initialisierer für alle Klassenmitglieder anzugeben, kann ich diesen Konstruktor in eine statische Methode konvertieren. Die statische Methode führt die vorläufigen Berechnungen durch und gibt dann ein Wertergebnis über einen privaten Konstruktor zurück, der nur eine elementweise Initialisierung durchführt.
Ich sage " Macht ", weil es davon abhängt, welcher Ansatz den klarsten Code liefert, ohne unnötig ineffizient zu sein.
quelle
Loki hat sowohl eine Fabrikmethode als auch eine abstrakte Fabrik . Beide sind (ausführlich) in Modern C ++ Design von Andei Alexandrescu dokumentiert. Die Factory-Methode ist wahrscheinlich näher an dem, wonach Sie zu suchen scheinen, obwohl sie immer noch etwas anders ist (zumindest wenn Speicher bereitgestellt wird, müssen Sie einen Typ registrieren, bevor die Factory Objekte dieses Typs erstellen kann).
quelle
Function
und die Typmanipulationen durchstd::function
und ersetzt werden können<type_traits>
und Lambdas, Threading und R-Wert-Refs Auswirkungen haben, die möglicherweise geringfügige Anpassungen erfordern, gibt es keinen Standardersatz für Singletons von Fabriken, wie er sie beschreibt.Ich versuche nicht, alle meine Fragen zu beantworten, da ich glaube, dass sie zu weit gefasst sind. Nur ein paar Anmerkungen:
Diese Klasse ist in der Tat eher ein Baumeister als eine Fabrik.
Dann könnten Sie Ihre Fabrik in einen intelligenten Zeiger einkapseln lassen. Ich glaube, auf diese Weise können Sie Ihren Kuchen haben und ihn auch essen.
Dies beseitigt auch die Probleme im Zusammenhang mit der Wertrendite.
Tatsächlich. Alle Entwurfsmuster haben ihre (sprachspezifischen) Einschränkungen und Nachteile. Es wird empfohlen, sie nur zu verwenden, wenn sie Ihnen bei der Lösung Ihres Problems helfen, nicht um ihrer selbst willen.
Wenn Sie nach der "perfekten" Werksimplementierung sind, dann viel Glück.
quelle
Dies ist meine Lösung im C ++ 11-Stil. Der Parameter 'base' gilt für die Basisklasse aller Unterklassen. Ersteller, sind std :: function-Objekte zum Erstellen von Unterklasseninstanzen, können eine Bindung zu Ihrer Unterklasse 'statische Elementfunktion' create (einige Argumente) 'sein. Das ist vielleicht nicht perfekt, funktioniert aber für mich. Und es ist eine Art "allgemeine" Lösung.
Ein Beispiel zur Verwendung.
quelle
Fabrikmuster
Und wenn Ihr Compiler die Rückgabewertoptimierung nicht unterstützt, lassen Sie es hinter sich, er enthält wahrscheinlich überhaupt nicht viel Optimierung ...
quelle
Factory
ist, dass es ziemlich allgemein gehalten ist und viel Boden abdeckt; Eine Factory kann beispielsweise Argumente hinzufügen (abhängig von der Umgebung / Einrichtung) oder Caching (im Zusammenhang mit Flyweight / Pools) bereitstellen. Diese Fälle sind jedoch nur in bestimmten Situationen sinnvoll.Ich weiß, dass diese Frage vor 3 Jahren beantwortet wurde, aber vielleicht haben Sie danach gesucht.
Google hat vor einigen Wochen eine Bibliothek veröffentlicht, die einfache und flexible dynamische Objektzuweisungen ermöglicht. Hier ist es: http://google-opensource.blogspot.fr/2014/01/introducing-infact-library.html
quelle