390 Stimmen

Wie man das Factory-Methodenmuster in C++ korrekt implementiert

Es gibt eine Sache in C++, die mir schon seit langem Unbehagen bereitet, weil ich ehrlich gesagt nicht weiß, wie es geht, auch wenn es einfach klingt:

Wie implementiere ich die Factory-Methode in C++ richtig?

Ziel: dem Client die Möglichkeit zu geben, ein Objekt mit Hilfe von Factory-Methoden anstelle der Konstruktoren des Objekts zu instanziieren, ohne inakzeptable Konsequenzen und Leistungseinbußen.

Mit "Fabrikmethodenmuster" meine ich sowohl statische Fabrikmethoden innerhalb eines Objekts als auch Methoden, die in einer anderen Klasse definiert sind, oder globale Funktionen. Ganz allgemein "das Konzept, den normalen Weg der Instanziierung der Klasse X an eine andere Stelle als den Konstruktor umzuleiten".

Lassen Sie mich kurz einige mögliche Antworten aufzählen, an die ich gedacht habe.


0) Machen Sie keine Fabriken, sondern Konstrukteure.

Das hört sich gut an (und ist auch oft die beste Lösung), ist aber kein Allheilmittel. Zunächst einmal gibt es Fälle, in denen die Konstruktion von Objekten eine Aufgabe ist, die komplex genug ist, um ihre Auslagerung in eine andere Klasse zu rechtfertigen. Aber selbst wenn man diese Tatsache beiseite lässt, reicht es selbst für einfache Objekte oft nicht aus, nur Konstruktoren zu verwenden.

Das einfachste Beispiel, das ich kenne, ist eine 2-D-Vektorklasse. So einfach und doch so knifflig. Ich möchte in der Lage sein, sie sowohl aus kartesischen als auch aus polaren Koordinaten zu konstruieren. Offensichtlich kann ich das nicht:

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);
    // ...
};

Das führt mich anstelle von Konstruktoren zur Verwendung von statischen Fabrikmethoden... was im Wesentlichen bedeutet, dass ich das Fabrikmuster in irgendeiner Weise implementiere ("die Klasse wird zu ihrer eigenen Fabrik"). Das sieht nett aus (und würde in diesem speziellen Fall auch passen), versagt aber in einigen Fällen, die ich in Punkt 2 beschreiben werde. Lesen Sie bitte weiter.

ein anderer Fall: der Versuch, mit zwei undurchsichtigen Typendefinitionen einer API (z. B. GUIDs von nicht verwandten Domänen oder eine GUID und ein Bitfeld) zu überladen, Typen, die semantisch völlig unterschiedlich sind (also - theoretisch - gültige Überladungen), die sich aber tatsächlich als dasselbe herausstellen - wie unsigned ints oder void pointers.


1) Der Java-Weg

Java hat es einfach, da wir nur dynamisch zugewiesene Objekte haben. Die Erstellung einer Fabrik 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++ heißt das übersetzt:

class FooFactory {
public:
    Foo* createFooInSomeWay() {
        return new Foo(some, args);
    }
};

Cool? Oft, in der Tat. Aber dann ist der Benutzer gezwungen, nur dynamische Zuweisungen zu verwenden. Die statische Zuweisung ist es, die C++ komplex macht, aber sie ist auch das, was es oft leistungsfähig macht. Ich glaube auch, dass es einige Ziele (Stichwort: embedded) gibt, die keine dynamische Zuweisung zulassen. Und das bedeutet nicht, dass die Benutzer dieser Plattformen gerne sauberes OOP schreiben.

Wie auch immer, Philosophie beiseite: Im allgemeinen Fall möchte ich die Benutzer der Fabrik nicht zwingen, sich auf eine dynamische Zuweisung zu beschränken.


2) Rückgabe nach Wert

