So implementieren Sie das Factory-Methodenmuster in C ++ korrekt

329

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 constoder 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.

Kos
quelle
7
@Zac, obwohl der Titel sehr ähnlich ist, sind die tatsächlichen Fragen meiner Meinung nach unterschiedlich.
Péter Török
2
Gutes Duplikat, aber der Text dieser Frage ist an und für sich wertvoll.
dmckee --- Ex-Moderator Kätzchen
7
Zwei Jahre nachdem ich dies gefragt habe, muss ich einige Punkte hinzufügen: 1) Diese Frage ist für mehrere Entwurfsmuster relevant ([abstrakte] Fabrik, Erbauer, Sie nennen es, ich mag es nicht, in ihre Taxonomie einzutauchen). 2) Das eigentliche Problem, das hier diskutiert wird, ist "wie man die Objektspeicherzuordnung sauber von der Objektkonstruktion entkoppelt?".
Kos
1
@ Tennis: Nur wenn du es nicht tust 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).
Boris Dalstein
1
@Boris @Dennis Sie können es auch sehr deutlich machen, indem Sie ein unique_ptr<T>statt zurückgeben T*.
Kos

Antworten:

107

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.

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.

Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!

Hierfür gibt es eine einfache Problemumgehung:

struct Cartesian {
  inline Cartesian(float x, float y): x(x), y(y) {}
  float x, y;
};
struct Polar {
  inline Polar(float angle, float magnitude): angle(angle), magnitude(magnitude) {}
  float angle, magnitude;
};
Vec2(const Cartesian &cartesian);
Vec2(const Polar &polar);

Der einzige Nachteil ist, dass es etwas ausführlich aussieht:

Vec2 v2(Vec2::Cartesian(3.0f, 4.0f));

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:

class Abstract {
  public:
    virtual void do() = 0;
};

class Factory {
  public:
    Abstract *create();
};

Factory f;
Abstract *a = f.create();
a->do();

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.

Sergei Tachenov
quelle
21
+1 für kartesische und polare Strukturen. Im Allgemeinen ist es am besten, Klassen und Strukturen zu erstellen, die die Daten, für die sie bestimmt sind, direkt darstellen (im Gegensatz zu einer allgemeinen Vec-Struktur). Ihre Fabrik ist auch ein gutes Beispiel, aber Ihr Beispiel zeigt nicht, wem der Zeiger 'a' gehört. Wenn die Factory 'f' es besitzt, wird es wahrscheinlich zerstört, wenn 'f' den Gültigkeitsbereich verlässt. Wenn 'f' es jedoch nicht besitzt, ist es für den Entwickler wichtig, sich daran zu erinnern, diesen Speicher freizugeben, da sonst ein Speicherverlust auftreten kann auftreten.
David Peterson
1
Natürlich kann ein Objekt als Referenz gelöscht werden! Siehe stackoverflow.com/a/752699/404734 Dies wirft natürlich die Frage auf, ob es sinnvoll ist, dynamischen Speicher als Referenz zurückzugeben, da der Rückgabewert möglicherweise durch Kopieren zugewiesen werden kann (der Aufrufer könnte natürlich auch etwas tun wie int a = * returnAPoninterToInt () und würde dann das gleiche Problem haben, wenn dynamisch allcoated Speicher zurückgegeben wird, wie für Referenzen, aber in der Zeigerversion muss der Benutzer explizit dereferenzieren, anstatt nur zu vergessen, explizit zu referenzieren, um falsch zu sein) .
Kaiserludi
1
@ Kaiserludi, schöner Punkt. Daran habe ich nicht gedacht, aber es ist immer noch eine "böse" Art, Dinge zu tun. Ich habe meine Antwort bearbeitet, um dies widerzuspiegeln.
Sergei Tachenov
Was ist mit der Erstellung verschiedener nicht polymorpher Klassen, die unveränderlich sind? Ist ein Factory-Muster dann für die Verwendung in C ++ geeignet?
Daaxix
@daaxix, warum benötigen Sie eine Factory, um Instanzen einer nicht polymorphen Klasse zu erstellen? Ich verstehe nicht, was Unveränderlichkeit damit zu tun hat.
Sergei Tachenov
49

