Come implementare il modello di metodo factory in C ++ correttamente

C’è questa cosa in C ++ che mi ha fatto sentire a disagio per un periodo piuttosto lungo, perché sinceramente non so come farlo, anche se sembra semplice:

Come si implementa correttamente il metodo factory in C ++?

Obiettivo: rendere ansible consentire al client di creare un’istanza di alcuni oggetti utilizzando metodi di fabbrica anziché i costruttori dell’object, senza conseguenze inaccettabili e un impatto sulle prestazioni.

Per “modello metodo di fabbrica”, intendo sia i metodi statici di fabbrica all’interno di un object o metodi definiti in un’altra class, sia le funzioni globali. Solo in generale “il concetto di reindirizzamento del modo normale di istanziazione della class X in qualsiasi altro luogo rispetto al costruttore”.

Permettetemi di sfogliare alcune possibili risposte a cui ho pensato.


0) Non fabbricare fabbriche, creare costruttori.

Questo suona bene (e anzi spesso la soluzione migliore), ma non è un rimedio generale. Prima di tutto, ci sono casi in cui la costruzione di oggetti è abbastanza complessa da giustificare la sua estrazione in un’altra class. Ma anche mettere da parte questo fatto, anche per oggetti semplici che usano solo i costruttori, spesso non lo faranno.

L’esempio più semplice che conosco è una class Vector 2D. Così semplice, ma difficile. Voglio essere in grado di costruirlo sia da coordinate cartesiane che polari. Ovviamente, non posso fare:

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

Il mio modo di pensare naturale è quindi:

 struct Vec2 { static Vec2 fromLinear(float x, float y); static Vec2 fromPolar(float angle, float magnitude); // ... }; 

Il che, al posto dei costruttori, mi porta all’utilizzo di metodi statici di fabbrica … il che significa essenzialmente che sto implementando il modello di fabbrica, in qualche modo (“la class diventa la propria fabbrica”). Questo sembra bello (e si addice a questo caso particolare), ma in alcuni casi fallisce, cosa che descriverò al punto 2. Continua a leggere.

un altro caso: provare a sovraccaricare da due typedef opachi di alcune API (come GUID di domini non collegati, o un GUID e un bitfield), tipi semanticamente totalmente diversi (quindi – in teoria – sovraccarichi validi) ma che in realtà risultano essere i stessa cosa – come intarsi non firmati o puntatori vuoti.


1) La Via Java

Java è semplice, poiché abbiamo solo oggetti allocati dynamicmente. Fare una fabbrica è banale come:

 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 ++, questo si traduce in:

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

Freddo? Spesso, anzi. Ma poi – questo costringe l’utente a usare solo l’allocazione dynamic. L’allocazione statica è ciò che rende complesso C ++, ma è anche ciò che spesso lo rende potente. Inoltre, credo che esistano alcuni obiettivi (parola chiave: embedded) che non consentono l’allocazione dynamic. E questo non implica che gli utenti di quelle piattaforms amano scrivere OOP pulito.

Ad ogni modo, filosofia a parte: nel caso generale, non voglio forzare gli utenti della fabbrica a essere vincolati all’assegnazione dynamic.


2) Return-by-value

OK, quindi sappiamo che 1) è bello quando vogliamo l’allocazione dynamic. Perché non aggiungeremo un’assegnazione statica su questo?

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

Che cosa? Non possiamo sovraccaricare con il tipo di ritorno? Oh, certo che non possiamo. Quindi cambiamo i nomi dei metodi per riflettere questo. E sì, ho scritto l’esempio di codice non valido sopra solo per sottolineare quanto non mi piaccia la necessità di cambiare il nome del metodo, ad esempio perché non possiamo implementare correttamente un progetto factory agnostico in modo corretto, dato che dobbiamo cambiare i nomi – e ogni utente di questo codice dovrà ricordare la differenza dell’implementazione dalla specifica.

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

