353 votes

Comment implémenter correctement le modèle de méthode d'usine en C++ ?

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

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.

114voto

Sergey Tachenov Points 8123

Tout d'abord, il ya des cas où construction de l'objet est une tâche complexe 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 de ce fait. Si un objet peut être construit en une seule étape (pas comme dans le générateur de modèle), 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 être une classe helper qui est utilisé à partir du constructeur de toute façon.

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

Il est facile de contourner ce:

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 ressemble un peu verbeux:

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

Mais la bonne chose est que vous pouvez voir immédiatement ce type de coordonnée que vous utilisez, et en même temps vous n'avez pas à vous soucier de la copie. Si vous souhaitez copier, et c'est cher (comme le prouve le profilage, bien sûr), vous pouvez utiliser quelque chose comme Qt partagé de classes afin d'éviter de copier les frais généraux.

Comme pour l'allocation de type, la principale raison d'utiliser le modèle de fabrique est généralement de polymorphisme. Les constructeurs ne peuvent pas être virtuel, et même si on le pouvait, il ne serait pas beaucoup de sens. Lors de l'utilisation de statique ou de l'allocation de pile, vous ne pouvez pas créer des objets dans un polymorphe car le compilateur a besoin de connaître la taille exacte. Donc, il ne fonctionne qu'avec des pointeurs et des références. Et renvoie une référence à partir d'une usine ne fonctionne pas trop, parce qu'un objet techniquement peut être supprimé par référence, il peut être assez déroutant et bug sur le ventre, voir Est la pratique de la restitution de C++ variable de référence, le mal? par exemple. Donc, les pointeurs sont la seule chose qui reste, et qui comprend des pointeurs intelligents. En d'autres termes, les usines sont plus utiles lorsqu'il est utilisé avec l'allocation dynamique, de sorte que vous pouvez 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 juste de l'aide pour résoudre les problèmes mineurs comme ceux avec des surcharges que vous avez mentionnés. Ce serait bien si il était possible de les utiliser d'une manière uniforme, mais il ne fait pas de mal bien qu'il est probablement impossible.

24 votes

+1 pour les structs cartésiens et polaires. Il est généralement préférable de créer des classes et des structures qui représentent directement les données auxquelles elles sont destinées (par opposition à une structure Vec générale). Votre usine est également un bon exemple, mais votre exemple n'indique pas à qui appartient le pointeur 'a'. Si la fabrique 'f' en est le propriétaire, il sera probablement détruit lorsque 'f' quittera le champ d'application, mais si 'f' n'en est pas le propriétaire, il est important que le développeur n'oublie pas de libérer cette mémoire, sinon une fuite de mémoire peut se produire.

1 votes