Einfaches Fabrikbeispiel:

// Factory returns object and ownership
// Caller responsible for deletion.
#include <memory>
class FactoryReleaseOwnership{
  public:
    std::unique_ptr<Foo> createFooInSomeWay(){
      return std::unique_ptr<Foo>(new Foo(some, args));
    }
};

// Factory retains object ownership
// Thus returning a reference.
#include <boost/ptr_container/ptr_vector.hpp>
class FactoryRetainOwnership{
  boost::ptr_vector<Foo>  myFoo;
  public:
    Foo& createFooInSomeWay(){
      // Must take care that factory last longer than all references.
      // Could make myFoo static so it last as long as the application.
      myFoo.push_back(new Foo(some, args));
      return myFoo.back();
    }
};
Martin York
quelle
2
@LokiAstari Da die Verwendung intelligenter Zeiger der einfachste Weg ist, die Kontrolle über den Speicher zu verlieren. Die Kontrolle darüber, welche C / C ++ langs im Vergleich zu anderen Sprachen bekanntermaßen überlegen sind und von denen sie den größten Vorteil erhalten. Ganz zu schweigen von der Tatsache, dass intelligente Zeiger einen ähnlichen Speicheraufwand verursachen wie andere verwaltete Sprachen. Wenn Sie die automatische Speicherverwaltung nutzen möchten, starten Sie die Programmierung in Java oder C #, aber fügen Sie dieses Chaos nicht in C / C ++ ein.
Luke1985
45
@ lukasz1985 das unique_ptrin 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.
TheCppZoo
1
@ EdMaster: Ich habe vorher nicht geantwortet, weil er offensichtlich trollte. Bitte füttere den Troll nicht.
Martin York
17
@LokiAstari er könnte ein Troll sein, aber was er sagt, könnte Leute verwirren
TheCppZoo
1
@yau: Ja. Aber: 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 von boost::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.
Martin York
41

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:

struct linear {
    linear(float x, float y) : x_(x), y_(y){}
    float x_;
    float y_;
};

struct polar {
    polar(float angle, float magnitude) : angle_(angle),  magnitude_(magnitude) {}
    float angle_;
    float magnitude_;
};


struct Vec2 {
    explicit Vec2(const linear &l) { /* ... */ }
    explicit Vec2(const polar &p) { /* ... */ }
};

So können Sie Dinge schreiben wie:

Vec2 v(linear(1.0, 2.0));

Option 2:

Sie können "Tags" verwenden, wie es die STL mit Iteratoren und dergleichen tut. Zum Beispiel:

struct linear_coord_tag linear_coord {}; // declare type and a global
struct polar_coord_tag polar_coord {};

struct Vec2 {
    Vec2(float x, float y, const linear_coord_tag &) { /* ... */ }
    Vec2(float angle, float magnitude, const polar_coord_tag &) { /* ... */ }
};

Mit diesem zweiten Ansatz können Sie Code schreiben, der folgendermaßen aussieht:

Vec2 v(1.0, 2.0, linear_coord);

Das ist auch schön und ausdrucksstark, während Sie für jeden Konstruktor einzigartige Prototypen haben können.

Evan Teran
quelle
29

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.

#ifndef FACTORY_H
#define FACTORY_H

#include <QMap>
#include <QString>

template <typename T>
class Factory
{
public:
    template <typename TDerived>
    void registerType(QString name)
    {
        static_assert(std::is_base_of<T, TDerived>::value, "Factory::registerType doesn't accept this type because doesn't derive from base class");
        _createFuncs[name] = &createFunc<TDerived>;
    }

    T* create(QString name) {
        typename QMap<QString,PCreateFunc>::const_iterator it = _createFuncs.find(name);
        if (it != _createFuncs.end()) {
            return it.value()();
        }
        return nullptr;
    }

private:
    template <typename TDerived>
    static T* createFunc()
    {
        return new TDerived();
    }