OK … ce l’abbiamo. È brutto, perché abbiamo bisogno di cambiare il nome del metodo. È imperfetto, dal momento che abbiamo bisogno di scrivere lo stesso codice due volte. Ma una volta fatto, funziona. Destra?

Beh, di solito. Ma a volte no. Quando creiamo Foo, in realtà dipendiamo dal compilatore per fare l’ottimizzazione del valore di ritorno per noi, perché lo standard C ++ è abbastanza ben fatto per i produttori del compilatore da non specificare quando l’object verrà creato sul posto e quando verrà copiato quando si restituisce un object temporaneo per valore in C ++. Quindi, se Foo è costoso da copiare, questo approccio è rischioso.

E se Foo non fosse affatto copiato? Bene, doh. ( Si noti che in C ++ 17 con copia elision garantita, non-essere-copiabile non è più un problema per il codice sopra )

Conclusione: Realizzare una fabbrica restituendo un object è in effetti una soluzione per alcuni casi (come il vettore 2-D precedentemente menzionato), ma non è ancora una sostituzione generale per i costruttori.


3) Costruzione a due fasi

Un’altra cosa che probabilmente qualcuno potrebbe fare è separare il problema dell’assegnazione degli oggetti e la sua inizializzazione. Questo di solito provoca un codice come questo:

 class Foo { public: Foo() { // empty or almost empty } // ... }; class FooFactory { public: void createFooInSomeWay(Foo& foo, some, args); }; void clientCode() { Foo staticFoo; auto_ptr dynamicFoo = new Foo(); FooFactory factory; factory.createFooInSomeWay(&staticFoo); factory.createFooInSomeWay(&dynamicFoo.get()); // ... } 

Si potrebbe pensare che funzioni come un incantesimo. L’unico prezzo che paghiamo nel nostro codice …

Dato che ho scritto tutto questo e lasciato questo come l’ultimo, anch’io non mi piace. 🙂 Perché?

Prima di tutto … sinceramente non mi piace il concetto di costruzione a due fasi e mi sento in colpa quando lo uso. Se progetto i miei oggetti con l’affermazione che “se esiste, è in uno stato valido”, ritengo che il mio codice sia più sicuro e meno sobject a errori. Mi piace così.

Dovendo abbandonare questa convenzione E cambiare il design del mio object solo allo scopo di farne fabbrica è … beh, poco maneggevole.

So che quanto sopra non convincerà molte persone, quindi lasciatemi dare argomenti più solidi. Usando la costruzione a due fasi, non è ansible:

  • inizializza le variabili membro const o di riferimento,
  • passare argomenti a costruttori di classi di base e costruttori di oggetti membri.

E probabilmente potrebbero esserci ulteriori svantaggi ai quali non riesco a pensare in questo momento, e non mi sento nemmeno particolarmente obbligato, visto che i punti sopra elencati mi convincono già.

Quindi: nemmeno vicino a una buona soluzione generale per l’implementazione di una fabbrica.


conclusioni:

Vogliamo avere un modo di istanziazione degli oggetti che:

  • consentire un’istanziazione uniforms indipendentemente dall’assegnazione,
  • dare nomi diversi e significativi ai metodi di costruzione (quindi non fare affidamento sul sovraccarico di argomenti),
  • non introdurre un successo significativo in termini di prestazioni e, preferibilmente, un significativo codice gonfiato, specialmente da parte del cliente,
  • essere generale, come in: ansible essere introdotto per qualsiasi class.

Credo di aver dimostrato che i modi che ho menzionato non soddisfano tali requisiti.

Qualche suggerimento? Per favore forniscimi una soluzione, non voglio pensare che questa lingua non mi permetta di implementare correttamente un concetto così banale.

Prima di tutto, ci sono casi in cui la costruzione di oggetti è abbastanza complessa da giustificare la sua estrazione in un’altra class.

Credo che questo punto sia errato. La complessità non ha molta importanza. La rilevanza è ciò che fa. Se un object può essere costruito in un unico passaggio (non come nel modello del builder), il costruttore è il posto giusto per farlo. Se hai davvero bisogno di un’altra class per eseguire il lavoro, allora dovrebbe essere una class di supporto che viene comunque utilizzata dal costruttore.

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

C’è una soluzione facile per questo:

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

L’unico svantaggio è che sembra un po ‘prolisso:

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

Ma la cosa buona è che puoi immediatamente vedere che tipo di coordinate stai usando, e allo stesso tempo non devi preoccuparti di copiare. Se vuoi copiare ed è costoso (come dimostrato dalla profilazione, ovviamente), potresti voler usare qualcosa come le classi condivise di Qt per evitare di copiare i costi generali.

Per quanto riguarda il tipo di allocazione, il motivo principale per utilizzare il modello di fabbrica è solitamente il polimorfismo. I costruttori non possono essere virtuali, e anche se potessero, non avrebbe molto senso. Quando si utilizza l’allocazione statica o di stack, non è ansible creare oggetti in modo polimorfico perché il compilatore deve conoscere la dimensione esatta. Quindi funziona solo con puntatori e riferimenti. E restituire un riferimento da una factory non funziona, perché mentre un object tecnicamente può essere cancellato per riferimento, potrebbe essere piuttosto confuso e sobject a bug, vedere La pratica di restituire una variabile di riferimento C ++, il male? per esempio. Quindi i puntatori sono l’unica cosa che rimane, e questo include anche i puntatori intelligenti. In altre parole, le fabbriche sono più utili se usate con allocazione dynamic, quindi puoi fare cose come questa:

 class Abstract { public: virtual void do() = 0; }; class Factory { public: Abstract *create(); }; Factory f; Abstract *a = f.create(); a->do(); 

In altri casi, le fabbriche aiutano solo a risolvere problemi minori come quelli con sovraccarico che hai menzionato. Sarebbe bello se fosse ansible usarli in modo uniforms, ma ciò non danneggia molto di quanto sia probabilmente imansible.

Esempio di fabbrica semplice:

 // Factory returns object and ownership // Caller responsible for deletion. #include  class FactoryReleaseOwnership{ public: std::unique_ptr createFooInSomeWay(){ return std::unique_ptr(new Foo(some, args)); } }; // Factory retains object ownership // Thus returning a reference. #include  class FactoryRetainOwnership{ boost::ptr_vector 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(); } }; 

Hai mai pensato di non usare affatto una fabbrica e invece di fare un buon uso del sistema di tipi? Posso pensare a due approcci diversi che fanno questo genere di cose:

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

Che ti permette di scrivere cose come:

 Vec2 v(linear(1.0, 2.0)); 

Opzione 2:

puoi usare “tag” come fa STL con gli iteratori e così via. Per esempio:

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

Questo secondo approccio ti consente di scrivere codice simile a questo:

 Vec2 v(1.0, 2.0, linear_coord); 

che è anche bello ed espressivo mentre consente di avere prototipi unici per ogni costruttore.

È ansible leggere un’ottima soluzione in: http://www.codeproject.com/Articles/363338/Factory-Pattern-in-Cplusplus

La soluzione migliore è sui “commenti e discussioni”, vedi “Non c’è bisogno di metodi di creazione statici”.

Da questa idea, ho fatto una fabbrica. Nota che sto usando Qt, ma puoi cambiare QMap e QString per gli equivalenti std.

 #ifndef FACTORY_H #define FACTORY_H #include  #include  template  class Factory { public: template  void registerType(QString name) { static_assert(std::is_base_of::value, "Factory::registerType doesn't accept this type because doesn't derive from base class"); _createFuncs[name] = &createFunc; } T* create(QString name) { typename QMap::const_iterator it = _createFuncs.find(name); if (it != _createFuncs.end()) { return it.value()(); } return nullptr; } private: template  static T* createFunc() { return new TDerived(); } typedef T* (*PCreateFunc)(); QMap _createFuncs; }; #endif // FACTORY_H 

Esempio di utilizzo:

 Factory f; f.registerType("Descendant1"); f.registerType("Descendant2"); Descendant1* d1 = static_cast(f.create("Descendant1")); Descendant2* d2 = static_cast(f.create("Descendant2")); BaseClass *b1 = f.create("Descendant1"); BaseClass *b2 = f.create("Descendant2"); 

Sono per lo più d’accordo con la risposta accettata, ma c’è un’opzione C ++ 11 che non è stata trattata nelle risposte esistenti:

  • Restituire i risultati del metodo di produzione in base al valore e
  • Fornire un costruttore di movimento economico.

Esempio:

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

Quindi puoi build oggetti nello stack:

 sandwich mine{sandwich::ham()}; 

Come sottooggetti di altre cose:

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

O allocati dynamicmente:

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

Quando potrei usare questo?

Se, su un costruttore pubblico, non è ansible fornire inizializzatori significativi per tutti i membri della class senza qualche calcolo preliminare, allora potrei convertire quel costruttore in un metodo statico. Il metodo statico esegue i calcoli preliminari, quindi restituisce un risultato di valore tramite un costruttore privato che esegue solo l’inizializzazione del membro.

Dico ” potrebbe ” perché dipende da quale approccio fornisce il codice più chiaro senza essere inutilmente inefficiente.

Loki ha sia un metodo di fabbrica che una fabbrica astratta . Entrambi sono documentati (ampiamente) in Modern C ++ Design , di Andei Alexandrescu. Il metodo factory è probabilmente più vicino a quello che sembri essere dopo, anche se è ancora un po ‘diverso (almeno se la memoria serve, richiede di registrare un tipo prima che la factory possa creare oggetti di quel tipo).

Non cerco di rispondere a tutte le mie domande, perché credo che sia troppo ampio. Solo un paio di note:

ci sono casi in cui la costruzione di oggetti è abbastanza complessa da giustificare la sua estrazione in un’altra class.

Quella class è in realtà un costruttore , piuttosto che una fabbrica.

Nel caso generale, non voglio forzare gli utenti della fabbrica a essere vincolati all’assegnazione dynamic.

Quindi potresti incapsulare la tua fabbrica in un puntatore intelligente. Credo che in questo modo puoi avere la tua torta e mangiarla anche tu.

Ciò elimina anche i problemi relativi al ritorno per valore.

Conclusione: Realizzare una fabbrica restituendo un object è in effetti una soluzione per alcuni casi (come il vettore 2-D precedentemente menzionato), ma non è ancora una sostituzione generale per i costruttori.

Infatti. Tutti i modelli di progettazione hanno i loro limiti e inconvenienti (specifici della lingua). Si raccomanda di usarli solo quando ti aiutano a risolvere il tuo problema, non per il loro stesso interesse.

Se sei alla ricerca dell’implementazione “perfetta” della fabbrica, beh, buona fortuna.

Modello di fabbrica

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

E se il compilatore non supporta l’ottimizzazione del valore restituito, eliminarlo, probabilmente non contiene molta ottimizzazione …

So che questa domanda ha avuto risposta 3 anni fa, ma potrebbe essere ciò che stavi cercando.

Google ha rilasciato un paio di settimane fa una libreria che consente allocazioni dinamiche facili e flessibili degli oggetti. Eccolo: http://google-opensource.blogspot.fr/2014/01/introducing-infact-library.html

Questa è la mia soluzione in stile c ++ 11. il parametro ‘base’ è per la class base di tutte le sottoclassi. i creatori, sono std :: oggetti funzione per creare istanze di sottoclass, potrebbero essere un legame con la funzione di membro statico della sottoclass ‘create (some args)’. Questo forse non è perfetto ma funziona per me. Ed è un po ‘una soluzione’ generale ‘.

 template  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 obj{creators_[key](args...)}; return obj; } void register_creator(const std::string name, std::function &&creator) { auto key = your_hash_func(name.c_str(), name.size()); creators_[key] = std::move(creator); } protected: std::unordered_map> creators_; }; 

Un esempio di utilizzo.

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