it-swarm.com.de

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

Es gibt eine Sache in C++, die mich lange Zeit unwohl gefühlt hat, weil ich ehrlich gesagt nicht weiß, wie ich es machen soll, obwohl es einfach klingt:

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

Ziel: Es soll dem Client ermöglicht werden, ein Objekt mithilfe von Factory-Methoden anstelle der Konstruktoren des Objekts zu instanziieren, ohne dass inakzeptable Konsequenzen und Leistungseinbußen auftreten.

Mit "Factory-Methodenmuster" meine ich sowohl statische Factory-Methoden innerhalb eines Objekts als auch Methoden, die in einer anderen Klasse definiert sind, oder globale Funktionen. Ganz 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 Konstrukteure.

Das klingt nett (und ist in der Tat oft die beste Lösung), ist aber kein generelles 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 wenn man diese Tatsache beiseite legt, reicht es oft nicht, wenn man nur Konstruktoren einsetzt.

Das einfachste Beispiel, das ich kenne, ist eine 2D-Vektorklasse. So einfach und doch so knifflig. Ich möchte es sowohl aus kartesischen als auch aus polaren Koordinaten 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 dazu bringt, statische Factory-Methoden zu verwenden ... was im Wesentlichen bedeutet, dass ich das Factory-Muster auf irgendeine Weise implementiere ("die Klasse wird zu ihrer eigenen Factory"). Dies sieht gut aus (und würde für diesen speziellen Fall passen), scheitert aber in einigen Fällen, was ich in Punkt 2 beschreiben werde. Lesen Sie weiter.

Ein weiterer Fall: Der Versuch, eine API durch zwei undurchsichtige Typedefs zu überladen (z. B. GUIDs nicht verwandter Domänen oder ein GUID und ein Bitfeld)), erfolgt semantisch vollständig verschiedene (also theoretisch gültige Überladungen), die sich jedoch tatsächlich als dasselbe herausstellen - wie nicht signierte Ints oder leere Zeiger.


1) Der Java Way

Java hat es einfach, da wir nur dynamisch zugewiesene Objekte haben. Eine Fabrik zu bauen ist so einfach 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 jedoch häufig auch leistungsfähig. Ich glaube auch, dass es einige Ziele gibt (Stichwort: eingebettet), die keine dynamische Zuordnung zulassen. Und das bedeutet nicht, dass die Benutzer dieser Plattformen gerne saubere 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 mir das Ändern des Methodennamens missfällt, z. B. weil wir ein sprachunabhängiges Factory-Design jetzt nicht ordnungsgemäß implementieren können, da wir die Namen ändern müssen - und Jeder Benutzer dieses Codes muss sich daran erinnern, dass die Implementierung von der Spezifikation abweicht.

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. Richtig?

Nun, normalerweise. Aber manchmal nicht. Bei der Erstellung von Foo ist der Compiler für die Rückgabewertoptimierung tatsächlich auf uns angewiesen, da der C++ - Standard für die Compilerhersteller nicht ausreichend ist, um anzugeben, 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 es teuer ist, Foo zu kopieren, ist dieser Ansatz riskant.

Und was ist, wenn Foo überhaupt nicht kopierbar ist? Na ja, doh. ( Beachten Sie, dass in C++ 17 mit garantierter Kopierentfernung das Nicht-Kopieren für den obigen Code kein Problem mehr ist )

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


3) Zweiphasenaufbau

Eine andere Sache, die sich wahrscheinlich jemand einfallen lassen würde, ist die Trennung von Objektzuordnung und -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 meinen, es funktioniert wie ein Zauber. Der einzige Preis, den wir in unserem Code bezahlen ...

Da ich das alles geschrieben habe und es als letztes hinterlassen habe, muss ich es auch ablehnen. :) 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, es sich in einem gültigen Zustand befindet", habe ich das Gefühl, dass mein Code sicherer und weniger fehleranfällig ist. Ich mag es so.

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

Ich weiß, dass das oben Genannte nicht viele Leute überzeugen wird, also lassen Sie mich einige fundiertere Argumente anführen. Bei einer zweiphasigen Konstruktion können Sie nicht:

  • const initialisieren oder Member-Variablen referenzieren,
  • übergeben von Argumenten an Basisklassenkonstruktoren und Elementobjektkonstruktoren.

Und wahrscheinlich könnte es noch einige Nachteile geben, an die ich im Moment nicht denken kann, und ich fühle mich nicht einmal besonders verpflichtet, da mich die obigen Punkte bereits überzeugen.

Also: nicht mal annähernd eine gute Gesamtlösung zur Realisierung einer Fabrik.


Schlussfolgerungen:

Wir möchten eine Möglichkeit der Objektinstanziierung haben, die Folgendes ermöglicht:

  • einheitliche Instantiierung unabhängig von der Zuordnung ermöglichen,
  • geben Sie Konstruktionsmethoden unterschiedliche, aussagekräftige Namen (ohne sich auf das Überladen durch Argumente zu verlassen).
  • keinen nennenswerten Leistungseinbruch und vorzugsweise keinen nennenswerten Codeschwund, insbesondere auf Client-Seite, einführen,
  • sei allgemein, 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 ermöglicht, ein so triviales Konzept ordnungsgemäß umzusetzen.

