Il y a une chose en C ++ qui me met mal à l'aise depuis assez longtemps, car honnêtement, je ne sais pas comment le faire, même si cela semble simple:
Comment implémenter correctement la méthode d'usine en C ++?
Objectif: permettre au client d'instancier un objet en utilisant des méthodes d'usine au lieu des constructeurs de l'objet, sans conséquences inacceptables et sans impact sur les performances.
Par «modèle de méthode d'usine», je veux dire à la fois des méthodes d'usine statiques à l'intérieur d'un objet ou des méthodes définies dans une autre classe, ou des fonctions globales. Généralement, "le concept de redirection de la manière normale d'instanciation de la classe X vers n'importe où ailleurs que le constructeur".
Permettez-moi de parcourir quelques réponses possibles auxquelles j'ai pensé.
0) Ne faites pas d'usines, faites des constructeurs.
Cela semble agréable (et en effet 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'objets est une tâche suffisamment complexe pour justifier son extraction dans une autre classe. Mais même en mettant ce fait de côté, même pour des objets simples utilisant simplement des constructeurs ne suffira pas.
L'exemple le plus simple que je connaisse est une classe de vecteur 2D. Si simple, mais délicat. Je veux pouvoir le construire à la fois à partir des 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 de penser naturelle 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 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. Continuez à lire.
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 champ de bits), des types sémantiquement totalement différents (donc - en théorie - des surcharges valides) mais qui s'avèrent en fait être les même chose - comme des entiers non signés ou des pointeurs vides.
1) La voie 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 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 pense qu'il existe des cibles (mot-clé: intégré) qui ne permettent pas d'allocation dynamique. Et cela n'implique pas que les utilisateurs de ces plateformes aiment écrire des POO propres.
Quoi qu'il en soit, la philosophie mise à 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, nous savons donc que 1) est cool quand nous voulons une allocation dynamique. Pourquoi ne pas ajouter une allocation statique en plus de cela?
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, nous ne pouvons pas. Modifions donc les noms de méthode 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 indépendante du langage, car nous devons changer les noms - et chaque utilisateur de ce code devra se souvenir de cette différence d'implémentation par rapport à 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 deux fois le même code. Mais une fois terminé, cela fonctionne. Droite?
Enfin, d'habitude. Mais parfois non. Lors de la création de Foo, nous dépendons en fait du compilateur pour effectuer l'optimisation de la valeur de retour pour nous, car la norme C ++ est suffisamment bienveillante pour que les fournisseurs du compilateur ne spécifient pas quand l'objet créé en place et quand sera-t-il copié lors du retour d'un objet temporaire par valeur en C ++. Donc, si Foo coûte cher à copier, cette approche est risquée.
Et si Foo n'est pas du tout copiable? Et bien. ( Notez qu'en C ++ 17 avec élision de copie garantie, ne pas être copiable n'est plus un problème pour le code ci-dessus )
Conclusion: Faire une usine en retournant un objet est en effet une solution pour certains cas (comme le vecteur 2D précédemment mentionné), mais toujours pas un remplacement général pour les constructeurs.
3) Construction en deux phases
Une autre chose que quelqu'un trouverait probablement est de séparer la question de l'allocation des objets et de son initialisation. Cela se traduit généralement par 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 que nous payons dans notre code ...
Depuis que j'ai écrit tout cela et que je l'ai laissé comme dernier, je dois aussi ne pas l'aimer. :) Pourquoi?
Tout d'abord ... Je n'aime pas sincèrement le concept de construction en deux phases et je me sens coupable quand je l'utilise. Si je conçois mes objets en affirmant que "s'il existe, il est dans un état valide", je pense que mon code est plus sûr et moins sujet aux erreurs. Je l'aime comme ça.
Devoir abandonner cette convention ET changer la conception de mon objet dans le seul but d'en faire une usine est… enfin, lourd.
Je sais que ce qui précède ne convaincra pas beaucoup de gens, alors laissez-moi vous donner quelques arguments plus solides. En utilisant une construction en deux phases, vous ne pouvez pas:
- initialiser
const
ou référencer les variables membres, - passer 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é de le faire car les points ci-dessus me convaincent déjà.
Donc: pas même proche d'une bonne solution générale pour l'implantation d'une usine.
Conclusions:
Nous voulons avoir un moyen d'instanciation d'objet qui:
- permettre une instanciation uniforme quelle que soit l'allocation,
- donner des noms différents et significatifs aux méthodes de construction (donc ne pas compter sur la surcharge par argument),
- ne pas introduire un hit significatif de performance et, de préférence, un hit significatif de ballonnement de code, en particulier côté client,
- être général, comme dans: peut être introduit pour n'importe quelle classe.
Je crois avoir prouvé que les moyens que j'ai mentionnés ne répondent pas à ces exigences.
Des indices? Veuillez me fournir une solution, je ne veux pas penser que ce langage ne me permettra pas de mettre correctement en œuvre un concept aussi trivial.
delete
. Ces types de méthodes sont parfaitement adaptés, tant qu'il est "documenté" (le code source est la documentation ;-)) que l'appelant s'approprie le pointeur (lire: est responsable de le supprimer le cas échéant).
unique_ptr<T>
au lieu de T*
.