Bien sûr, un objet peut être supprimé par référence ! Voir stackoverflow.com/a/752699/404734 Cela soulève bien sûr la question de savoir s'il est sage de renvoyer de la mémoire dynamique par référence, à cause du problème de l'assignation potentielle de la valeur de retour par copie (l'appelant pourrait bien sûr aussi faire quelque chose comme int a = *returnsAPoninterToInt() et serait alors confronté au même problème, si de la mémoire dynamique allcoated est renvoyée, comme pour les références, mais dans la version pointeur, l'utilisateur doit explicitement déréférencer au lieu d'oublier de référencer explicitement, pour être faux).

1 votes

@Kaiserludi, bon point. Je n'y avais pas pensé, mais c'est quand même une façon "mauvaise" de faire les choses. J'ai modifié ma réponse pour refléter cela.

53voto

Loki Astari Points 116129

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

2 votes

@LokiAstari Parce que l'utilisation de pointeurs intelligents est le moyen le plus simple de perdre le contrôle de la mémoire. Le contrôle dont les langages C/C++ sont connus pour être suprême par rapport aux autres langages, et dont ils tirent le plus grand avantage. Sans mentionner le fait que les pointeurs intelligents produisent une surcharge mémoire similaire à celle des autres langages gérés. Si vous voulez la commodité de la gestion automatique de la mémoire, commencez à programmer en Java ou en C#, mais ne mettez pas ce désordre en C/C++.

0 votes

Idée très intéressante d'avoir un conteneur avec des objets dans l'usine.

50 votes

@lukasz1985 le unique_ptr dans cet exemple n'a pas de surcharge de performance. La gestion des ressources, y compris la mémoire, est l'un des avantages suprêmes du C++ par rapport à tout autre langage, car vous pouvez le faire sans pénalité de performance et de manière déterministe, sans perdre le contrôle, mais vous dites exactement le contraire. Certaines personnes n'aiment pas les choses que le C++ fait implicitement, comme la gestion de la mémoire par le biais de pointeurs intelligents, mais si ce que vous voulez, c'est que tout soit obligatoirement explicite, utilisez le C ; la contrepartie est des ordres de grandeur de moins de problèmes. Je pense qu'il est injuste de rejeter une bonne recommandation.

42voto

Evan Teran Points 42370

Avez-vous pensé à ne pas utiliser de fabrique du tout, et à faire plutôt bon usage du système de types ? Je peux penser à deux approches différentes qui font ce genre de choses :

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

Ce 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 un code qui ressemble à ceci :

Vec2 v(1.0, 2.0, linear_coord);

qui est également agréable et expressif tout en vous permettant d'avoir des prototypes uniques pour chaque constructeur.

11voto

Jerry Coffin Points 237758

Loki a à la fois un Méthode d'usine et un Usine abstraite . Les deux sont documentés (de manière extensive) dans Conception moderne du C++ par Andei Alexandrescu. La méthode factory est probablement plus proche de ce que vous semblez rechercher, bien qu'elle soit encore un peu différente (du moins si ma mémoire est bonne, elle exige que vous enregistriez un type avant que la factory puisse créer des objets de ce type).

1 votes

Même si elle est dépassée (ce que je conteste), elle est encore parfaitement utilisable. J'utilise toujours une Factory basée sur MC++D's dans un nouveau projet C++14 avec beaucoup d'effet ! De plus, les patterns Factory et Singleton sont probablement les parties les moins dépassées. Alors que des éléments de Loki comme Function et les manipulations de type peuvent être remplacées par std::function y <type_traits> et alors que les lambdas, le threading, les refs rvalue ont des implications qui peuvent nécessiter quelques ajustements mineurs, il n'y a pas de remplacement standard pour les singletons de fabriques tels qu'il les décrit.

5voto

Péter Török Points 72981

Je n'essaie pas de répondre à toutes les questions, car je pense que c'est trop vaste. Juste quelques notes :

il y a des cas où la construction d'objets est une tâche suffisamment complexe pour justifier son extraction vers une autre classe.

Cette classe est en fait une Constructeur plutôt qu'une usine.

Dans le cas général, je ne veux pas obliger les utilisateurs de l'usine à se limiter à une allocation dynamique.

Votre usine pourrait alors l'encapsuler dans un pointeur intelligent. Je pense que de cette façon, vous pouvez avoir le beurre et l'argent du beurre.

Cela élimine également les problèmes liés au retour par valeur.

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.

En effet. Tous les modèles de conception ont leurs contraintes et leurs inconvénients (spécifiques au langage). Il est recommandé de les utiliser uniquement lorsqu'ils vous aident à résoudre votre problème, et non pour leur propre intérêt.

Si vous êtes à la recherche de la mise en œuvre d'usine "parfaite", eh bien, bonne chance.

0 votes

Merci pour la réponse ! Mais pourriez-vous expliquer comment l'utilisation d'un pointeur intelligent permettrait de lever la restriction de l'allocation dynamique ? Je n'ai pas bien compris cette partie.

0 votes

@Kos, avec les pointeurs intelligents, vous pouvez cacher l'allocation/désallocation de l'objet réel à vos utilisateurs. Ils ne voient que l'encapsulation du pointeur intelligent, qui pour le monde extérieur se comporte comme un objet alloué statiquement.

0 votes

@Kos, pas au sens strict, AFAIR. Vous passez l'objet à envelopper, que vous avez probablement alloué dynamiquement à un moment donné. Ensuite, le pointeur intelligent en prend possession et s'assure qu'il est correctement détruit lorsqu'il n'est plus nécessaire (le moment de cette destruction est décidé différemment pour les différents types de pointeurs intelligents).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X