    typedef T* (*PCreateFunc)();
    QMap<QString,PCreateFunc> _createFuncs;
};

#endif // FACTORY_H

Beispielnutzung:

Factory<BaseClass> f;
f.registerType<Descendant1>("Descendant1");
f.registerType<Descendant2>("Descendant2");
Descendant1* d1 = static_cast<Descendant1*>(f.create("Descendant1"));
Descendant2* d2 = static_cast<Descendant2*>(f.create("Descendant2"));
BaseClass *b1 = f.create("Descendant1");
BaseClass *b2 = f.create("Descendant2");
mabg
quelle
17

Ich stimme der akzeptierten Antwort größtenteils zu, aber es gibt eine C ++ 11-Option, die in vorhandenen Antworten nicht behandelt wurde:

  • Gibt die Ergebnisse der Factory-Methode nach Wert und zurück
  • Stellen Sie einen billigen Zugkonstruktor bereit .

Beispiel:

struct sandwich {
  // Factory methods.
  static sandwich ham();
  static sandwich spam();
  // Move constructor.
  sandwich(sandwich &&);
  // etc.
};

Dann können Sie Objekte auf dem Stapel erstellen:

sandwich mine{sandwich::ham()};

Als Unterobjekte anderer Dinge:

auto lunch = std::make_pair(sandwich::spam(), apple{});

Oder dynamisch zugeordnet:

auto ptr = std::make_shared<sandwich>(sandwich::ham());

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.

mbrcknl
quelle
1
Ich habe dies ausgiebig beim Umschließen von OpenGL-Ressourcen verwendet. Gelöschte Kopierkonstruktoren und Kopierzuweisung erzwingen die Verwendung der Verschiebungssemantik. Ich habe dann eine Reihe statischer Factory-Methoden zum Erstellen der einzelnen Ressourcentypen erstellt. Dies war viel besser lesbar als der auf Enum basierende Laufzeitversand von OpenGL, der abhängig von der übergebenen Enum häufig eine Reihe redundanter Funktionsparameter enthält. Es ist ein sehr nützliches Muster, überrascht, dass diese Antwort nicht weiter oben steht.
Fibbles
11

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).

Jerry Sarg
quelle
1
Auch wenn es veraltet ist (was ich bestreite), ist es immer noch einwandfrei zu warten. Ich verwende immer noch eine auf MC ++ D basierende Factory in einem neuen C ++ 14-Projekt mit großer Wirkung! Darüber hinaus sind die Factory- und Singleton-Muster wahrscheinlich die am wenigsten veralteten Teile. Während Teile von Loki wie Functionund die Typmanipulationen durch std::functionund 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.
Metall
5

Ich versuche nicht, alle meine Fragen zu beantworten, da ich glaube, dass sie zu weit gefasst sind. Nur ein paar Anmerkungen:

Es gibt Fälle, in denen die Objektkonstruktion eine Aufgabe ist, die komplex genug ist, um ihre Extraktion in eine andere Klasse zu rechtfertigen.

Diese Klasse ist in der Tat eher ein Baumeister als eine Fabrik.

Im Allgemeinen möchte ich die Benutzer der Fabrik nicht dazu zwingen, sich auf die dynamische Zuordnung zu beschränken.

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.

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.

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.

Péter Török
quelle
Danke für die Antwort! Aber können Sie erklären, wie die Verwendung eines intelligenten Zeigers die Einschränkung der dynamischen Zuordnung aufheben würde? Ich habe diesen Teil nicht ganz verstanden.
Kos
@Kos, mit intelligenten Zeigern können Sie die Zuordnung / Freigabe des tatsächlichen Objekts vor Ihren Benutzern verbergen. Sie sehen nur den einkapselnden intelligenten Zeiger, der sich nach außen wie ein statisch zugewiesenes Objekt verhält.
Péter Török
@ Kos, nicht im engeren Sinne, AFAIR. Sie übergeben das zu verpackende Objekt, das Sie wahrscheinlich irgendwann dynamisch zugewiesen haben. Dann übernimmt der Smart Pointer das Eigentum daran und stellt sicher, dass es ordnungsgemäß zerstört wird, wenn es nicht mehr benötigt wird (dessen Zeitpunkt für verschiedene Arten von Smart Pointern unterschiedlich festgelegt wird).
Péter Török
3

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.

