paint-brush
Patrones de diseño: patrón de constructor en C++ modernopor@IndianWestCoast
16,952 lecturas
16,952 lecturas

Patrones de diseño: patrón de constructor en C++ moderno

por Vishal Chovatiya10m2020/07/28
Read on Terminal Reader
Read this story w/o Javascript

Demasiado Largo; Para Leer

Los fragmentos de código que ve a lo largo de esta serie de artículos son simplificados, no sofisticados, no sofisticados. Por lo tanto, a menudo me ve que no uso palabras clave como anular, final, herencia pública solo para hacer que el código sea compacto y consumible en un solo tamaño de pantalla estándar. También prefiero la estructura de clase en lugar de la clase solo para guardar la línea al no escribir "público:" a veces y también pierdo el destructor virtual, el constructor, el constructor de copias, el prefijo estándar::, el prefijo estándar::, eliminando la memoria dinámica, intencionalmente. Creo que te animará a explorar más sobre este tema.

Company Mentioned

Mention Thumbnail
featured image - Patrones de diseño: patrón de constructor en C++ moderno
Vishal Chovatiya HackerNoon profile picture

En ingeniería de software, los patrones de diseño creacional se ocupan de los mecanismos de creación de objetos, tratando de crear objetos de una manera adecuada a la situación. La 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. Builder Design Pattern en C++ resuelve este problema específico al separar la construcción de un objeto complejo de su representación .

Por cierto, si no ha consultado mis otros artículos sobre patrones de diseño creativo, aquí está la lista:

  1. Fábrica
  2. Constructor
  3. Prototipo
  4. ú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 crear/crear instancias de objetos complejos y complicados por partes y de manera sucinta al proporcionar una API en una entidad separada.
  • Builder Design Pattern se utiliza cuando queremos construir un objeto complejo. Sin embargo, no queremos tener un miembro constructor complejo o uno que necesite muchos argumentos.
  • El patrón de diseño del constructor construye un objeto complejo paso a paso y el paso final devolverá el objeto. El proceso de construcción de un objeto debe ser genérico para que pueda usarse para crear diferentes representaciones del mismo objeto con la ayuda de una variedad de métodos.

La vida sin constructores

  • Supongamos que tiene que crear el generador de HTML usando C ++, entonces una forma muy ingenua de hacerlo es:
 // <p>hello</p> auto text = "hello" ; string output; output += "<p>" ; output += text; output += "</p>" ; printf ( "<p>%s</p>" , text); // <ul><li>hello</li><li>world</li></ul> string words[] = { "hello" , "world" }; ostringstream oss; oss << "<ul>" ; for ( auto w : words) oss << " <li>" << w << "</li>" ; oss << "</ul>" ; printf (oss.str().c_str());
  • Un desarrollador sofisticado creará una clase con un montón de argumentos y métodos de construcción para agregar un nodo secundario. Sin embargo, este es un buen enfoque, pero puede complicar la representación del objeto.
  • En general, algunos objetos son simples y se pueden crear en una sola llamada de constructor, mientras que otros objetos requieren muchas ceremonias para crear.
  • Tener un objeto con 10 argumentos constructores no es productivo. En su lugar, deberíamos optar por la construcción por partes.
  • Builder proporciona una API para construir un objeto paso a paso sin revelar la representación real del objeto.

