source patterns pattern method making book c++ design-patterns idioms factory-method

patterns - factory pattern c++



Cómo implementar el patrón de método de fábrica en C++ correctamente (10)

En primer lugar, hay casos en que la construcción de objetos es una tarea lo suficientemente compleja como para justificar su extracción a otra clase.

Creo que este punto es incorrecto. La complejidad realmente no importa. La relevancia es lo que hace. Si un objeto se puede construir en un solo paso (no como en el patrón de construcción), el constructor es el lugar correcto para hacerlo. Si realmente necesita otra clase para realizar el trabajo, entonces debe ser una clase auxiliar que se use desde el constructor de todos modos.

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

Hay una solución fácil para esto:

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

La única desventaja es que se ve un poco detallado:

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

Pero lo bueno es que puedes ver inmediatamente qué tipo de coordenadas estás usando y, al mismo tiempo, no tienes que preocuparte por copiar. Si desea copiar, y es caro (como lo demuestra el perfil, por supuesto), es posible que desee utilizar algo como las clases compartidas de Qt para evitar la sobrecarga de copia.

En cuanto al tipo de asignación, la razón principal para usar el patrón de fábrica suele ser el polimorfismo. Los constructores no pueden ser virtuales, e incluso si pudieran, no tendría mucho sentido. Al utilizar la asignación estática o de pila, no puede crear objetos de forma polimórfica porque el compilador necesita saber el tamaño exacto. Así que solo funciona con punteros y referencias. Y devolver una referencia de una fábrica tampoco funciona, porque aunque técnicamente un objeto se puede eliminar por referencia, podría ser bastante confuso y propenso a errores, consulte ¿Es la práctica de devolver una variable de referencia de C ++, mala? por ejemplo. Así que los punteros son lo único que queda, y eso también incluye punteros inteligentes. En otras palabras, las fábricas son más útiles cuando se usan con una asignación dinámica, por lo que puedes hacer cosas como estas:

class Abstract { public: virtual void do() = 0; }; class Factory { public: Abstract *create(); }; Factory f; Abstract *a = f.create(); a->do();

En otros casos, las fábricas solo ayudan a resolver problemas menores como aquellos con sobrecargas que ha mencionado. Sería bueno si fuera posible usarlos de manera uniforme, pero no duele mucho que probablemente sea imposible.

Hay una cosa en C ++ que me ha hecho sentir incómodo durante mucho tiempo, porque honestamente no sé cómo hacerlo, aunque suene simple:

¿Cómo implemento el Método Factory en C ++ correctamente?

Objetivo: hacer posible que el cliente pueda crear una instancia de algún objeto utilizando métodos de fábrica en lugar de los constructores del objeto, sin consecuencias inaceptables y un impacto en el rendimiento.

Por "patrón de método de fábrica", me refiero tanto a métodos de fábrica estáticos dentro de un objeto o métodos definidos en otra clase, como a funciones globales. Simplemente, en general, "el concepto de redireccionar la forma normal de creación de instancias de clase X a cualquier otro lugar que no sea el constructor".

Permítanme repasar algunas posibles respuestas que he pensado.

0) No hagas fábricas, haz constructores.

Esto suena bien (y, a menudo, la mejor solución), pero no es un remedio general. En primer lugar, hay casos en que la construcción de objetos es una tarea lo suficientemente compleja como para justificar su extracción a otra clase. Pero incluso dejando de lado ese hecho, incluso para los objetos simples que usan solo constructores a menudo no sirve.

El ejemplo más simple que conozco es una clase vectorial de 2-D. Tan simple, pero complicado. Quiero poder construirlo tanto desde coordenadas cartesianas como polares. Obviamente, no puedo hacer:

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

Mi forma natural de pensar es entonces:

struct Vec2 { static Vec2 fromLinear(float x, float y); static Vec2 fromPolar(float angle, float magnitude); // ... };

Lo que, en lugar de los constructores, me lleva al uso de métodos estáticos de fábrica ... lo que esencialmente significa que estoy implementando el patrón de fábrica, de alguna manera ("la clase se convierte en su propia fábrica"). Esto se ve bien (y se adaptaría a este caso en particular), pero falla en algunos casos, lo que voy a describir en el punto 2. Sigue leyendo.

otro caso: intentar sobrecargar con dos definiciones de tipo opacas de alguna API (como los GUID de dominios no relacionados, o un GUID y un campo de bits), tipos semánticamente totalmente diferentes (en teoría, sobrecargas válidas) pero que en realidad resultan ser el Lo mismo - como ints sin firmar o punteros nulos.

1) El Camino de Java