template <class base, class... params> class factory {
public:
  factory() {}
  factory(const factory &) = delete;
  factory &operator=(const factory &) = delete;

  auto create(const std::string name, params... args) {
    auto key = your_hash_func(name.c_str(), name.size());
    return std::move(create(key, args...));
  }

  auto create(key_t key, params... args) {
    std::unique_ptr<base> obj{creators_[key](args...)};
    return obj;
  }

  void register_creator(const std::string name,
                        std::function<base *(params...)> &&creator) {
    auto key = your_hash_func(name.c_str(), name.size());
    creators_[key] = std::move(creator);
  }

protected:
  std::unordered_map<key_t, std::function<base *(params...)>> creators_;
};

Ein Beispiel zur Verwendung.

class base {
public:
  base(int val) : val_(val) {}

  virtual ~base() { std::cout << "base destroyed\n"; }

protected:
  int val_ = 0;
};

class foo : public base {
public:
  foo(int val) : base(val) { std::cout << "foo " << val << " \n"; }

  static foo *create(int val) { return new foo(val); }

  virtual ~foo() { std::cout << "foo destroyed\n"; }
};

class bar : public base {
public:
  bar(int val) : base(val) { std::cout << "bar " << val << "\n"; }

  static bar *create(int val) { return new bar(val); }

  virtual ~bar() { std::cout << "bar destroyed\n"; }
};

int main() {
  common::factory<base, int> factory;

  auto foo_creator = std::bind(&foo::create, std::placeholders::_1);
  auto bar_creator = std::bind(&bar::create, std::placeholders::_1);

  factory.register_creator("foo", foo_creator);
  factory.register_creator("bar", bar_creator);

  {
    auto foo_obj = std::move(factory.create("foo", 80));
    foo_obj.reset();
  }

  {
    auto bar_obj = std::move(factory.create("bar", 90));
    bar_obj.reset();
  }
}
DAG
quelle
Sieht gut aus für mich. Wie würden Sie eine statische Registrierung implementieren (möglicherweise etwas Makromagie)? Stellen Sie sich vor, die Basisklasse ist eine Wartungsklasse für Objekte. Die abgeleiteten Klassen bieten eine spezielle Art der Wartung dieser Objekte. Und Sie möchten schrittweise verschiedene Arten von Diensten hinzufügen, indem Sie für jede dieser Arten von Diensten eine Klasse hinzufügen, die von der Basis abgeleitet ist.
St0fF
2

Fabrikmuster

class Point
{
public:
  static Point Cartesian(double x, double y);
private:
};

Und wenn Ihr Compiler die Rückgabewertoptimierung nicht unterstützt, lassen Sie es hinter sich, er enthält wahrscheinlich überhaupt nicht viel Optimierung ...

Matthieu M.
quelle
Kann dies wirklich als Implementierung des Fabrikmusters angesehen werden?
Dennis
1
@ Tennis: Als entarteter Fall würde ich so denken. Das Problem dabei Factoryist, 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.
Matthieu M.
Wenn nur das Ändern des Compilers so einfach wäre, wie Sie es klingen lassen :)
rozina
@rozina: :) Es funktioniert gut unter Linux (gcc / clang sind bemerkenswert kompatibel); Ich gebe zu, dass Windows noch relativ geschlossen ist, obwohl es auf einer 64-Bit-Plattform besser werden sollte (weniger Patente im Weg, wenn ich mich richtig erinnere).
Matthieu M.
Und dann haben Sie die ganze eingebettete Welt mit einigen unterdurchschnittlichen Compilern. :) Ich arbeite mit einem solchen, der keine Rückgabewertoptimierung hat. Ich wünschte, es hätte. Leider ist ein Wechsel derzeit keine Option. Hoffentlich wird es in Zukunft aktualisiert oder wir werden für etwas anderes wechseln :)
rozina