OK, wir wissen also, dass 1) in Ordnung ist, wenn wir eine dynamische Zuweisung wünschen. Warum fügen wir nicht noch eine statische Zuweisung 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 nicht. Ändern wir also die Methodennamen, um das zu berücksichtigen. Und ja, ich habe das ungültige Codebeispiel oben nur geschrieben, um zu betonen, wie sehr ich die Notwendigkeit, den Methodennamen zu ändern, ablehne, zum Beispiel, weil wir jetzt kein sprachunabhängiges Fabrikdesign richtig implementieren können, da wir die Namen ändern müssen - und jeder Benutzer dieses Codes wird sich diesen Unterschied zwischen der Implementierung und der Spezifikation merken müssen.

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 Namen der Methode ändern müssen. Es ist unvollkommen, da wir denselben Code zweimal schreiben müssen. Aber einmal gemacht, funktioniert es. Oder?

Nun, normalerweise. Aber manchmal auch nicht. Bei der Erstellung von Foo sind wir darauf angewiesen, dass der Compiler die Optimierung des Rückgabewerts für uns übernimmt, denn der C++-Standard ist so wohlwollend, dass die Compiler-Hersteller nicht angeben, wann das Objekt an Ort und Stelle erstellt und wann es kopiert wird, wenn ein temporäres Objekt nach Wert in C++ zurückgegeben wird. Wenn Foo also teuer zu kopieren ist, ist dieser Ansatz riskant.

Und was, wenn Foo gar nicht kopierbar ist? Tja, puh. ( Beachten Sie, dass in C++17 mit garantierter Copy Elision das Nicht-Kopierbar-Sein kein Problem mehr für den obigen Code ist )

Schlussfolgerung: Die Erstellung einer Fabrik durch Rückgabe eines Objekts ist in der Tat eine Lösung für einige Fälle (wie z. B. den bereits erwähnten 2-D-Vektor), aber noch kein allgemeiner Ersatz für Konstruktoren.


3) Zweiphasige Konstruktion

Eine andere Sache, auf die jemand wahrscheinlich kommen würde, ist die Trennung von Objektzuweisung und Initialisierung. Dies führt normalerweise zu einem Code wie diesem:

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 meinen, es funktioniert wie ein Zauber. Der einzige Preis, den wir für unseren Code zahlen...

Da ich das alles geschrieben habe und dies als letztes, muss es mir auch nicht gefallen :) Und warum?

Zunächst einmal... Ich mag das Konzept des zweiphasigen Aufbaus wirklich nicht und habe ein schlechtes Gewissen, wenn ich es verwende. Wenn ich meine Objekte mit der Behauptung entwerfe, dass "wenn es existiert, ist es in einem gültigen Zustand", habe ich das Gefühl, dass mein Code sicherer und weniger fehleranfällig ist. Das ist gut so.

Diese Konvention fallen zu lassen UND das Design meines Objekts zu ändern, nur um eine Fabrik daraus zu machen, ist nun ja, unhandlich.

Ich weiß, dass die obigen Ausführungen viele Leute nicht überzeugen werden, also möchte ich einige solidere Argumente anführen. Mit der Zwei-Phasen-Konstruktion können Sie nicht:

  • initialisieren const oder Referenzmitgliedsvariablen,
  • Argumente an Basisklassenkonstruktoren und Konstruktoren von Mitgliedsobjekten übergeben.

Und wahrscheinlich gibt es noch weitere Nachteile, die mir im Moment nicht einfallen, und ich fühle mich auch nicht besonders verpflichtet, da mich die oben genannten Punkte bereits überzeugen.

Also: nicht einmal annähernd eine gute allgemeine Lösung für die Implementierung einer Fabrik.


Schlussfolgerungen:

