web-dev-qa-db-fra.com

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 assez longtemps, parce que honnêtement, je ne sais pas comment le faire, même si cela semble simple:

Comment implémenter la méthode Factory en C++ correctement?

Objectif: permettre au client d’instancier un objet à l’aide de méthodes usine à la place des constructeurs de l’objet, sans conséquences inacceptables et sans perte de performances.

Par "modèle de méthode d'usine", j'entends les méthodes d'usine statiques à l'intérieur d'un objet ou les méthodes définies dans une autre classe, ou les fonctions globales. Simplement en général "le concept de redirection de la manière habituelle d'instanciation de classe X vers n'importe quel endroit autre que le constructeur".

Permettez-moi de parcourir certaines des réponses possibles auxquelles j'ai pensé.


0) Ne faites pas d'usines, faites des constructeurs.

Cela sonne bien (et 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 dans une autre classe. Mais même en mettant cela de côté, même pour de simples objets utilisant uniquement des constructeurs, cela ne suffit souvent pas.

L'exemple le plus simple que je connaisse est une classe de vecteur 2D. Si simple, mais difficile. Je veux pouvoir 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);
    // ...
};

Ce qui, au lieu de constructeurs, m'amène à utiliser des méthodes d'usine statiques ... ce qui signifie essentiellement que je mets en œuvre 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.

autre cas: essayer de surcharger par deux types de caractères opaques de certaines API (tels que 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 - surcharges valides) mais qui se révèlent être la même chose - comme des ints non signés ou des pointeurs vides


1) Le chemin 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é: incorporé) qui ne permettent pas l’allocation dynamique. Et cela ne signifie pas que les utilisateurs de ces plates-formes aiment écrire de la programmation orientée objet propre.

Quoi qu’il en soit, à part la philosophie: dans le cas général, je ne veux pas forcer les utilisateurs de l’usine à se limiter à une allocation dynamique.


2) retour par valeur

OK, donc nous savons que 1) est cool quand on veut une allocation dynamique. Pourquoi n'allons-nous 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);
    }
};

Quelle? Nous ne pouvons pas surcharger par le type de retour? Oh, bien sûr que nous ne pouvons pas. Alors changeons les noms de méthodes pour refléter cela. Et oui, j'ai écrit l'exemple de code invalide ci-dessus juste pour souligner combien je n'aime pas le besoin 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 de la langue, car nous devons changer de nom - et chaque utilisateur de ce code devra garder à l'esprit 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 le même code deux fois. Mais une fois fait, ça marche. Droite?

Eh bien, d'habitude. Mais parfois ce n'est pas le cas. Lors de la création de Foo, nous comptons sur le compilateur pour qu'il optimise la valeur de retour, car la norme C++ est suffisamment bienveillante pour que les fournisseurs du compilateur ne spécifient pas quand l'objet créé sera remplacé ni quand il sera copié lors du renvoi d'un objet. objet temporaire par valeur en C++. Donc, si Foo coûte cher à copier, cette approche est risquée.

Et si Foo n'est pas copiable du tout? Bien, doh. ( Notez qu'en C++ 17 avec élision de copie garantie, le fait de ne pas être copiable n'est plus un problème pour le code ci-dessus )

Conclusion: créer une usine en renvoyant un objet est certes une solution dans certains cas (comme le vecteur 2D précédemment mentionné), mais ne constitue toujours pas un remplacement général pour les constructeurs.


3) construction en deux phases

Une autre chose que quelqu'un proposerait probablement serait de séparer la question de l'allocation d'objet et de son initialisation. Cela donne généralement le code suivant:

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é ceci en dernier, je dois aussi ne pas aimer. :) Pourquoi?

Tout d'abord ... je n'aime pas 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’affirmation que "s’il existe, il est en état de validité", j’ai le sentiment que mon code est plus sûr et moins sujet aux erreurs. Je l'aime comme ça.

Devoir abandonner cette convention ET changer le design de mon objet dans le seul but d'en faire une usine est… bien, difficile à manier.

Je sais que ce qui précède ne convaincra pas beaucoup de gens, alors laissez-moi vous donner des arguments plus solides. En utilisant une construction en deux phases, vous ne pouvez pas:

  • initialise const ou variables de membre de référence,
  • transmettre des arguments aux constructeurs de la classe de base et des membres.

Et probablement, il pourrait y avoir d'autres inconvénients auxquels je ne peux pas penser pour le moment, et je ne me sens même pas particulièrement obligé de le faire puisque les points ci-dessus me convainquent déjà.

Donc: même pas près d'une bonne solution générale pour la mise en place 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 (en évitant de surcharger l'argument par argument),
  • n'introduit pas un impact significatif sur les performances et, de préférence, un impact significatif sur le code, en particulier chez le 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 répondent pas à ces exigences.

Des allusions? S'il vous plaît, donnez-moi une solution, je ne veux pas penser que ce langage ne me permettra pas de mettre en œuvre correctement un concept aussi trivial.

306
Kos

Tout d'abord, il existe des cas où la construction d'un objet est une tâche suffisamment complexe pour justifier son extraction dans une autre classe.