306
Kos

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 ein bisschen wortreich 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 dies teuer ist (wie natürlich durch die Profilerstellung bewiesen), können Sie beispielsweise gemeinsam genutzte Klassen von Qt verwenden, um den Kopieraufwand zu verringern.

Der Hauptgrund für die Verwendung des Factory-Musters für den Zuordnungstyp ist normalerweise der Polymorphismus. Konstruktoren können nicht virtuell sein, und selbst wenn dies möglich wäre, würde dies keinen Sinn ergeben. Bei Verwendung der statischen oder 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 die Rückgabe einer Referenz aus einer Fabrik funktioniert auch nicht, da ein Objekt zwar technisch per Referenz gelöscht werden kann , aber verwirrend und fehlerhaft sein kann. anfällig, siehe Ist das Zurückgeben einer C++ - Referenzvariablen üblich, 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 dabei, kleinere Probleme zu lösen, wie die mit Überlastungen, die Sie erwähnt haben. Es wäre schön, wenn es möglich wäre, sie einheitlich zu verwenden, aber es schadet nicht viel, dass es wahrscheinlich unmöglich ist.

98
Sergei Tachenov

Einfaches Factory-Beispiel:

// 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();
    }
};
45
Martin York

Haben Sie darüber nachgedacht, überhaupt keine Fabrik zu verwenden und stattdessen das Typensystem von Nice zu verwenden? 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) { /* ... */ }
};

Mit denen können Sie Dinge schreiben wie:

Vec2 v(linear(1.0, 2.0));

Option 2:

sie können "Tags" wie die STL mit Iteratoren und dergleichen verwenden. 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 wie folgt aussieht:

Vec2 v(1.0, 2.0, linear_coord);

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

38
Evan Teran

Sie können eine sehr gute Lösung lesen in: http://www.codeproject.com/Articles/363338/Factory-Pattern-in-Cplusplus

Die beste Lösung finden Sie unter "Kommentare und Diskussionen", "Keine statischen Erstellungsmethoden erforderlich".

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");
27
mabg

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 zurück nach Wert, und
  • Geben Sie ein billiges Konstruktor verschieben an.

Beispiel:

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

Dann können Sie Objekte auf dem Stapel konstruieren:

sandwich mine{sandwich::ham()};

Als Unterobjekte anderer Dinge:

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

Oder dynamisch vergeben:

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

Wann könnte ich das benutzen?

Wenn es in 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 Initialisierung nach Mitgliedern vornimmt.

Ich sage 'könnte', weil es davon abhängt, welcher Ansatz den klarsten Code liefert, ohne unnötig ineffizient zu sein.

15
mbrcknl

Loki hat sowohl eine Factory-Methode als auch eine Abstract Factory . Beide sind (ausführlich) in Modern C++ Design von Andei Alexandrescu dokumentiert. Die Factory-Methode ähnelt wahrscheinlich eher dem, was Sie zu suchen scheinen, obwohl es immer noch ein bisschen anders ist (zumindest wenn der Speicher belegt ist, müssen Sie einen Typ registrieren, bevor die Factory Objekte dieses Typs erstellen kann).

11
Jerry Coffin

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

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 Builder als eine Factory.

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

Dann können Sie Ihre Factory veranlassen, sie in einem intelligenten Zeiger zu kapseln. Ich glaube, auf diese Weise können Sie Ihren Kuchen haben und ihn auch essen.

Dadurch werden auch die Probleme im Zusammenhang mit der Wertrendite beseitigt.

Fazit: Die Erstellung einer Factory durch Rückgabe eines Objekts ist in der Tat eine Lösung für einige Fälle (z. B. den zuvor erwähnten 2D-Vektor), aber immer noch kein genereller 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 für sich.

Wenn Sie nach der "perfekten" Factory-Implementierung sind, nun, viel Glück.

5
Péter Török

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, es enthält wahrscheinlich überhaupt keine große Optimierung ...

2
Matthieu M.

Dies ist meine C++ 11-Lösung. Der Parameter 'base' ist für die Basisklasse aller Unterklassen. creators sind std :: function-Objekte zum Erstellen von Unterklasseninstanzen und können eine Bindung zu Ihrer Unterklasse 'static member function' create (some args) 'darstellen. 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();
  }
}
1
DAG

Ich weiß, dass diese Frage vor 3 Jahren beantwortet wurde, aber das könnte genau das sein, wonach Sie gesucht haben.

Google hat vor einigen Wochen eine Bibliothek veröffentlicht, die eine einfache und flexible dynamische Objektzuweisung ermöglicht. Hier ist es: http://google-opensource.blogspot.fr/2014/01/introducing-infact-library.html

1
Florian Richoux