Java lo tiene simple, ya que solo tenemos objetos asignados dinámicamente. Hacer una fábrica es tan trivial como:

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 ++, esto se traduce en:

class FooFactory { public: Foo* createFooInSomeWay() { return new Foo(some, args); } };

¿Guay? A menudo, de hecho. Pero entonces, esto obliga al usuario a usar solo la asignación dinámica. La asignación estática es lo que hace complejo a C ++, pero también es lo que a menudo lo hace poderoso. Además, creo que existen algunos objetivos (palabra clave: incrustado) que no permiten la asignación dinámica. Y eso no implica que a los usuarios de esas plataformas les guste escribir OOP limpio.

De todos modos, dejando a un lado la filosofía: en el caso general, no quiero obligar a los usuarios de la fábrica a ser restringidos a una asignación dinámica.

2) Devolución por valor

OK, entonces sabemos que 1) es genial cuando queremos una asignación dinámica. ¿Por qué no agregaremos la asignación estática a eso?

class FooFactory { public: Foo* createFooInSomeWay() { return new Foo(some, args); } Foo createFooInSomeWay() { return Foo(some, args); } };

¿Qué? ¿No podemos sobrecargar por el tipo de retorno? Oh, claro que no podemos. Así que vamos a cambiar los nombres de los métodos para reflejar eso. Y sí, he escrito el ejemplo de código no válido anterior para subrayar cuánto me disgusta la necesidad de cambiar el nombre del método, por ejemplo, porque no podemos implementar un diseño de fábrica independiente del idioma ahora, ya que tenemos que cambiar los nombres, y cada usuario de este código deberá recordar esa diferencia de la implementación de la especificación.

class FooFactory { public: Foo* createDynamicFooInSomeWay() { return new Foo(some, args); } Foo createFooObjectInSomeWay() { return Foo(some, args); } };

OK ... ahí lo tenemos. Es feo, ya que necesitamos cambiar el nombre del método. Es imperfecto, ya que necesitamos escribir el mismo código dos veces. Pero una vez hecho, funciona. ¿Derecha?

Bueno, generalmente. Pero a veces no lo hace. Al crear Foo, realmente dependemos del compilador para que realice la optimización del valor de retorno, ya que el estándar de C ++ es lo suficientemente benévolo como para que los proveedores del compilador no especifiquen cuándo se creará el objeto en el lugar y cuándo se copiará al devolver un Objeto temporal por valor en C ++. Entonces, si Foo es caro para copiar, este enfoque es arriesgado.

¿Y qué si Foo no es copiable en absoluto? Bien doh ( Tenga en cuenta que en C ++ 17 con elección de copia garantizada, no ser copiable ya no es un problema para el código anterior )

Conclusión: crear una fábrica devolviendo un objeto es, de hecho, una solución para algunos casos (como el vector 2-D mencionado anteriormente), pero aún no es un reemplazo general para los constructores.

3) Construcción de dos fases

Otra cosa que alguien probablemente propondría es separar el problema de la asignación de objetos y su inicialización. Esto generalmente resulta en un código como este:

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

Uno puede pensar que funciona como un encanto. El único precio que pagamos en nuestro código ...

Desde que escribí todo esto y lo dejé como el último, también me debe disgustar. :) ¿Por qué?

En primer lugar ... Sinceramente, no me gusta el concepto de construcción de dos fases y me siento culpable cuando lo uso. Si diseño mis objetos con la afirmación de que "si existe, está en estado válido", siento que mi código es más seguro y menos propenso a errores. Me gusta de esa forma.

Tener que abandonar esa convención Y cambiar el diseño de mi objeto con el único propósito de hacer una fábrica es ... bueno, difícil de manejar.

Sé que lo anterior no convencerá a mucha gente, así que déjeme dar algunos argumentos más sólidos. Usando la construcción de dos fases, no puedes:

  • inicializar const o variables miembro de referencia,
  • pasar argumentos a los constructores de la clase base y constructores de objetos miembros.

Y probablemente podría haber algunos inconvenientes más que no puedo recordar en este momento, y ni siquiera me siento particularmente obligado a hacerlo ya que los puntos anteriores me convencen ya.

Entonces: ni siquiera cerca de una buena solución general para implementar una fábrica.

Conclusiones:

Queremos tener una forma de instanciación de objetos que:

  • permitir una instanciación uniforme independientemente de la asignación,
  • dar nombres diferentes y significativos a los métodos de construcción (por lo tanto, no depender de la sobrecarga de argumentos),
  • no introducir un impacto significativo en el rendimiento y, preferiblemente, un golpe significativo en el código, especialmente en el lado del cliente,
  • ser general, como en: posible ser introducido para cualquier clase.

Creo que he probado que las formas que he mencionado no cumplen con esos requisitos.

¿Alguna pista? Por favor, dame una solución, no quiero pensar que este lenguaje no me permitirá implementar correctamente un concepto tan trivial.


Ejemplo de fábrica 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(); } };


¿Ha pensado en no usar una fábrica y, en cambio, hacer un buen uso del sistema de tipos? Puedo pensar en dos enfoques diferentes que hacen este tipo de cosas:

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

Lo que te permite escribir cosas como:

Vec2 v(linear(1.0, 2.0));

Opcion 2:

Puedes usar "etiquetas" como hace el STL con los iteradores y demás. Por ejemplo:

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

Este segundo enfoque le permite escribir código que se ve así:

Vec2 v(1.0, 2.0, linear_coord);

que también es agradable y expresivo y le permite tener prototipos únicos para cada constructor.


En general, estoy de acuerdo con la respuesta aceptada, pero hay una opción de C ++ 11 que no se ha cubierto en las respuestas existentes:

  • Devuelva los resultados del método de fábrica por valor , y
  • Proporcionar un constructor de movimiento barato.

Ejemplo:

struct sandwich { // Factory methods. static sandwich ham(); static sandwich spam(); // Move constructor. sandwich(sandwich &&); // etc. };

Luego puedes construir objetos en la pila:

sandwich mine{sandwich::ham()};

Como subobjetos de otras cosas:

auto lunch = std::make_pair(sandwich::spam(), apple{});

O asignados dinámicamente:

auto ptr = std::make_shared<sandwich>(sandwich::ham());

¿Cuándo podría usar esto?

Si, en un constructor público, no es posible dar inicializadores significativos para todos los miembros de la clase sin algún cálculo preliminar, entonces podría convertir ese constructor en un método estático. El método estático realiza los cálculos preliminares, luego devuelve un resultado de valor a través de un constructor privado que solo realiza una inicialización por miembro.

Digo " podría " porque depende de qué enfoque proporciona el código más claro sin ser innecesariamente ineficiente.


Esta es mi solución de estilo c ++ 11. el parámetro ''base'' es para la clase base de todas las subclases. los creadores, son objetos std :: function para crear instancias de subclase, pueden ser un enlace a su subclase ''función miembro estática'' crear (algunos argumentos) ''. Esto tal vez no sea perfecto pero funciona para mí. Y es una solución algo ''general''.

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 ejemplo de uso.

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


Loki tiene un método de fábrica y una fábrica abstracta . Ambos están documentados (extensivamente) en Modern C ++ Design , por Andei Alexandrescu. El método de fábrica probablemente esté más cerca de lo que parece ser después, aunque aún es un poco diferente (al menos si la memoria sirve, requiere que registres un tipo antes de que la fábrica pueda crear objetos de ese tipo).


No trato de responder a todas mis preguntas, ya que creo que es demasiado amplia. Solo un par de notas:

hay casos en que la construcción de objetos es una tarea lo suficientemente compleja como para justificar su extracción a otra clase.

Esa clase es de hecho un Builder , en lugar de una Fábrica.

En el caso general, no quiero forzar a los usuarios de la fábrica a ser restringidos a una asignación dinámica.

Entonces podría hacer que su fábrica lo encapsule en un puntero inteligente. Creo que de esta manera puedes tener tu pastel y comerlo también.

Esto también elimina los problemas relacionados con el retorno por valor.

Conclusión: crear una fábrica devolviendo un objeto es, de hecho, una solución para algunos casos (como el vector 2-D mencionado anteriormente), pero aún no es un reemplazo general para los constructores.

En efecto. Todos los patrones de diseño tienen sus limitaciones e inconvenientes (específicos del idioma). Se recomienda usarlos solo cuando le ayuden a resolver su problema, no por su propio bien.

Si buscas la implementación de fábrica "perfecta", bueno, buena suerte.


Puede leer una muy buena solución en: http://www.codeproject.com/Articles/363338/Factory-Pattern-in-Cplusplus

La mejor solución es en los "comentarios y discusiones", consulte "No es necesario crear métodos estáticos de Creación".

A partir de esta idea, he hecho una fábrica. Tenga en cuenta que estoy usando Qt, pero puede cambiar QMap y QString para equivalentes estándar.

#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

Uso de la muestra:

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");



Patrón de fábrica

class Point { public: static Point Cartesian(double x, double y); private: };

Y si el compilador no admite la optimización del valor de retorno, córtelo, probablemente no contenga mucha optimización ...