Wir wollen eine Möglichkeit der Objektinstanziierung haben, die das ermöglicht:

  • ermöglichen eine einheitliche Instanziierung unabhängig von der Zuweisung,
  • den Konstruktionsmethoden unterschiedliche, aussagekräftige Namen geben (und sich somit nicht auf das Überladen von By-Argumenten verlassen),
  • keine signifikanten Leistungseinbußen und vorzugsweise keine signifikante Codeaufblähung mit sich bringen, insbesondere auf der Client-Seite,
  • allgemein sein, d.h. für jede Klasse eingeführt werden können.

Ich glaube, ich habe bewiesen, dass die von mir genannten Möglichkeiten diese Anforderungen nicht erfüllen.

Haben Sie einen Tipp? Bitte bieten Sie mir eine Lösung, ich will nicht denken, dass diese Sprache wird nicht zulassen, dass ich richtig zu implementieren, wie ein triviales Konzept.

12voto

Jerry Coffin Punkte 452852

Loki hat sowohl eine Fabrik-Methode und ein Abstrakte Fabrik . Beide sind (ausführlich) dokumentiert in Modernes C++-Design , von Andei Alexandrescu. Die Factory-Methode ist wahrscheinlich näher an dem, was Sie zu suchen scheinen, obwohl es immer noch ein bisschen anders ist (zumindest wenn ich mich richtig erinnere, müssen Sie einen Typ registrieren, bevor die Factory Objekte dieses Typs erstellen kann).

5voto

Péter Török Punkte 111735

Ich versuche nicht, alle meine Fragen zu beantworten, weil ich glaube, dass das zu weit gefasst ist. Nur ein paar Anmerkungen:

Es gibt Fälle, in denen die Konstruktion von Objekten eine Aufgabe ist, die komplex genug ist, um ihre Auslagerung in eine andere Klasse zu rechtfertigen.

Diese Klasse ist in Wirklichkeit eine Bauherr und nicht eine Fabrik.

Im allgemeinen Fall möchte ich die Benutzer der Fabrik nicht zwingen, sich auf eine dynamische Zuweisung zu beschränken.

Dann könnten Sie es in Ihrer Fabrik in einen intelligenten Zeiger einkapseln lassen. Ich glaube, auf diese Weise können Sie Ihren Kuchen haben und ihn auch essen.

Dadurch werden auch die Probleme im Zusammenhang mit der wertbezogenen Rückgabe beseitigt.

Schlussfolgerung: Die Erstellung einer Fabrik durch Rückgabe eines Objekts ist in der Tat eine Lösung für einige Fälle (wie z. B. den bereits erwähnten 2-D-Vektor), aber noch kein allgemeiner Ersatz für Konstruktoren.

In der Tat. Alle Entwurfsmuster haben ihre (sprachspezifischen) Beschränkungen und Nachteile. Es wird empfohlen, sie nur dann zu verwenden, wenn sie Ihnen helfen, Ihr Problem zu lösen, nicht um ihrer selbst willen.

Wenn Sie auf der Suche nach der "perfekten" Umsetzung im Werk sind, dann viel Glück.

3voto

DAG Punkte 367

Dies ist meine Lösung im Stil von C++11. Der Parameter 'base' ist für die Basisklasse aller Unterklassen. creators sind std::function-Objekte, um Instanzen von Unterklassen zu erstellen, möglicherweise eine Bindung an die statische Mitgliedsfunktion 'create(some args)' Ihrer Unterklasse. Das ist vielleicht nicht perfekt, aber für mich funktioniert es. 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 für die 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();
  }
}

2voto

Matthieu M. Punkte 266317

Fabrik-Muster

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

Und wenn Ihr Compiler die Rückgabewert-Optimierung nicht unterstützt, dann lassen Sie ihn stehen, er enthält wahrscheinlich überhaupt keine Optimierung...

2voto

user1095108 Punkte 13219
extern std::pair<std::string_view, Base*(*)()> const factories[2];

decltype(factories) factories{
  {"blah", []() -> Base*{return new Blah;}},
  {"foo", []() -> Base*{return new Foo;}}
};

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X