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ückgeben T*
.