Ejemplo de patrón de diseño de constructor en C++ moderno

 class HtmlBuilder ; class HtmlElement { string m_name; string m_text; vector <HtmlElement> m_childs; constexpr static size_t m_indent_size = 4 ; HtmlElement() = default ; HtmlElement( const string &name, const string &text) : m_name(name), m_text(text) {} friend class HtmlBuilder ; public : string str ( int32_t indent = 0 ) { ostringstream oss; oss << string (m_indent_size * indent, ' ' ) << "<" << m_name << ">" << endl ; if (m_text.size()) oss << string (m_indent_size * (indent + 1 ), ' ' ) << m_text << endl ; for ( auto &element : m_childs) oss << element.str(indent + 1 ); oss << string (m_indent_size * indent, ' ' ) << "</" << m_name << ">" << endl ; return oss.str(); } static unique_ptr <HtmlBuilder> build( string root_name) { return make_unique<HtmlBuilder>(root_name); } }; class HtmlBuilder { HtmlElement m_root; public : HtmlBuilder( string root_name) { m_root.m_name = root_name; } HtmlBuilder * add_child ( string child_name, string child_text) { m_root.m_childs.emplace_back(HtmlElement{child_name, child_text}); return this ; } string str () { return m_root.str(); } operator HtmlElement () { return m_root; } }; int main () { auto builder = HtmlElement::build( "ul" ); builder->add_child( "li" , "hello" )->add_child( "li" , "world" ); cout << builder->str() << endl ; return EXIT_SUCCESS; } /* <ul> <li> hello </li> <li> world </li> </ul> */
  • Estamos obligando a los usuarios aquí a usar el constructor al hacer que los datos de los miembros de HtmlElements sean privados.
  • Como puede ver, hemos declarado HtmlBuilder y HtmlElement en el mismo archivo y, para hacerlo, necesitamos una declaración directa, es decir, clase HtmlBuilder; ya que es un tipo incompleto . Y no podemos crear el objeto de tipo incompleto antes de que el compilador analice su declaración real. La razón es simple, el compilador necesita el tamaño de un objeto para asignarle memoria. Por lo tanto, el puntero es la única forma de evitarlo, por lo que hemos tomado unique_ptr <HtmlBuilder>.

Ejemplo de patrón de diseño de constructor sofisticado y fluido

  • A continuación se muestra el ejemplo más sofisticado del patrón de diseño de generador en C++ organizado en cuatro archivos diferentes (es decir, Person.h, Person.cpp, PersonBuilder.h PersonBuilder.cpp).

Persona.h

 # pragma once # include <iostream> using namespace std ; class PersonBuilder ; class Person { std :: string m_name, m_street_address, m_post_code, m_city; // Personal Detail std :: string m_company_name, m_position, m_annual_income; // Employment Detail Person( std :: string name) : m_name(name) {} public : friend class PersonBuilder ; friend ostream& operator <<(ostream& os, const Person& obj); static PersonBuilder create ( std :: string name) ; };

Persona.cpp

 # include <iostream> # include "Person.h" # include "PersonBuilder.h" PersonBuilder Person::create( string name) { return PersonBuilder{name}; } ostream& operator <<(ostream& os, const Person& obj) { return os << obj.m_name << std :: endl << "lives : " << std :: endl << "at " << obj.m_street_address << " with postcode " << obj.m_post_code << " in " << obj.m_city << std :: endl << "works : " << std :: endl << "with " << obj.m_company_name << " as a " << obj.m_position << " earning " << obj.m_annual_income; }
  • Como puede ver en el ejemplo anterior, la persona puede tener muchos detalles como Personal y Profesional. Y también lo hace el recuento de miembros de datos.
  • En nuestro caso, hay 7 miembros de datos. Tener una sola clase para todas las acciones necesarias para crear una Persona a través del constructor podría hacer que nuestra clase se hinche y pierda su propósito original. Además, el usuario de la biblioteca debe cuidar toda la secuencia de parámetros del constructor.

PersonBuilder.h

 # pragma once # include "Person.h" class PersonBuilder { Person person; public : PersonBuilder( string name) : person(name) {} operator Person () const { return move(person); } PersonBuilder& lives () ; PersonBuilder& at ( std :: string street_address) ; PersonBuilder& with_postcode ( std :: string post_code) ; PersonBuilder& in ( std :: string city) ; PersonBuilder& works () ; PersonBuilder& with ( string company_name) ; PersonBuilder& as_a ( string position) ; PersonBuilder& earning ( string annual_income) ; };