Je crois que ce point est incorrect. La complexité n'a pas d'importance. La pertinence est ce qui fait. Si un objet peut être construit en une étape (contrairement au modèle de générateur), le constructeur est le bon endroit pour le faire. Si vous avez vraiment besoin d'une autre classe pour effectuer le travail, il devrait s'agir d'une classe d'assistance utilisée depuis le constructeur.

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

Il existe une solution de contournement simple 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 que cela a l'air un peu prolixe:

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

Mais la bonne chose est que vous pouvez voir immédiatement quel type de coordonnées vous utilisez, sans avoir à vous soucier de la copie. Si vous voulez copier, et que cela coûte cher (comme le prouve le profilage, bien sûr), vous pouvez utiliser quelque chose du genre classes partagées de Qt pour éviter la copie.

En ce qui concerne le type d'allocation, la principale raison d'utiliser le modèle d'usine est généralement le polymorphisme. Les constructeurs ne peuvent pas être virtuels, et même s'ils le pouvaient, cela n'aurait aucun sens. Lors de l'utilisation d'allocation statique ou de pile, vous ne pouvez pas créer d'objets de manière polymorphe car le compilateur doit connaître la taille exacte. Donc, cela ne fonctionne qu'avec des pointeurs et des références. Et renvoyer une référence depuis une usine ne fonctionne pas aussi, car bien qu’un objet techniquement puisse être supprimé par référence, il pourrait être assez déroutant et enclin, voir La pratique consistant à renvoyer une variable de référence C++ est-elle diabolique? par exemple. Les pointeurs sont donc la seule chose qui reste, et cela inclut également les pointeurs intelligents. En d'autres termes, les usines sont plus utiles lorsqu'elles sont utilisées avec une allocation dynamique. Vous pouvez donc effectuer les opérations suivantes:

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 tels que ceux avec des surcharges que vous avez mentionnés. Ce serait bien s'il était possible de les utiliser de manière uniforme, mais cela ne fait pas beaucoup de mal que c'est probablement impossible.

98
Sergei Tachenov

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

Avez-vous pensé à ne pas utiliser d'usine du tout et à utiliser plutôt le système de typage à Nice? Je peux penser à 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) { /* ... */ }
};

Ce qui vous permet d'écrire des choses comme:

Vec2 v(linear(1.0, 2.0));

Option 2:

vous pouvez utiliser des "balises" 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 seconde approche vous permet d’écrire du code qui ressemble à ceci:

Vec2 v(1.0, 2.0, linear_coord);

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

38
Evan Teran

Vous pouvez lire une très bonne solution dans: http://www.codeproject.com/Articles/363338/Factory-Pattern-in-Cplusplus

La meilleure solution est sur les "commentaires et discussions", voir "Pas besoin de méthodes Create statiques".

De cette idée, j'ai fait une usine. Notez que j'utilise Qt, mais vous pouvez modifier QMap et QString pour les équivalents en 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");
27
mabg

Je suis principalement d'accord avec la réponse acceptée, mais il existe une option C++ 11 qui n'a pas été traitée dans les réponses existantes:

  • Retourner les résultats de la méthode usine par valeur, et
  • Fournir un bon marché constructeur de déménagement.

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

En tant que 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 l'utiliser?

Si, sur un constructeur public, il n'est pas possible de donner des initialiseurs significatifs à tous les membres de la classe sans quelques calculs préliminaires, je pourrais alors convertir ce constructeur en 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 membre.

Je dis 'pourrait' car cela dépend de l'approche qui donne le code le plus clair sans être inutilement inefficace.

15
mbrcknl

Loki a à la fois une méthode d'usine et une sine abstraite . Les deux sont documentés (en détail) dans Modem C++ Design , par Andei Alexandrescu. La méthode factory est probablement plus proche de ce que vous semblez être après, bien que cela reste un peu différent (du moins si la mémoire est bonne, il est nécessaire d’enregistrer un type avant que l’usine puisse créer des objets de ce type).

11
Jerry Coffin

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

il existe des cas où la construction d'un objet est une tâche suffisamment complexe pour justifier son extraction dans une autre classe.

Cette classe est en fait un constructeur , plutôt qu'une usine.

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

Ensuite, votre usine pourrait 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 rendement par valeur.

Conclusion: créer une usine en renvoyant un objet est certes une solution dans certains cas (comme le vecteur 2D précédemment mentionné), mais ne constitue toujours pas 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 les utiliser uniquement lorsqu'ils vous aident à résoudre votre problème, pas pour vous-même.

Si vous recherchez une implémentation "parfaite" de l'usine, bonne chance.

5
Péter Török

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, laissez tomber, il ne contient probablement pas beaucoup d'optimisation du tout ...

2
Matthieu M.

Ceci est ma solution de style c ++ 11. paramètre 'base' est pour la classe de base de toutes les sous-classes. creators, sont des objets std :: function pour créer des instances de sous-classe, peuvent être une liaison à la fonction membre statique 'create (some args)' de votre sous-classe. Ceci peut ne pas être parfait mais fonctionne pour moi. Et c'est un peu la solution 'générale'.

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

Je sais que cette question a reçu une réponse il y a 3 ans, mais c'est peut-être ce que vous recherchiez.

Google a publié il y a quelques semaines une bibliothèque permettant des allocations d'objets dynamiques simples et flexibles. Le voici: http://google-opensource.blogspot.fr/2014/01/introducing-infact-library.html

1
Florian Richoux