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.

123voto

Sergei Tachenov Punkte 23067

Erstens, die Objektkonstruktion eine so komplexe Aufgabe ist genug ist, um ihre Ausgliederung in eine einer anderen Klasse zu rechtfertigen.

Ich glaube, dieser Punkt ist nicht korrekt. Die Komplexität spielt eigentlich keine Rolle. Es kommt auf die Relevanz an. Wenn ein Objekt in einem Schritt konstruiert werden kann (nicht wie beim Builder-Pattern), ist der Konstruktor der richtige Ort, um dies zu tun. Wenn Sie wirklich eine andere Klasse benötigen, um die Aufgabe zu erfüllen, dann sollte es eine Hilfsklasse sein, die ohnehin vom Konstruktor aus verwendet wird.

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

Hierfür gibt es eine einfache Lösung:

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 wenig langatmig aussieht:

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

Das Gute daran ist, dass man sofort sehen kann, welchen Koordinatentyp man verwendet, und dass man sich keine Gedanken über das Kopieren machen muss. Wenn Sie das Kopieren wünschen und es teuer ist (was natürlich durch die Profilerstellung bewiesen wird), können Sie etwas wie Qt's gemeinsame Klassen um Kopieraufwand zu vermeiden.

Was die Zuweisungsart betrifft, so ist der Hauptgrund für die Verwendung des Fabrikmusters in der Regel die Polymorphie. Konstruktoren können nicht virtuell sein, und selbst wenn sie es könnten, würde es nicht viel Sinn machen. Bei statischer oder Stack-Zuweisung können Sie keine polymorphen Objekte 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 Factory funktioniert auch nicht, denn während ein Objekt technisch gesehen よろしい per Verweis gelöscht werden, könnte dies ziemlich verwirrend und fehleranfällig sein, siehe Ist die Praxis, eine C++-Referenzvariable zurückzugeben, schlecht? zum Beispiel. Es bleiben also nur noch Zeiger übrig, und zwar auch intelligente Zeiger. Mit anderen Worten, Fabriken sind am nützlichsten, wenn sie mit dynamischer Zuweisung verwendet werden, so dass Sie Dinge wie diese tun können:

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 von Ihnen erwähnten Probleme mit Überlastungen. Es wäre schön, wenn es möglich wäre, sie auf einheitliche Weise zu verwenden, aber es schadet nicht, dass es wahrscheinlich unmöglich ist.

54voto

Martin York Punkte 245363

Beispiel einer einfachen Fabrik:

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

45voto

Evan Teran Punkte 83711

Haben Sie schon einmal darüber nachgedacht, überhaupt keine Fabrik zu verwenden und stattdessen das Typsystem zu nutzen? Ich kann mir zwei verschiedene Ansätze vorstellen, die diese Art von Dingen 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) { /* ... */ }
};

Damit können Sie Dinge schreiben wie:

Vec2 v(linear(1.0, 2.0));

Option 2:

können Sie "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 einen Code schreiben, der wie folgt aussieht:

Vec2 v(1.0, 2.0, linear_coord);

was ebenfalls schön und aussagekräftig ist und es Ihnen ermöglicht, eindeutige Prototypen für jeden Konstruktor zu haben.

29voto

mabg Punkte 1698

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

Die beste Lösung finden Sie in den "Kommentaren und Diskussionen", siehe "Kein Bedarf an statischen Create-Methoden".

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

Beispielhafte Verwendung:

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

21voto

mbrcknl Punkte 665

Ich stimme größtenteils mit der akzeptierten Antwort überein, aber es gibt eine C++11-Option, die in den vorhandenen Antworten nicht berücksichtigt wurde:

  • Rückgabe der Ergebnisse der Fabrikmethode nach Wert und
  • Eine billige Lösung anbieten Bewegungskonstrukteur .

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 von anderen Dingen:

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

Oder dynamisch zugewiesen:

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

Wann kann ich das verwenden?

Wenn es bei einem öffentlichen Konstruktor nicht möglich ist, sinnvolle Initialisierungen für alle Klassenmitglieder ohne vorherige Berechnung vorzunehmen, dann könnte ich diesen Konstruktor in eine statische Methode umwandeln. Die statische Methode führt die vorläufigen Berechnungen durch und gibt dann einen Wert als Ergebnis über einen privaten Konstruktor zurück, der lediglich eine mitgliederbezogene Initialisierung vornimmt.

Ich sage ' könnte ', denn es kommt darauf an, welcher Ansatz den klarsten Code ergibt, ohne unnötig ineffizient zu sein.

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