Comment implémenter correctement le modèle de méthode factory en C++
Il y a une chose en C++ qui me met mal à l'aise depuis longtemps, parce que honnêtement je ne sais pas comment le faire, même si cela semble simple:
Comment puis-je implémenter correctement la méthode Factory en C++?
But: permettre au client d'instancier un objet en utilisant des méthodes factory au lieu des constructeurs de l'objet, sans conséquences inacceptables et un hit de performance.
Par " modèle de méthode D'usine", Je veux dire les deux méthodes d'usine statiques dans un objet ou des méthodes définies dans une autre classe, ou des fonctions globales. Juste généralement "le concept de rediriger la manière normale d'instanciation de la classe X vers n'importe où ailleurs que le constructeur".
Laissez-moi parcourir quelques réponses possibles auxquelles j'ai pensé.
0) Ne faites pas d'usines, faites des constructeurs.
Cela semble bien (et en effet souvent la meilleure solution), mais n'est pas un remède général. Tout d'abord, il y a cas où la construction d'objets est une tâche assez complexe pour justifier son extraction vers une autre classe. Mais même en mettant ce fait de côté, même pour les objets simples en utilisant seulement des constructeurs ne le feront souvent pas.
L'exemple le plus simple que je connaisse est une classe de vecteur 2-D. Tellement simple, et pourtant délicat. Je veux être capable de le construire à la fois à partir de coordonnées cartésiennes et polaires. Évidemment, je ne peux pas faire:
struct Vec2 {
Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!
// ...
};
Ma façon naturelle de penser est alors:
struct Vec2 {
static Vec2 fromLinear(float x, float y);
static Vec2 fromPolar(float angle, float magnitude);
// ...
};
Qui, au lieu de constructeurs, conduit moi à l'utilisation des méthodes d'usine statiques... ce qui signifie essentiellement que j'implémente le modèle d'usine, d'une certaine manière ("la classe devient sa propre usine"). Cela a l'air bien (et conviendrait à ce cas particulier), mais échoue dans certains cas, que je vais décrire au point 2. Ne lisez la suite.
un autre cas: essayer de surcharger par deux typedefs opaques de certaines API (comme les GUID de domaines non liés, ou un GUID et un bitfield), types sémantiquement totalement différents (donc-en théorie - surcharges valides) mais qui s'avèrent être la même chose - comme des ints non signés ou des pointeurs vides.
1) La Manière Java
Java est simple, car nous n'avons que des objets alloués dynamiquement. Faire une usine est aussi trivial que:
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);
}
}
En C++, cela se traduit par:
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
};
Cool? Souvent, en effet. Mais cela oblige l'utilisateur à utiliser l'allocation dynamique. L'allocation statique est ce qui rend c++ complexe, mais c'est aussi ce qui le rend souvent puissant. Aussi, Je croyez qu'il existe des cibles (mot-clé: embedded) qui ne permettent pas l'allocation dynamique. Et cela n'implique pas que les utilisateurs de ces plates-formes aiment écrire la POO propre.
De toute façon, la philosophie à part: dans le cas général, je ne veux pas forcer les utilisateurs de l'usine à être restreints à l'allocation dynamique.
2) Retour par valeur
OK, donc nous savons que 1) est cool quand nous voulons une allocation dynamique. Pourquoi ne pas ajouter une allocation statique au-dessus de qui?
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooInSomeWay() {
return Foo(some, args);
}
};
Quoi? Nous ne pouvons pas surcharger par le type de retour? Oh, bien sûr que nous ne pouvons pas. alors changeons les noms des méthodes pour refléter cela. Et oui, j'ai écrit l'exemple de code invalide ci-dessus juste pour souligner à quel point je n'aime pas la nécessité de changer le nom de la méthode, par exemple parce que nous ne pouvons pas implémenter correctement une conception d'usine agnostique, puisque nous devons changer les noms-et chaque utilisateur de ce code devra se souvenir de cette différence spécification.
class FooFactory {
public:
Foo* createDynamicFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooObjectInSomeWay() {
return Foo(some, args);
}
};
OK... là, nous avons. C'est moche, car nous devons changer le nom de la méthode. C'est imparfait, car nous devons écrire le même code deux fois. Mais une fois fait, il fonctionne. Droit?
Eh bien, d'habitude. Mais parfois il ne le fait pas. Lors de la création de Foo, Nous dépendons en fait du compilateur pour nous faire l'optimisation de la valeur de retour, car le standard c++ est suffisamment bienveillant pour que les fournisseurs du compilateur ne spécifient pas quand l'objet sera créé sur place et quand sera-t-il copié lors du retour d'un objet temporaire par valeur en C++. Donc, si Foo est coûteux à copier, cette approche est risquée.
Et si Foo n'est pas copiable du tout? Eh bien, doh. (notez qu'en C++17 avec une élision de copie garantie, ne pas être copiable ne pose plus de problème pour le code ci-dessus )
Conclusion: faire une fabrique en retournant un objet est en effet une solution pour certains cas (comme le vecteur 2-D précédemment mentionné), mais toujours pas un remplacement général pour constructeur.
3) construction en deux phases
Une autre chose que quelqu'un pourrait probablement trouver est de séparer la question de l'allocation d'objet et de son initialisation. Cela se traduit généralement par un code comme ceci:
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());
// ...
}
On peut penser que cela fonctionne comme un charme. Le seul prix que nous payons dans notre code...
Depuis que j'ai écrit tout cela et que j'ai laissé cela comme le dernier, je ne dois pas l'aimer aussi. :) Pourquoi?
Tout d'Abord... Je n'aime sincèrement pas le concept de construction en deux phases et je me sens coupable quand je l'utilise. Si je conçois mes objets avec l'affirmation que "s'il existe, il est dans un état valide", je pense que mon code est plus sûr et moins sujet aux erreurs. J'aime cette façon.
Devoir abandonner cette convention et changer la conception de mon objet juste dans le but d'en faire l'usine est.. bien, lourd.
Je sais que ce qui précède ne convaincra pas beaucoup de gens, alors permettez-moi de donner des arguments plus solides. En utilisant la construction en deux phases, vous ne peut pas:
- initialise
const
ou les variables de membre de référence, - transmet des arguments aux constructeurs de classe de base et aux constructeurs d'objets membres.
Et il pourrait probablement y avoir d'autres inconvénients auxquels je ne peux pas penser en ce moment, et je ne me sens même pas particulièrement obligé puisque les points ci-dessus me convainquent déjà.
Donc: même pas proche d'une bonne solution générale pour implémenter une usine.
Conclusions:
Nous voulons avoir un moyen d'instanciation d'objet qui serait:
- permet une instanciation uniforme indépendamment de l'allocation,
- donne des noms différents et significatifs aux méthodes de construction (ne s'appuyant donc pas sur une surcharge par-argument),
- ne pas introduire un hit de performance significatif et, de préférence, un hit de ballonnement de code significatif, en particulier du côté client,
- être général, comme dans: possible d'être introduit pour n'importe quelle classe.
Je crois avoir prouvé que le les moyens que j'ai mentionnés ne remplissent pas ces exigences.
Des indices? Veuillez me fournir une solution, je ne veux pas penser que ce langage ne me permettra pas d'implémenter correctement un concept aussi trivial.
10 réponses
Tout d'abord, il y a des cas où la construction d'objets est un complexe de tâches assez pour justifier son extraction à une autre classe.
Je crois que ce point est incorrect. La complexité n'a pas vraiment d'importance. La pertinence est ce qui fait. Si un objet peut être construit en une seule étape (pas comme dans le modèle builder), le constructeur est le bon endroit pour le faire. Si vous avez vraiment besoin d'une autre classe pour effectuer le travail, alors il devrait s'agir d'une classe d'aide utilisée à partir du constructeur de toute façon.
Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!
Il y a une solution de contournement facile pour cela:
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);
Le seul inconvénient est qu'il semble un peu verbeux:
Vec2 v2(Vec2::Cartesian(3.0f, 4.0f));
Mais la bonne chose est que vous pouvez immédiatement voir quel type de coordonnées vous utilisez, et en même temps vous n'avez pas à vous soucier de la copie. Si vous voulez copier, et c'est cher (comme prouvé par le profilage, bien sûr), vous pouvez utiliser quelque chose comme Les classes partagées de Qt pour éviter de copier des frais généraux.
Comme pour l'attribution type, la principale raison d'utiliser le modèle de fabrique est généralement de polymorphisme. Les constructeurs ne peuvent pas être virtuels, et même s'ils le pouvaient, cela n'aurait pas beaucoup de sens. Lors de l'utilisation de l'allocation statique ou de la pile, vous ne pouvez pas créer d'objets de manière polymorphe car le compilateur doit connaître la taille exacte. Donc, il ne fonctionne qu'avec des pointeurs et des références. Et renvoyer une référence d'une usine ne fonctionne pas aussi, car alors qu'un objet techniquement peut être supprimé par référence, il pourrait être plutôt déroutant et sujet aux bugs, voir la pratique de retourner une variable de référence C++ est-elle diabolique?, par exemple. Donc, les pointeurs sont la seule chose qui reste, et cela inclut aussi les pointeurs intelligents. En d'autres termes, les usines sont les plus utiles lorsqu'elles sont utilisées avec une allocation dynamique, vous pouvez donc faire des choses comme ceci:
class Abstract {
public:
virtual void do() = 0;
};
class Factory {
public:
Abstract *create();
};
Factory f;
Abstract *a = f.create();
a->do();
Dans d'autres cas, les usines aident simplement à résoudre des problèmes mineurs comme ceux avec des surcharges que vous avez mentionnés. Ce serait bien si il était possible de les utiliser dans un de manière uniforme, mais cela ne fait pas beaucoup de mal que c'est probablement impossible.
Exemple D'Usine Simple:
// 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();
}
};
Avez-vous pensé à ne pas utiliser une usine du tout, et à la place faire un bon usage du système de type? Je pense à deux approches différentes qui font ce genre de chose:
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) { /* ... */ }
};
Qui vous permet d'écrire des choses comme:
Vec2 v(linear(1.0, 2.0));
Option 2:
Vous pouvez utiliser des "tags" comme le fait la STL avec les itérateurs et autres. Par exemple:
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 &) { /* ... */ }
};
Cette deuxième approche vous permet d'écrire du code qui ressemble à ceci:
Vec2 v(1.0, 2.0, linear_coord);
Qui est aussi agréable et expressif tout en vous permettant d'avoir des prototypes uniques pour chaque constructeur.
Vous pouvez lire une très bonne solution: http://www.codeproject.com/Articles/363338/Factory-Pattern-in-Cplusplus
La meilleure solution est sur les "commentaires et discussions", voir le "pas besoin de méthodes de création statiques".
De cette idée, j'ai fait une usine. Notez que J'utilise Qt, mais vous pouvez changer QMap et QString pour les équivalents std.
#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
Exemple d'utilisation:
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");
Je suis généralement d'accord avec la réponse acceptée, mais il y a une option C++11 qui n'a pas été couverte dans les réponses existantes:
- renvoie les résultats de la méthode d'usine par valeur , et
- fournir un constructeur move bon marché .
Exemple:
struct sandwich {
// Factory methods.
static sandwich ham();
static sandwich spam();
// Move constructor.
sandwich(sandwich &&);
// etc.
};
Ensuite, vous pouvez construire des objets sur la pile:
sandwich mine{sandwich::ham()};
Comme sous-objets d'autres choses:
auto lunch = std::make_pair(sandwich::spam(), apple{});
Ou alloué dynamiquement:
auto ptr = std::make_shared<sandwich>(sandwich::ham());
Quand pourrais-je utiliser ceci?
Si, sur un constructeur public, il n'est pas possible de donner des initialiseurs significatifs pour tous les membres de la classe sans un calcul préliminaire, alors je pourrais convertir ce constructeur en une méthode statique. La méthode statique effectue les calculs préliminaires, puis renvoie un résultat de valeur via un constructeur privé qui effectue simplement une initialisation par membre.
Je dis ' might ' car cela dépend de l'approche qui donne le code le plus clair sans être inutilement inefficace.
Loki a à la fois un Méthode de Fabrique et Abstract Factory. Les deux sont documentés (largement) dans Modern C++ Design , par Andei Alexandrescu. La méthode factory est probablement plus proche de ce que vous semblez être après, bien qu'elle soit encore un peu différente (au moins si la mémoire sert, elle vous oblige à enregistrer un type avant que l'usine puisse créer des objets de ce type).
Je n'essaie pas de répondre à toutes mes questions, car je crois que c'est trop large. Juste quelques notes:
Il y a des cas où la construction d'objets est une tâche assez complexe pour justifier son extraction vers une autre classe.
Cette classe est en fait un Constructeur, plutôt que d'une Usine.
Dans le cas général, je ne veux pas forcer les utilisateurs de l'usine à être limités à l'allocation dynamique.
, Alors vous pourriez avoir votre usine l'encapsuler dans un pointeur intelligent. Je crois que de cette façon, vous pouvez avoir votre gâteau et le manger aussi.
Cela élimine également les problèmes liés au retour par valeur.
Conclusion: faire une fabrique en retournant un objet est en effet une solution dans certains cas (comme le vecteur 2-D précédemment mentionné), mais pas encore un remplacement général pour les constructeurs.
En effet. Tous les modèles de conception ont leurs contraintes et leurs inconvénients (spécifiques à la langue). Il est recommandé de utilisez-les seulement quand ils vous aident à résoudre votre problème, pas pour eux - mêmes.
Si vous êtes après la mise en œuvre de l'usine" parfaite", Eh bien, bonne chance.
Modèle D'Usine
class Point
{
public:
static Point Cartesian(double x, double y);
private:
};
Et si votre compilateur ne prend pas en charge L'optimisation de la valeur de retour, abandonnez-le, il ne contient probablement pas beaucoup d'optimisation du tout...
Je sais que cette question a été répondue il y a 3 ans, mais c'est peut-être ce que vous cherchiez.
Google a publié il y a quelques semaines une bibliothèque permettant des allocations d'objets dynamiques faciles et flexibles. Voici: http://google-opensource.blogspot.fr/2014/01/introducing-infact-library.html
C'est ma solution de style c++11. le paramètre 'base' est pour la classe de base de toutes les sous-classes. les créateurs, sont des objets std::function pour créer des instances de sous-classe, peuvent être une liaison à votre sous-classe' static member function 'create(some args)'. Ce n'est peut-être pas parfait mais fonctionne pour moi. Et c'est un peu le "général" de la solution.
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_;
};
Un exemple d'utilisation.
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();
}
}