Il y a une chose en C++ qui me met mal à l'aise depuis longtemps, parce que je ne sais vraiment pas comment la faire, même si elle semble simple :
Comment implémenter correctement la méthode Factory en C++ ?
Objectif : permettre au client d'instancier un objet en utilisant des méthodes de fabrique au lieu des constructeurs de l'objet, sans conséquences inacceptables et sans perte de performance.
Par "Factory method pattern", j'entends à la fois les méthodes d'usine statiques à l'intérieur d'un objet ou les méthodes définies dans une autre classe, ou encore les fonctions globales. Plus généralement, "le concept de redirection de la manière normale d'instanciation de la classe X vers un endroit autre que le constructeur".
Permettez-moi de passer en revue quelques réponses possibles auxquelles j'ai pensé.
0) Ne faites pas de fabriques, faites des constructeurs.
Cela semble bien (et c'est même souvent la meilleure solution), mais ce n'est pas un remède général. Tout d'abord, il existe des cas où la construction d'un objet est une tâche suffisamment complexe pour justifier son extraction vers une autre classe. Mais même en mettant ce fait de côté, même pour des objets simples, utiliser uniquement des constructeurs ne suffit souvent pas.
L'exemple le plus simple que je connaisse est une classe de vecteurs 2D. C'est si simple et pourtant si délicat. Je veux pouvoir la construire à la fois à partir de coordonnées cartésiennes et polaires. Évidemment, je ne peux pas le 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);
// ...
};
Ce qui, au lieu de constructeurs, m'amène à utiliser des méthodes de fabrique statiques... ce qui signifie essentiellement que j'implémente le factory pattern, d'une certaine manière ("la classe devient sa propre fabrique"). Cela semble bien (et conviendrait à ce cas particulier), mais échoue dans certains cas, que je vais décrire au point 2. Continuez à lire.
un autre cas : essayer de surcharger par deux typedefs opaques d'une API (comme des GUIDs de domaines non liés, ou un GUID et un bitfield), des types sémantiquement totalement différents (donc - en théorie - des surcharges valides) mais qui s'avèrent en fait être la même chose - comme des ints non signés ou des pointeurs void.
1) La méthode Java
Java est simple, car nous n'avons que des objets alloués de manière dynamique. La création d'une usine est aussi triviale 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 alors - cela oblige l'utilisateur à n'utiliser que l'allocation dynamique. L'allocation statique est ce qui rend le C++ complexe, mais c'est aussi ce qui le rend souvent puissant. De plus, je crois qu'il existe certaines cibles (mot-clé : embarquées) qui ne permettent pas l'allocation dynamique. Et cela n'implique pas que les utilisateurs de ces plateformes aiment écrire de la POO propre.
Quoi qu'il en soit, philosophie mise à part : dans le cas général, je ne veux pas forcer les utilisateurs de la fabrique à être limités à 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 l'allocation statique par-dessus ?
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooInSomeWay() {
return Foo(some, args);
}
};
Quoi ? On ne peut pas surcharger par le type de retour ? Oh, bien sûr qu'on ne peut 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 déteste le besoin de changer le nom de la méthode, par exemple parce que nous ne pouvons pas implémenter une conception de fabrique agnostique au niveau du langage correctement maintenant, puisque nous devons changer les noms - et chaque utilisateur de ce code devra se souvenir de cette différence entre l'implémentation et la spécification.
class FooFactory {
public:
Foo* createDynamicFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooObjectInSomeWay() {
return Foo(some, args);
}
};
OK... nous l'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, ça marche. Pas vrai ?
Eh bien, en général. Mais parfois, ce n'est pas le cas. Lors de la création de Foo, nous dépendons en fait du compilateur pour faire l'optimisation de la valeur de retour à notre place, car la norme C++ est suffisamment bienveillante pour que les fournisseurs de compilateurs ne précisent pas quand l'objet sera créé in-place et quand il sera 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 du tout copiable ? Eh bien, doh. ( Notez qu'en C++17 avec l'élision de copie garantie, le fait de ne pas être copiable n'est plus un problème pour le code ci-dessus. )
Conclusion : Fabriquer une usine en retournant un objet est effectivement une solution pour certains cas (comme le vecteur 2-D mentionné précédemment), mais pas encore un remplacement général des constructeurs.
3) Construction en deux phases
Une autre chose que quelqu'un pourrait probablement proposer est de séparer la question de l'allocation des objets et de leur initialisation. Cela donne généralement un code comme celui-ci :
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 à payer dans notre code...
Puisque j'ai écrit tout cela et que j'ai laissé celui-ci comme le dernier, je dois aussi ne pas l'aimer. :) Pourquoi ?
Tout d'abord... Je déteste sincèrement le concept de construction en deux phases et je me sens coupable lorsque je l'utilise. Si je conçois mes objets avec l'assertion que "s'il existe, il est dans un état valide", j'ai le sentiment que mon code est plus sûr et moins sujet aux erreurs. C'est ce que j'aime.
Devoir abandonner cette convention ET modifier la conception de mon objet dans le seul but d'en faire une usine est bien lourd.
Je sais que ce qui précède ne convaincra pas grand monde, alors donnons des arguments plus solides. En utilisant la construction en deux phases, vous ne pouvez pas :
- initialiser
const
ou des variables membres de référence, - passer des arguments aux constructeurs de la 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 pour l'instant, et je ne me sens même pas particulièrement obligé de le faire puisque les points ci-dessus m'ont déjà convaincu.
Donc : on est loin d'une bonne solution générale pour la mise en œuvre d'une usine.
Conclusions :
Nous voulons avoir une méthode d'instanciation d'objet qui.. :
- permettent une instanciation uniforme indépendamment de l'allocation,
- donner des noms différents et significatifs aux méthodes de construction (donc ne pas s'appuyer sur la surcharge par argument),
- ne pas introduire une baisse significative des performances et, de préférence, un gonflement significatif du code, 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 les moyens que j'ai mentionnés ne remplissent pas ces conditions.
Des conseils ? 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.
0 votes
Duplicata de stackoverflow.com/questions/4992307/
7 votes
@Zac, bien que le titre soit très similaire, les questions réelles sont, à mon avis, différentes.
3 votes
Bon duplicata mais le texte de este La question est précieuse en soi.
0 votes
Ne voudriez-vous pas un constructeur privé ? En réfléchissant à ce problème il y a quelque temps, j'ai pensé que la meilleure façon d'encapsuler la création d'objets était d'utiliser une classe de gestionnaire séparée, qui était un ami d'un constructeur privé. Cela forcerait tout objet extérieur à passer par les fonctions de la fabrique que le gestionnaire possède.
0 votes
@sellibitze- non, plutôt "allez, vous êtes une marque de voiture tellement respective, comment se fait-il que je ne trouve pas une seule voiture qui ne soit pas cassée ? ils vendent de bonnes voitures partout ailleurs et j'attends la même chose de vous".
0 votes
Je pense que votre premier exemple décrit vraiment l'idiome "constructeur nommé" et non un modèle de fabrique ( fr.wikibooks.org/wiki/More_C%2B%2B_Idioms/Named_Constructor )
0 votes
@Peter, @dmckee : Je conviens que cette question est plus approfondie, mais il s'agit néanmoins essentiellement de la même question : "Comment puis-je utiliser correctement le design pattern factory en C++ ?"
0 votes
@Zac : Le point de mon commentaire est que cette question devrait rester non supprimée et nonmergée même si elle est fermée en tant que duplicata.
0 votes
Umm, tu ne voulais pas dire : Foo* FooFactory::createDynamicFooInSomeWay() ; Foo FooFactory::createStaticFooInSomeWay()
0 votes
J'ai pris la liberté de changer l'exemple dans la question originale. J'allais appeler le deuxième createStaticFooInSomeWay(), mais ce n'était pas correct - c'est plutôt createDynamicallyAllocatedCFooInSomeWayAndReturnPtr(), et createFooValueAndReturnCopy(), mais ceux-ci sont trop verbeux. (S'il existe une convention de dénomination standard pour ce genre de choses, j'aimerais en entendre parler).
0 votes
@KrazyGlew Merci pour l'édition, c'est une bonne prise.
7 votes
Deux ans après avoir posé cette question, j'ai quelques points à ajouter : 1) Cette question est pertinente pour plusieurs patrons de conception ([abstrait] factory, builder, vous les nommez, je n'aime pas me plonger dans leur taxonomie). 2) La question qui est discutée ici est "comment découpler proprement l'allocation du stockage des objets de la construction des objets".
0 votes
Hum, votre paragraphe 2) ne décrit pas
static
allocation, c'est l'allocation de la pile, alias locale, aliasauto
en C++0 votes
L'option d'allocation dynamique que vous donnez en C++ ne va-t-elle pas créer une fuite de mémoire ?
1 votes
@Dennis : seulement si vous ne le faites pas.
delete
il. Ce type de méthodes est parfaitement acceptable, tant qu'il est "documenté" (le code source est de la documentation ;-) ) que l'appelant est propriétaire du pointeur (lire : est responsable de sa suppression le cas échéant).2 votes
@Boris @Dennis vous pourriez aussi le rendre très explicite en retournant un
unique_ptr<T>
au lieu deT*
.0 votes
Je ne comprends pas l'intérêt d'une factory quand on n'utilise pas de pointeurs : si on n'utilise pas de pointeurs (et donc le polymorphisme), alors on doit connaître le type concret quand on écrit le code. Alors pourquoi avez-vous besoin d'une fabrique ? Et si vous pensez à mettre l'objet (de la pile) dans un pointeur, eh bien, c'est un peu compliqué et je ne vois pas où/quand cela pourrait être pratique (mais il ne faut jamais dire jamais).
0 votes
@bartgol Je me trouve dans cette position car je veux unittester une classe qui crée des objets à l'intérieur de certaines de ses méthodes, et je n'ai pas de tas. J'ai donc besoin d'une allocation de pile, et je dois pouvoir varier les types selon qu'il s'agit de code de production ou de test.