PersonBuilder.cpp

 # include "PersonBuilder.h" PersonBuilder& PersonBuilder::lives() { return * this ; } PersonBuilder& PersonBuilder::works() { return * this ; } PersonBuilder& PersonBuilder::with( string company_name) { person.m_company_name = company_name; return * this ; } PersonBuilder& PersonBuilder::as_a( string position) { person.m_position = position; return * this ; } PersonBuilder& PersonBuilder::earning( string annual_income) { person.m_annual_income = annual_income; return * this ; } PersonBuilder& PersonBuilder::at( std :: string street_address) { person.m_street_address = street_address; return * this ; } PersonBuilder& PersonBuilder::with_postcode( std :: string post_code) { person.m_post_code = post_code; return * this ; } PersonBuilder& PersonBuilder::in( std :: string city) { person.m_city = city; return * this ; }
  • En lugar de incluir todas esas API relacionadas con la construcción en Person, podemos delegar esa tarea a una entidad separada, es decir, PersonBuilder.

Principal.cpp

 # include <iostream> # include "Person.h" # include "PersonBuilder.h" using namespace std ; int main () { Person p = Person::create( "John" ) .lives() .at( "123 London Road" ) .with_postcode( "SW1 1GB" ) .in( "London" ) .works() .with( "PragmaSoft" ) .as_a( "Consultant" ) .earning( "10e6" ); cout << p << endl ; return EXIT_SUCCESS; }
  • ¿No es la construcción anterior un inglés más intuitivo, natural y sencillo?
  • Si le preocupan los métodos en blanco como vidas() y obras(), no se preocupe, se eliminarán en la optimización.
  • También puede observar que estamos obligando a los usuarios a usar el constructor en lugar del constructor haciendo que el constructor sea privado y exponiendo solo la API de creación ( std::string name ) .
  • No compliques demasiado las cosas diseñando una interfaz o clases abstractas a menos que lo necesites. He visto esto en muchos ejemplos de patrones de diseño de constructores en la Web.

Beneficios del patrón de diseño del generador

  • El número de líneas de código aumenta al menos al doble en el patrón de construcción. Pero el esfuerzo vale la pena en términos de flexibilidad de diseño, menos o ningún parámetro para el constructor y un código mucho más legible.
  • Builder Design Pattern también ayuda a minimizar la cantidad de parámetros en el constructor y, por lo tanto, no es necesario pasar nulo para los parámetros opcionales al constructor.
  • Los objetos inmutables se pueden construir sin mucha lógica compleja en el proceso de construcción de objetos.
  • Separar la construcción de la representación de objetos hace que la representación de objetos se divida y sea precisa. Tener una entidad constructora separada brinda la flexibilidad de crear e instanciar diferentes representaciones de objetos.

Resumen por preguntas frecuentes

¿Cuándo se debe utilizar el patrón de diseño del constructor?

Siempre que la creación de un nuevo objeto requiera configurar muchos parámetros y algunos de ellos (o todos ellos) son opcionales.

¿Por qué necesitamos una clase Builder cuando implementamos un patrón de diseño Builder?

No es necesario, pero hay algunos beneficios al hacerlo:

  • La preocupación del objeto de construcción debe estar en la entidad separada según SRP .
  • El objeto original no estaría hinchado.
  • Código fácil y mantenible.
  • Probar y comprender un constructor con muchos argumentos de entrada se vuelve exponencialmente más complicado.

¡La mayor ventaja del patrón de diseño del constructor!

Código más expresivo.
MiClase o = new MiClase(5, 5.5, 'A', var, 1000, obj9, "hola");

  • En cambio
    MiClase o = MiClase.builder().a(5).b(5.5).c('A').d(var).e(1000).f(obj9).g("hola");
  • Puede ver qué miembro de datos está siendo asignado por qué e incluso cambiar el orden de asignación.

¿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.

Publicado anteriormente en http://www.vishalchovatiya.com/builder-design-pattern-in-modern-cpp/