Patrones de diseño en C++: cuándo y por qué
\n\n¿Alguna vez has reescrito código por décima vez porque una nueva funcionalidad rompía la arquitectura anterior? ¿O has pasado horas depurando, tratando de entender quién y por qué cambió una variable global? ¿Te suena? Entonces bienvenido al mundo de los patrones de diseño.
\n\nLos patrones no son una píldora mágica. Son soluciones probadas por desarrolladores para problemas típicos. No harán tu código perfecto, pero te ayudarán a hablar el mismo idioma con otros desarrolladores y evitar los errores en los que ya han caído miles de programadores antes que tú.
\n\nEn este artículo, analizaremos los tres grupos principales de patrones en C++: creacionales, estructurales y de comportamiento. Aprenderemos cuándo aplicarlos y cuándo es mejor evitarlos. ¡Allá vamos!
\n\nPatrones creacionales: control sobre la creación de objetos
\n\nLos patrones creacionales se encargan de la creación flexible de objetos. Ocultan la lógica de instanciación y hacen que el sistema sea independiente de cómo se crean sus componentes.
\n\nSingleton (Instancia única)
\n\nUn clásico. Garantiza que una clase tenga solo una instancia y proporciona un punto de acceso global a ella. Suena tentador, pero úsalo con cuidado: Singleton a menudo se convierte en un antipatrón si crece hasta convertirse en un "objeto dios".
\n\nCuándo usarlo: logger, gestor de configuración, pool de hilos (thread pool), trabajo con hardware.
\n\nEjemplo en C++ (thread-safe, C++11 y posteriores):
\n\nclass Logger {\npublic:\n static Logger& getInstance() {\n static Logger instance; // Se inicializa una vez, incluso en entornos multihilo\n return instance;\n }\n\n void log(const std::string& msg) {\n std::cout << "[LOG]: " << msg << std::endl;\n }\n\n // Prohibimos copia y asignación\n Logger(const Logger&) = delete;\n Logger& operator=(const Logger&) = delete;\n\nprivate:\n Logger() = default; // Constructor privado\n ~Logger() = default;\n};\n\n// Uso\nLogger::getInstance().log("Sistema iniciado");\n\nFactory Method (Método de fábrica)
\n\nDefine una interfaz para crear un objeto, pero permite que las subclases modifiquen el tipo de objeto creado. Es ideal cuando no sabes de antemano qué clase se necesitará.
\n\nCuándo usarlo: análisis de diferentes formatos (JSON, XML, YAML), creación de elementos de interfaz de usuario para diferentes SO, trabajo con diferentes bases de datos.
\n\nEjemplo:
\n\n// Producto abstracto\nclass Document {\npublic:\n virtual void open() = 0;\n virtual ~Document() = default;\n};\n\n// Productos concretos\nclass PDFDocument : public Document {\n void open() override { std::cout << "Abriendo PDF" << std::endl; }\n};\n\nclass WordDocument : public Document {\n void open() override { std::cout << "Abriendo Word" << std::endl; }\n};\n\n// Fábrica\nclass Application {\npublic:\n virtual Document* createDocument() = 0; // Método de fábrica\n virtual ~Application() = default;\n\n void newDocument() {\n Document* doc = createDocument();\n doc->open();\n delete doc; // En código real, usar punteros inteligentes\n }\n};\n\nclass PDFApp : public Application {\n Document* createDocument() override { return new PDFDocument(); }\n};\n\n// Uso\nPDFApp app;\napp.newDocument(); // Abriendo PDF\n\nPatrones estructurales: armando el rompecabezas
\n\nEstos patrones ayudan a organizar clases y objetos en estructuras más grandes, manteniendo la flexibilidad y eficiencia.
\n\nAdapter (Adaptador)
\n\nPermite que objetos con interfaces incompatibles trabajen juntos. Imagina que tienes una biblioteca antigua para trabajar con XML, pero un nuevo módulo espera JSON. El adaptador es tu salvador.
\n\nCuándo usarlo: