En ingeniería de software, los patrones de diseño creacional se ocupan de los mecanismos de creación de objetos, es decir, tratan de crear objetos de una manera adecuada a la situación. Además de esta forma básica u ordinaria de creación de objetos, podría dar lugar a problemas de diseño o complejidad añadida al diseño.
Factory Design Pattern en C++ ayuda a mitigar este problema al crear objetos usando métodos separados o clases polimórficas .
Por cierto, si no ha consultado mis otros artículos sobre patrones de diseño creativo, aquí está la lista:
- Fábrica
- Constructor
- Prototipo
- único
Los fragmentos de código que ve a lo largo de esta serie de artículos son simplificados, no sofisticados. Por lo tanto, a menudo me ve sin usar palabras clave como anular, final, público (mientras se hereda) solo para hacer que el código sea compacto y consumible (la mayoría de las veces) en un tamaño de pantalla estándar único. También prefiero struct en lugar de class solo para guardar la línea al no escribir "public:" a veces y también pierdo virtual destructor , constructor, copy constructor , prefix std::, eliminando la memoria dinámica, intencionalmente.
También me considero una persona pragmática que quiere transmitir una idea de la manera más simple posible en lugar de la forma estándar o el uso de jergas.
Nota:
- Si tropezó aquí directamente, le sugiero que revise ¿Qué es el patrón de diseño? primero, aunque sea trivial. Creo que te animará a explorar más sobre este tema.
- Todo el código que encuentra en esta serie de artículos está compilado usando C++20 (aunque en la mayoría de los casos he usado funciones de C++ moderno hasta C++17). Entonces, si no tiene acceso al compilador más reciente, puede usar https://wandbox.org/ que también tiene una biblioteca boost preinstalada.
Intención
Para la creación de objetos al por mayor a diferencia del constructor (que crea por partes).
Motivación
- Digamos que tiene una clase de punto que tiene x e y como coordenadas que pueden ser coordenadas cartesianas o polares como se muestra a continuación:
struct Point { Point( float x, float y){ /*...*/ } // Cartesian co-ordinates // Not OK: Cannot overload with same type of arguments // Point(float a, float b){ /*...*/ } // Polar co-ordinates // ... Implementation };
- Esto no es posible, ya que sabrá que no puede crear dos constructores con el mismo tipo de argumentos.
- Al revés es:
enum class PointType { cartesian, polar }; class Point { Point( float a, float b, PointTypetype = PointType::cartesian) { if (type == PointType::cartesian) { x = a; b = y; } else { x = a * cos (b); y = a * sin (b); } } };
- Pero esta no es una forma sofisticada de hacerlo. Más bien, deberíamos delegar la instanciación separada a métodos separados.
Ejemplos de patrones de diseño de fábrica en C++
- Entonces, como puedes adivinar. Vamos a mitigar la limitación del constructor moviendo el proceso de inicialización del constructor a otra estructura. Y vamos a usar el método de fábrica para eso.
- Y tal como sugiere el nombre, utiliza el método o la función miembro para inicializar el objeto.
Método de fábrica
enum class PointType { cartesian, polar }; class Point { float m_x; float m_y; PointType m_type; // Private constructor, so that object can't be created directly Point( const float x, const float y, PointType t) : m_x{x}, m_y{y}, m_type{t} {} public : friend ostream & operator <<(ostream &os, const Point &obj) { return os << "x: " << obj.m_x << " y: " << obj.m_y; } static Point NewCartesian ( float x, float y) { return {x, y, PointType::cartesian}; } static Point NewPolar ( float a, float b) { return {a * cos (b), a * sin (b), PointType::polar}; } }; int main () { // Point p{ 1,2 }; // will not work auto p = Point::NewPolar( 5 , M_PI_4); cout << p << endl ; // x: 3.53553 y: 3.53553 return EXIT_SUCCESS; }
- Como se puede observar en la implementación. En realidad, no permite el uso del constructor y obliga a los usuarios a usar métodos estáticos en su lugar. Y esta es la esencia del Método Factory, es decir, constructor privado y método estático .
Patrón de diseño de fábrica clásica
- Si tiene un código dedicado para la construcción, mientras no lo movemos a una clase dedicada. Y solo para separar las preocupaciones, es decir, el principio de responsabilidad única de los principios de diseño SOLID.
class Point { // ... as it is from above friend class PointFactory ; }; class PointFactory { public : static Point NewCartesian ( float x, float y) { return { x, y }; } static Point NewPolar ( float r, float theta) { return { r* cos (theta), r* sin (theta) }; } };
- Tenga en cuenta que esta no es la fábrica abstracta, es una fábrica concreta. Haciendo la clase amiga de PointFactory de Point hemos violado el Principio Abierto-Cerrado (OCP). Como palabra clave amigo en sí misma contraria a OCP.
Fábrica interior
- Hay una cosa crítica que nos perdimos en nuestra Fábrica de que no existe un vínculo sólido entre PointFactory y Point, lo que confunde al usuario al usar Point solo al ver que todo es privado.
- Entonces, en lugar de diseñar una fábrica fuera de la clase. Simplemente podemos ponerlo en la clase que anima a los usuarios a usar Factory.
- Por lo tanto, también atendemos el segundo problema que es romper el Principio Abierto-Cerrado . Y esto será algo más intuitivo para el usuario al usar Factory.
class Point { float m_x; float m_y; Point( float x, float y) : m_x(x), m_y(y) {} public : struct Factory { static Point NewCartesian ( float x, float y) { return { x,y }; } static Point NewPolar ( float r, float theta) { return { r* cos (theta), r* sin (theta) }; } }; }; int main () { auto p = Point::Factory::NewCartesian( 2 , 3 ); return EXIT_SUCCESS; }
Fábrica abstracta
¿Por qué necesitamos una Fábrica Abstracta?
- C ++ tiene el soporte de destrucción de objetos polimórficos utilizando el destructor virtual de su clase base. Del mismo modo, falta un soporte equivalente para la creación y copia de objetos, ya que С++ no admite constructores virtuales y constructores de copia .
- Además, no puede crear un objeto a menos que conozca su tipo estático, porque el compilador debe saber la cantidad de espacio que necesita asignar. Por la misma razón, la copia de un objeto también requiere que se conozca su tipo en tiempo de compilación.
struct Point { virtual ~Point(){ cout << "~Point\n" ; } }; struct Point2D : Point { ~Point2D(){ cout << "~Point2D\n" ; } }; struct Point3D : Point { ~Point3D(){ cout << "~Point3D\n" ; } }; void who_am_i (Point *who) { // Not sure whether Point2D would be passed here or Point3D // How to `create` the object of same type ie pointed by who ? // How to `copy` object of same type ie pointed by who ? delete who; // you can delete object pointed by who, thanks to virtual destructor }
Ejemplo de patrón de diseño de fábrica abstracta
- Abstract Factory es útil en una situación que requiere la creación de muchos tipos diferentes de objetos, todos derivados de un tipo base común.
- Abstract Factory define un método para crear los objetos, que las subclases pueden anular para especificar el tipo derivado que se creará. Por lo tanto, en el tiempo de ejecución, se llamará al método de fábrica abstracto apropiado según el tipo de objeto al que se hace referencia/señaló y devolverá un puntero de clase base a una nueva instancia de ese objeto.
struct Point { virtual ~Point() = default ; virtual unique_ptr <Point> create() = 0 ; virtual unique_ptr <Point> clone() = 0 ; }; struct Point2D : Point { unique_ptr <Point> create() { return make_unique<Point2D>(); } unique_ptr <Point> clone() { return make_unique<Point2D>(* this ); } }; struct Point3D : Point { unique_ptr <Point> create() { return make_unique<Point3D>(); } unique_ptr <Point> clone() { return make_unique<Point3D>(* this ); } }; void who_am_i (Point *who) { auto new_who = who->create(); // `create` the object of same type ie pointed by who ? auto duplicate_who = who->clone(); // `copy` the object of same type ie pointed by who ? delete who; }
Enfoque funcional del patrón de diseño de fábrica usando C++ moderno
- En nuestro ejemplo de Abstract Factory, hemos seguido el enfoque orientado a objetos, pero hoy en día es igualmente posible adoptar un enfoque más funcional.
- Por lo tanto, construyamos un tipo similar de Factory sin depender de la funcionalidad polimórfica, ya que podría no adaptarse a alguna aplicación con limitaciones de tiempo, como un sistema integrado . Debido a que la tabla virtual y el mecanismo de envío dinámico pueden controlar el sistema durante la funcionalidad crítica.
- Esto es bastante sencillo ya que utiliza funciones funcionales y lambda de la siguiente manera:
struct Point { /* . . . */ }; struct Point2D : Point { /* . . . */ }; struct Point3D : Point { /* . . . */ }; class PointFunctionalFactory { map <PointType, function< unique_ptr <Point>() >> m_factories; public : PointFunctionalFactory() { m_factories[PointType::Point2D] = [] { return make_unique<Point2D>(); }; m_factories[PointType::Point3D] = [] { return make_unique<Point3D>(); }; } unique_ptr <Point> create(PointType type) { return m_factories[type](); } }; int main () { PointFunctionalFactory pf; auto p2D = pf.create(PointType::Point2D); return EXIT_SUCCESS; }
- Si está pensando que estamos haciendo un exceso de ingeniería, tenga en cuenta que la construcción de nuestro objeto es simple aquí solo para demostrar la técnica y también lo hace nuestra función lambda.
- Cuando aumenta la representación de su objeto, se requieren muchos métodos para llamar para instanciar el objeto correctamente, en tal caso, solo necesita modificar la expresión lambda de la fábrica o introducir Builder Design Pattern .
Beneficios del patrón de diseño de fábrica
- Punto/clase único para la creación de diferentes objetos. Por lo tanto, es un software fácil de mantener y comprender.
- Puede crear el objeto sin siquiera saber su tipo utilizando Abstract Factory.
- Aporta una gran modularidad. Imagina programar un videojuego, donde te gustaría agregar nuevos tipos de enemigos en el futuro, cada uno de los cuales tiene diferentes funciones de IA y puede actualizarse de manera diferente. Mediante el uso de un método de fábrica, el controlador del programa puede llamar a la fábrica para crear los enemigos, sin ninguna dependencia o conocimiento de los tipos reales de enemigos. Ahora, los futuros desarrolladores pueden crear nuevos enemigos, con nuevos controles de IA y nuevas funciones de miembros de dibujo, agregarlo a la fábrica y crear un nivel que llama a la fábrica y pregunta por los enemigos por su nombre. Combine este método con una descripción XML de los niveles y los desarrolladores podrían crear nuevos niveles sin tener que volver a compilar su programa. Todo esto, gracias a la separación de la creación de objetos del uso de objetos.
- Le permite cambiar el diseño de su aplicación más fácilmente, esto se conoce como acoplamiento flexible.
Resumen por preguntas frecuentes
¿Cuál es la forma correcta de implementar el patrón de diseño de fábrica en C++?
Abstract Factory & Functional Factory siempre es una buena elección.
¿Fábrica vs Factor abstracto vs Fábrica funcional?
- Fábrica: cree un objeto con una instanciación variada.
- Factor abstracto: cree un objeto sin conocer su tipo y refiéralo usando el puntero y la referencia de la clase base. Acceso mediante métodos polimórficos.
- Fábrica Funcional: Cuando la creación de objetos es más compleja. Patrón de diseño de fábrica abstracta + constructor . Aunque no he incluido Builder en el ejemplo de Functional Factory.
¿Cuál es la diferencia entre Abstract Factory y Builder Design Pattern?
- Factory produce los objetos al por mayor que podrían ser cualquier objeto de la jerarquía de herencia (como Point, Point2D, Point3D). Mientras que Builder se ocupa de la creación de instancias de un objeto que se limita a un solo objeto (aunque esta afirmación aún es discutible).
- Verá que Factory tiene que ver con la creación de objetos al por mayor, mientras que el constructor es la creación de objetos por partes. En ambos patrones, puede separar el mecanismo relacionado con la creación de objetos en otras clases.
¿Cuándo usar el patrón de diseño de fábrica?
Emplee el patrón de diseño de fábrica para crear un objeto con la(s) funcionalidad(es) requerida(s), pero el tipo de objeto permanecerá indeciso o se decidirá a partir de los parámetros dinámicos que se pasan.
Publicado anteriormente en http://www.vishalchovatiya.com/factory-design-pattern-in-modern-cpp/