Principios SOLID de Diseño de Software en C++
Aprende los principios SOLID de diseño de software (SRP, OCP, LSP, ISP, DIP) con ejemplos prácticos en C++ y mejora la calidad de tu código con type safety y performance.
¡Domina los principios SOLID de diseño de software en C++! En este tutorial especializado te guiaré paso a paso para que aprendas los cinco principios fundamentales que te ayudarán a crear software mantenible, extensible y de alta calidad, con ejemplos prácticos en C++.
C++ es especialmente adecuado para demostrar los principios SOLID porque:
- Type Safety estática: El sistema de tipos fuerte de C++ detecta violaciones de LSP en tiempo de compilación
- Zero-cost abstractions: Los principios SOLID no tienen overhead de runtime en C++
- RAII y Resource Management: SRP y DIP se aplican naturalmente con smart pointers y ownership semantics
- Templates y Generic Programming: OCP e ISP se implementan elegantemente con templates
- Multiple Inheritance: C++ permite implementar ISP con herencia múltiple de interfaces abstractas
- Performance: Los principios SOLID en C++ no comprometen el rendimiento
- Memory Safety: Smart pointers ayudan a cumplir LSP y DIP con garantías de memoria
Objetivo: Aprender y aplicar los principios SOLID (SRP, OCP, LSP, ISP, DIP) en el diseño de software con C++, entender sus beneficios específicos en este lenguaje, y saber cuándo y cómo aplicarlos en proyectos reales.
Índice
- Paso 1: ¿Qué son los principios SOLID?
- Paso 2: SRP - Principio de Responsabilidad Única
- Paso 3: OCP - Principio Abierto/Cerrado
- Paso 4: LSP - Principio de Sustitución de Liskov
- Paso 5: ISP - Principio de Segregación de Interfaces
- Paso 6: DIP - Principio de Inversión de Dependencias
- Paso 7: Combinando principios SOLID
- Paso 8: Refactoring aplicando SOLID
- Paso 9: Cuando NO aplicar SOLID
- Conclusión
- 💡 Tip Importante
Paso 1: ¿Qué son los principios SOLID?
Los principios SOLID son cinco principios de diseño orientado a objetos que ayudan a crear software más mantenible, flexible y entendible. Fueron introducidos por Robert C. Martin (Uncle Bob) y se han convertido en estándares fundamentales para el desarrollo de software de calidad.
Los 5 principios SOLID
| Principio | Significado | Descripción |
|---|---|---|
| SRP | Single Responsibility Principle | Una clase debe tener una sola razón para cambiar |
| OCP | Open/Closed Principle | Las clases deben estar abiertas para extensión pero cerradas para modificación |
| LSP | Liskov Substitution Principle | Las subclases deben ser sustituibles por sus clases base |
| ISP | Interface Segregation Principle | Muchas interfaces específicas son mejores que una interfaz general |
| DIP | Dependency Inversion Principle | Depender de abstracciones, no de implementaciones concretas |
Beneficios de aplicar SOLID
- Mantenibilidad: Código más fácil de entender y modificar
- Extensibilidad: Fácil agregar nuevas funcionalidades
- Testabilidad: Código más fácil de probar unitariamente
- Reusabilidad: Componentes que pueden ser reutilizados
- Flexibilidad: Fácil adaptación a cambios de requisitos
Paso 2: SRP - Principio de Responsabilidad Única
El Principio de Responsabilidad Única establece que una clase debe tener una y solo una razón para cambiar. Esto significa que una clase debe ocuparse de una única funcionalidad o responsabilidad.
Ejemplo violando SRP
class Usuario {
private:
std::string nombre_;
std::string email_;
public:
Usuario(const std::string& nombre, const std::string& email)
: nombre_(nombre), email_(email) {}
void guardar_en_bd() {
// Lógica para guardar en base de datos
std::cout << "Guardando " << nombre_ << " en BD" << std::endl;
}
void enviar_email(const std::string& mensaje) {
// Lógica para enviar email
std::cout << "Enviando email a " << email_ << ": " << mensaje << std::endl;
}
bool validar_email() {
// Lógica de validación de email
if (email_.find("@") == std::string::npos) {
throw std::invalid_argument("Email inválido");
}
return true;
}
// Getters
const std::string& get_nombre() const { return nombre_; }
const std::string& get_email() const { return email_; }
};
// ❌ Problema: La clase Usuario tiene múltiples responsabilidades:
// 1. Representar datos de usuario
// 2. Persistencia en BD
// 3. Envío de emails
// 4. Validación de emails
Refactorizando aplicando SRP
#include <iostream>
#include <string>
#include <memory>
// ✅ Aplicando SRP: Separar responsabilidades en clases distintas
class Usuario {
private:
std::string nombre_;
std::string email_;
public:
Usuario(const std::string& nombre, const std::string& email)
: nombre_(nombre), email_(email) {}
const std::string& get_nombre() const { return nombre_; }
const std::string& get_email() const { return email_; }
};
class ValidadorUsuario {
public:
static void validar_email(const std::string& email) {
if (email.find("@") == std::string::npos) {
throw std::invalid_argument("Email inválido");
}
}
static void validar_nombre(const std::string& nombre) {
if (nombre.empty()) {
throw std::invalid_argument("Nombre no puede estar vacío");
}
}
};
class RepositorioUsuario {
public:
void guardar(const Usuario& usuario) {
// Lógica para guardar en base de datos
std::cout << "Guardando " << usuario.get_nombre() << " en BD" << std::endl;
}
std::unique_ptr<Usuario> obtener_por_id(int id) {
// Lógica para obtener usuario
return nullptr;
}
};
class ServicioEmail {
public:
void enviar(const std::string& destinatario, const std::string& mensaje) {
// Lógica para enviar email
std::cout << "Enviando email a " << destinatario << ": " << mensaje << std::endl;
}
};
// Uso
int main() {
auto usuario = std::make_unique<Usuario>("Ana García", "[email protected]");
ValidadorUsuario::validar_email(usuario->get_email());
ValidadorUsuario::validar_nombre(usuario->get_nombre());
RepositorioUsuario repositorio;
repositorio.guardar(*usuario);
ServicioEmail servicio_email;
servicio_email.enviar(usuario->get_email(), "Bienvenida al sistema");
return 0;
}
Casos de uso comunes para SRP
- Separar lógica de negocio de persistencia: Usar RAII y smart pointers para gestión de recursos
- Separar validación de modelos de datos: Clases específicas para validación con templates
- Separar lógica de presentación de lógica de aplicación: Interfaces abstractas para UI
- Separar configuración de ejecución: Singletons thread-safe para configuración global
- Separar manejo de errores de lógica de negocio: Excepciones específicas vs lógica normal
Paso 3: OCP - Principio Abierto/Cerrado
El Principio Abierto/Cerrado establece que las clases deben estar abiertas para extensión pero cerradas para modificación. Podemos agregar nuevas funcionalidades sin modificar el código existente.
Ejemplo violando OCP
class ProcesadorPagos {
private:
std::string _procesar_tarjeta(double monto) {
return "Procesando $" + std::to_string(monto) + " con tarjeta";
}
std::string _procesar_paypal(double monto) {
return "Procesando $" + std::to_string(monto) + " con PayPal";
}
std::string _procesar_bitcoin(double monto) {
return "Procesando $" + std::to_string(monto) + " con Bitcoin";
}
public:
std::string procesar_pago(const std::string& metodo, double monto) {
if (metodo == "tarjeta") {
return _procesar_tarjeta(monto);
} else if (metodo == "paypal") {
return _procesar_paypal(monto);
} else if (metodo == "bitcoin") {
return _procesar_bitcoin(monto);
} else {
throw std::invalid_argument("Método de pago no soportado");
}
}
};
// ❌ Problema: Cada vez que agregamos un nuevo método de pago,
// tenemos que modificar la clase ProcesadorPagos
Refactorizando aplicando OCP
#include <iostream>
#include <string>
#include <memory>
#include <vector>
// ✅ Aplicando OCP: Usar abstracciones y herencia
class MetodoPago {
public:
virtual ~MetodoPago() = default;
virtual std::string procesar(double monto) = 0;
};
class TarjetaCredito : public MetodoPago {
public:
std::string procesar(double monto) override {
return "Procesando $" + std::to_string(monto) + " con tarjeta de crédito";
}
};
class PayPal : public MetodoPago {
public:
std::string procesar(double monto) override {
return "Procesando $" + std::to_string(monto) + " con PayPal";
}
};
class Bitcoin : public MetodoPago {
public:
std::string procesar(double monto) override {
return "Procesando $" + std::to_string(monto) + " con Bitcoin";
}
};
class ProcesadorPagos {
public:
std::string procesar_pago(std::unique_ptr<MetodoPago> metodo, double monto) {
return metodo->procesar(monto);
}
};
// Ahora podemos agregar nuevos métodos de pago SIN modificar ProcesadorPagos
class TransferenciaBancaria : public MetodoPago {
public:
std::string procesar(double monto) override {
return "Procesando $" + std::to_string(monto) + " con transferencia bancaria";
}
};
// Uso
int main() {
ProcesadorPagos procesador;
std::vector<std::unique_ptr<MetodoPago>> metodos;
metodos.push_back(std::make_unique<TarjetaCredito>());
metodos.push_back(std::make_unique<PayPal>());
metodos.push_back(std::make_unique<Bitcoin>());
metodos.push_back(std::make_unique<TransferenciaBancaria>()); // ✅ Nuevo método sin modificar existentes
double monto = 100.50;
for (const auto& metodo : metodos) {
std::string resultado = metodo->procesar(monto);
std::cout << resultado << std::endl;
}
return 0;
}
Estrategias para aplicar OCP
- Usar herencia y polimorfismo: Interfaces abstractas con implementación virtual
- Implementar patrones Strategy o Template Method: Templates para algoritmos genéricos
- Usar composición sobre herencia: Smart pointers para dependencias
- Crear interfaces abstractas: Clases base abstractas con métodos virtuales puros
- Templates y metaprogramación: Para extensiones en tiempo de compilación
- Políticas de clase: Para configurar comportamiento sin herencia
Paso 4: LSP - Principio de Sustitución de Liskov
El Principio de Sustitución de Liskov establece que los objetos de una superclase deben ser reemplazables por objetos de una subclase sin alterar las propiedades del programa.
Ejemplo violando LSP
class Rectangulo {
protected:
double ancho_;
double alto_;
public:
Rectangulo(double ancho, double alto) : ancho_(ancho), alto_(alto) {}
virtual ~Rectangulo() = default;
double get_ancho() const { return ancho_; }
double get_alto() const { return alto_; }
void set_ancho(double ancho) { ancho_ = ancho; }
void set_alto(double alto) { alto_ = alto; }
virtual double calcular_area() {
return ancho_ * alto_;
}
};
class Cuadrado : public Rectangulo {
public:
Cuadrado(double lado) : Rectangulo(lado, lado) {}
// ❌ Violación LSP: Cuadrado cambia el comportamiento esperado
void set_ancho(double ancho) override {
ancho_ = ancho;
alto_ = ancho; // Forzar que alto sea igual a ancho
}
void set_alto(double alto) override {
alto_ = alto;
ancho_ = alto; // Forzar que ancho sea igual a alto
}
};
void probar_rectangulo(Rectangulo* rect) {
rect->set_ancho(5);
rect->set_alto(4);
double area_esperada = 20;
double area_real = rect->calcular_area();
if (std::abs(area_real - area_esperada) > 0.001) {
throw std::logic_error("LSP violado: Esperado " + std::to_string(area_esperada) +
", obtenido " + std::to_string(area_real));
}
std::cout << "✅ LSP cumplido" << std::endl;
}
// Uso que demuestra la violación
int main() {
try {
auto rectangulo = std::make_unique<Rectangulo>(5, 4);
probar_rectangulo(rectangulo.get()); // ✅ Funciona
auto cuadrado = std::make_unique<Cuadrado>(5);
probar_rectangulo(cuadrado.get()); // ❌ Falla - LSP violado
} catch (const std::logic_error& e) {
std::cout << e.what() << std::endl;
}
return 0;
}
Refactorizando aplicando LSP
#include <iostream>
#include <memory>
#include <cmath>
// ✅ Aplicando LSP: Diseñar jerarquías correctas
class Forma {
public:
virtual ~Forma() = default;
virtual double calcular_area() = 0;
};
class Rectangulo : public Forma {
private:
double ancho_;
double alto_;
public:
Rectangulo(double ancho, double alto) : ancho_(ancho), alto_(alto) {}
double get_ancho() const { return ancho_; }
double get_alto() const { return alto_; }
void set_ancho(double ancho) { ancho_ = ancho; }
void set_alto(double alto) { alto_ = alto; }
double calcular_area() override {
return ancho_ * alto_;
}
};
class Cuadrado : public Forma {
private:
double lado_;
public:
Cuadrado(double lado) : lado_(lado) {}
double get_lado() const { return lado_; }
void set_lado(double lado) { lado_ = lado; }
double calcular_area() override {
return lado_ * lado_;
}
};
// Ahora ambas clases implementan Forma correctamente
void probar_area(const Forma* forma, double area_esperada) {
double area_real = forma->calcular_area();
if (std::abs(area_real - area_esperada) > 0.001) {
throw std::logic_error("Área incorrecta: Esperado " + std::to_string(area_esperada) +
", obtenido " + std::to_string(area_real));
}
std::cout << "✅ Área correcta: " << area_real << std::endl;
}
// Uso
int main() {
auto rectangulo = std::make_unique<Rectangulo>(5, 4);
auto cuadrado = std::make_unique<Cuadrado>(5);
probar_area(rectangulo.get(), 20); // ✅
probar_area(cuadrado.get(), 25); // ✅
// Ambas formas son sustituibles y se comportan correctamente
return 0;
}
Reglas para cumplir LSP
- Mismos métodos: Las subclases deben implementar todos los métodos virtuales de la superclase
- Mismas precondiciones: Las subclases no deben fortalecer las precondiciones (más restrictivas)
- Mismas postcondiciones: Las subclases no deben debilitar las postcondiciones (menos específicas)
- Mismas invariantes: Las subclases deben preservar las invariantes de la superclase
- Covarianza de retorno: Los tipos de retorno pueden ser más específicos en subclases
- Contravarianza de parámetros: Los parámetros pueden ser más generales en subclases
- Excepciones: Las subclases pueden lanzar excepciones más específicas, no más generales
Paso 5: ISP - Principio de Segregación de Interfaces
El Principio de Segregación de Interfaces establece que es mejor tener muchas interfaces específicas que una interfaz general. Los clientes no deben verse forzados a depender de interfaces que no usan.
Ejemplo violando ISP
class Dispositivo {
public:
virtual ~Dispositivo() = default;
virtual void imprimir(const std::string& documento) = 0;
virtual void escanear(const std::string& documento) = 0;
virtual void fax(const std::string& documento) = 0;
};
class ImpresoraAntigua : public Dispositivo {
public:
void imprimir(const std::string& documento) override {
std::cout << "Imprimiendo: " << documento << std::endl;
}
void escanear(const std::string& documento) override {
throw std::runtime_error("Esta impresora no puede escanear");
}
void fax(const std::string& documento) override {
throw std::runtime_error("Esta impresora no puede fax");
}
};
class ImpresoraMultifuncional : public Dispositivo {
public:
void imprimir(const std::string& documento) override {
std::cout << "Imprimiendo: " << documento << std::endl;
}
void escanear(const std::string& documento) override {
std::cout << "Escaneando: " << documento << std::endl;
}
void fax(const std::string& documento) override {
std::cout << "Enviando fax: " << documento << std::endl;
}
};
// ❌ Problema: ImpresoraAntigua debe implementar métodos que no usa
Refactorizando aplicando ISP
#include <iostream>
#include <string>
#include <memory>
// ✅ Aplicando ISP: Interfaces específicas y pequeñas
class Imprimible {
public:
virtual ~Imprimible() = default;
virtual void imprimir(const std::string& documento) = 0;
};
class Escaneable {
public:
virtual ~Escaneable() = default;
virtual void escanear(const std::string& documento) = 0;
};
class Faxeable {
public:
virtual ~Faxeable() = default;
virtual void fax(const std::string& documento) = 0;
};
class ImpresoraAntigua : public Imprimible {
public:
void imprimir(const std::string& documento) override {
std::cout << "Imprimiendo: " << documento << std::endl;
}
};
class Escaner : public Escaneable {
public:
void escanear(const std::string& documento) override {
std::cout << "Escaneando: " << documento << std::endl;
}
};
class ImpresoraMultifuncional : public Imprimible, public Escaneable, public Faxeable {
public:
void imprimir(const std::string& documento) override {
std::cout << "Imprimiendo: " << documento << std::endl;
}
void escanear(const std::string& documento) override {
std::cout << "Escaneando: " << documento << std::endl;
}
void fax(const std::string& documento) override {
std::cout << "Enviando fax: " << documento << std::endl;
}
};
// Ahora cada clase sólo implementa lo que necesita
void usar_impresora(Imprimible* impresora, const std::string& documento) {
impresora->imprimir(documento);
}
void usar_escaner(Escaneable* escaner, const std::string& documento) {
escaner->escanear(documento);
}
// Uso
int main() {
auto impresora_vieja = std::make_unique<ImpresoraAntigua>();
auto impresora_multifuncional = std::make_unique<ImpresoraMultifuncional>();
auto escaner = std::make_unique<Escaner>();
std::string documento = "Mi documento importante";
usar_impresora(impresora_vieja.get(), documento);
usar_impresora(impresora_multifuncional.get(), documento);
usar_escaner(escaner.get(), documento);
usar_escaner(impresora_multifuncional.get(), documento); // ✅ También funciona
return 0;
}
Beneficios de ISP
- Menos acoplamiento: Clientes dependen sólo de lo que necesitan
- Mejor mantenibilidad: Cambios en una interfaz afectan menos código
- Mayor cohesión: Interfaces más enfocadas y específicas
- Mejor testing: Más fácil mockear interfaces pequeñas
- Herencia múltiple: C++ permite implementar múltiples interfaces específicas
- Compilación más rápida: Interfaces pequeñas reducen tiempos de compilación
- Zero-cost abstractions: Interfaces abstractas no tienen overhead en C++
Paso 6: DIP - Principio de Inversión de Dependencias
El Principio de Inversión de Dependencias establece que los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones.
Ejemplo violando DIP
#include <iostream>
#include <string>
#include <memory>
// Módulos de bajo nivel
class BaseDatosMySQL {
public:
void guardar(const std::string& datos) {
std::cout << "Guardando en MySQL: " << datos << std::endl;
}
std::string obtener(int id) {
std::cout << "Obteniendo de MySQL: " << id << std::endl;
return "Usuario desde MySQL";
}
};
class ServicioEmail {
public:
void enviar(const std::string& destinatario, const std::string& mensaje) {
std::cout << "Enviando email a " << destinatario << ": " << mensaje << std::endl;
}
};
// Módulo de alto nivel
class ServicioUsuarios {
private:
BaseDatosMySQL* db_; // ❌ Dependencia concreta
ServicioEmail* email_; // ❌ Dependencia concreta
public:
ServicioUsuarios() : db_(new BaseDatosMySQL()), email_(new ServicioEmail()) {}
~ServicioUsuarios() {
delete db_;
delete email_;
}
std::string registrar_usuario(const std::string& nombre, const std::string& email) {
// Lógica de negocio
std::string usuario = "Usuario: " + nombre + ", Email: " + email;
// Depende directamente de implementaciones concretas
db_->guardar(usuario);
email_->enviar(email, "Bienvenido al sistema");
return usuario;
}
};
// ❌ Problema: ServicioUsuarios está fuertemente acoplado a implementaciones específicas
Refactorizando aplicando DIP
#include <iostream>
#include <string>
#include <memory>
// ✅ Aplicando DIP: Depender de abstracciones
// Abstracciones (interfaces)
class Repositorio {
public:
virtual ~Repositorio() = default;
virtual void guardar(const std::string& datos) = 0;
virtual std::string obtener(int id) = 0;
};
class ServicioNotificaciones {
public:
virtual ~ServicioNotificaciones() = default;
virtual void enviar(const std::string& destinatario, const std::string& mensaje) = 0;
};
// Implementaciones concretas de bajo nivel
class BaseDatosMySQL : public Repositorio {
public:
void guardar(const std::string& datos) override {
std::cout << "Guardando en MySQL: " << datos << std::endl;
}
std::string obtener(int id) override {
std::cout << "Obteniendo de MySQL: " << id << std::endl;
return "Usuario desde MySQL";
}
};
class BaseDatosPostgreSQL : public Repositorio {
public:
void guardar(const std::string& datos) override {
std::cout << "Guardando en PostgreSQL: " << datos << std::endl;
}
std::string obtener(int id) override {
std::cout << "Obteniendo de PostgreSQL: " << id << std::endl;
return "Usuario desde PostgreSQL";
}
};
class ServicioEmail : public ServicioNotificaciones {
public:
void enviar(const std::string& destinatario, const std::string& mensaje) override {
std::cout << "Enviando email a " << destinatario << ": " << mensaje << std::endl;
}
};
class ServicioSMS : public ServicioNotificaciones {
public:
void enviar(const std::string& destinatario, const std::string& mensaje) override {
std::cout << "Enviando SMS a " << destinatario << ": " << mensaje << std::endl;
}
};
// Módulo de alto nivel
class ServicioUsuarios {
private:
std::unique_ptr<Repositorio> repositorio_; // ✅ Dependencia abstracta
std::unique_ptr<ServicioNotificaciones> notificador_; // ✅ Dependencia abstracta
public:
ServicioUsuarios(std::unique_ptr<Repositorio> repositorio,
std::unique_ptr<ServicioNotificaciones> notificador)
: repositorio_(std::move(repositorio)), notificador_(std::move(notificador)) {}
std::string registrar_usuario(const std::string& nombre, const std::string& email) {
std::string usuario = "Usuario: " + nombre + ", Email: " + email;
repositorio_->guardar(usuario);
notificador_->enviar(email, "Bienvenido al sistema");
return usuario;
}
};
// Configuración de dependencias (Inyección de Dependencias)
std::unique_ptr<ServicioUsuarios> configurar_aplicacion() {
// Podemos cambiar fácilmente las implementaciones
auto repositorio = std::make_unique<BaseDatosPostgreSQL>(); // o BaseDatosMySQL()
auto notificador = std::make_unique<ServicioSMS>(); // o ServicioEmail()
return std::make_unique<ServicioUsuarios>(std::move(repositorio), std::move(notificador));
}
// Uso
int main() {
auto servicio = configurar_aplicacion();
std::string usuario = servicio->registrar_usuario("Ana García", "[email protected]");
std::cout << "Usuario registrado: " << usuario << std::endl;
return 0;
}
Ventajas de DIP
- Desacoplamiento: Módulos independientes y reutilizables
- Flexibilidad: Fácil cambiar implementaciones en tiempo de ejecución
- Testabilidad: Fácil usar mocks en tests unitarios
- Mantenibilidad: Cambios aislados y controlados
- Inyección de dependencias: Constructor injection con smart pointers
- Gestión de memoria: RAII automático con unique_ptr
- Type safety: Verificación en tiempo de compilación
- Zero-cost abstractions: Interfaces abstractas sin overhead
Paso 7: Combinando principios SOLID
Los principios SOLID funcionan mejor cuando se aplican en conjunto. Veamos un ejemplo que combina múltiples principios.
Sistema de procesamiento de pedidos
#include <iostream>
#include <string>
#include <memory>
#include <vector>
#include <unordered_map>
#include <stdexcept>
// DIP: Abstracciones
class RepositorioPedidos {
public:
virtual ~RepositorioPedidos() = default;
virtual std::string guardar(const std::string& pedido) = 0;
virtual std::string obtener_por_id(const std::string& id) = 0;
};
class ServicioNotificaciones {
public:
virtual ~ServicioNotificaciones() = default;
virtual void enviar(const std::string& destinatario, const std::string& mensaje) = 0;
};
class CalculadorDescuentos {
public:
virtual ~CalculadorDescuentos() = default;
virtual double calcular_descuento(double total) = 0;
};
// ISP: Interfaces específicas
class ValidadorPedido {
public:
virtual ~ValidadorPedido() = default;
virtual void validar(const std::unordered_map<std::string, std::string>& pedido) = 0;
};
// Implementaciones concretas
class RepositorioPedidosMySQL : public RepositorioPedidos {
public:
std::string guardar(const std::string& pedido) override {
std::cout << "Guardando pedido en MySQL: " << pedido.substr(0, 50) << "..." << std::endl;
return "pedido_123";
}
std::string obtener_por_id(const std::string& id) override {
std::cout << "Obteniendo pedido " << id << " desde MySQL" << std::endl;
return "Pedido procesado";
}
};
class ServicioEmail : public ServicioNotificaciones {
public:
void enviar(const std::string& destinatario, const std::string& mensaje) override {
std::cout << "Enviando email a " << destinatario << ": " << mensaje << std::endl;
}
};
class CalculadorDescuentosEstándar : public CalculadorDescuentos {
public:
double calcular_descuento(double total) override {
if (total > 1000) {
return total * 0.1; // 10% de descuento
} else if (total > 500) {
return total * 0.05; // 5% de descuento
}
return 0;
}
};
class ValidadorStock : public ValidadorPedido {
private:
std::unordered_map<std::string, int> inventario_;
public:
ValidadorStock(const std::unordered_map<std::string, int>& inventario)
: inventario_(inventario) {}
void validar(const std::unordered_map<std::string, std::string>& pedido) override {
// Simular validación de stock
if (inventario_.find("prod1") == inventario_.end() || inventario_["prod1"] < 1) {
throw std::runtime_error("Stock insuficiente para producto prod1");
}
}
};
// SRP: Cada clase tiene una responsabilidad clara
class ProcesadorPedidos {
private:
std::unique_ptr<RepositorioPedidos> repositorio_;
std::unique_ptr<ServicioNotificaciones> notificador_;
std::unique_ptr<CalculadorDescuentos> calculador_descuentos_;
std::vector<std::unique_ptr<ValidadorPedido>> validadores_;
public:
ProcesadorPedidos(std::unique_ptr<RepositorioPedidos> repositorio,
std::unique_ptr<ServicioNotificaciones> notificador,
std::unique_ptr<CalculadorDescuentos> calculador_descuentos,
std::vector<std::unique_ptr<ValidadorPedido>> validadores)
: repositorio_(std::move(repositorio)),
notificador_(std::move(notificador)),
calculador_descuentos_(std::move(calculador_descuentos)),
validadores_(std::move(validadores)) {}
// OCP: Fácil extender con nuevos validadores
void agregar_validador(std::unique_ptr<ValidadorPedido> validador) {
validadores_.push_back(std::move(validador));
}
std::string procesar_pedido(const std::string& usuario, const std::string& email,
const std::vector<std::pair<std::string, double>>& items) {
// Crear pedido
std::unordered_map<std::string, std::string> pedido = {
{"usuario", usuario},
{"email", email},
{"estado", "pendiente"}
};
// Validar pedido (OCP: nuevos validadores se agregan fácilmente)
for (const auto& validador : validadores_) {
validador->validar(pedido);
}
// Calcular total
double total = 0;
for (const auto& item : items) {
total += item.second;
}
double descuento = calculador_descuentos_->calcular_descuento(total);
double total_con_descuento = total - descuento;
pedido["total"] = std::to_string(total);
pedido["descuento"] = std::to_string(descuento);
pedido["total_con_descuento"] = std::to_string(total_con_descuento);
pedido["estado"] = "procesado";
// Guardar y notificar
std::string pedido_id = repositorio_->guardar("Pedido procesado");
notificador_->enviar(email, "Pedido " + pedido_id + " procesado. Total: $" +
std::to_string(total_con_descuento));
return pedido_id;
}
};
// Configuración (DIP: Inyección de dependencias)
std::unique_ptr<ProcesadorPedidos> configurar_sistema() {
std::unordered_map<std::string, int> inventario = {
{"prod1", 10}, {"prod2", 5}, {"prod3", 20}
};
auto repositorio = std::make_unique<RepositorioPedidosMySQL>();
auto notificador = std::make_unique<ServicioEmail>();
auto calculador_descuentos = std::make_unique<CalculadorDescuentosEstándar>();
std::vector<std::unique_ptr<ValidadorPedido>> validadores;
validadores.push_back(std::make_unique<ValidadorStock>(inventario));
return std::make_unique<ProcesadorPedidos>(
std::move(repositorio),
std::move(notificador),
std::move(calculador_descuentos),
std::move(validadores)
);
}
// Uso
int main() {
auto sistema = configurar_sistema();
std::string usuario = "Ana";
std::string email = "[email protected]";
std::vector<std::pair<std::string, double>> items = {
{"prod1", 1200.0}, // Laptop
{"prod2", 90.0} // 2 x Mouse
};
try {
std::string pedido_id = sistema->procesar_pedido(usuario, email, items);
std::cout << "Pedido procesado exitosamente: " << pedido_id << std::endl;
} catch (const std::exception& e) {
std::cout << "Error procesando pedido: " << e.what() << std::endl;
}
return 0;
}
Paso 8: Refactoring aplicando SOLID
Veamos un ejemplo práctico de refactorización aplicando todos los principios SOLID.
Código original con problemas SOLID
class TiendaOnline {
private:
std::vector<std::unordered_map<std::string, std::string>> productos_;
std::vector<std::unordered_map<std::string, std::string>> usuarios_;
public:
TiendaOnline() = default;
void agregar_producto(const std::string& nombre, double precio) {
productos_.push_back({{"nombre", nombre}, {"precio", std::to_string(precio)}});
}
void registrar_usuario(const std::string& nombre, const std::string& email) {
usuarios_.push_back({{"nombre", nombre}, {"email", email}});
}
double calcular_total_carrito(const std::vector<std::pair<std::string, double>>& items) {
double total = 0;
for (const auto& item : items) {
total += item.second;
}
if (total > 100) {
total *= 0.9; // Descuento
}
return total;
}
std::string procesar_pago(double total, const std::string& metodo_pago) {
if (metodo_pago == "tarjeta") {
return "Procesando $" + std::to_string(total) + " con tarjeta";
} else if (metodo_pago == "paypal") {
return "Procesando $" + std::to_string(total) + " con PayPal";
} else {
throw std::invalid_argument("Método de pago no soportado");
}
}
void guardar_en_bd(const std::string& datos) {
// Lógica de base de datos
std::cout << "Guardando en BD: " << datos << std::endl;
}
void enviar_email(const std::string& destinatario, const std::string& mensaje) {
// Lógica de email
std::cout << "Enviando email a " << destinatario << ": " << mensaje << std::endl;
}
};
// ❌ Problemas SOLID:
// - SRP: Múltiples responsabilidades en una clase
// - OCP: Difícil agregar nuevos métodos de pago
// - DIP: Dependencias concretas
Refactorización aplicando SOLID
#include <iostream>
#include <string>
#include <memory>
#include <vector>
#include <unordered_map>
// SRP: Modelos separados
class Producto {
private:
std::string nombre_;
double precio_;
public:
Producto(const std::string& nombre, double precio)
: nombre_(nombre), precio_(precio) {}
const std::string& get_nombre() const { return nombre_; }
double get_precio() const { return precio_; }
};
class Usuario {
private:
std::string nombre_;
std::string email_;
public:
Usuario(const std::string& nombre, const std::string& email)
: nombre_(nombre), email_(email) {}
const std::string& get_nombre() const { return nombre_; }
const std::string& get_email() const { return email_; }
};
// DIP: Abstracciones
class Repositorio {
public:
virtual ~Repositorio() = default;
virtual std::string guardar(const std::string& datos) = 0;
};
class ServicioNotificaciones {
public:
virtual ~ServicioNotificaciones() = default;
virtual void enviar(const std::string& destinatario, const std::string& mensaje) = 0;
};
class MetodoPago {
public:
virtual ~MetodoPago() = default;
virtual std::string procesar(double monto) = 0;
};
class CalculadorDescuentos {
public:
virtual ~CalculadorDescuentos() = default;
virtual double calcular_descuento(double total) = 0;
};
// ISP: Interfaces específicas
class Validador {
public:
virtual ~Validador() = default;
virtual void validar(const std::vector<std::pair<std::string, double>>& items) = 0;
};
// Implementaciones concretas
class RepositorioMySQL : public Repositorio {
public:
std::string guardar(const std::string& datos) override {
std::cout << "Guardando en MySQL: " << datos.substr(0, 50) << "..." << std::endl;
return "compra_123";
}
};
class ServicioEmail : public ServicioNotificaciones {
public:
void enviar(const std::string& destinatario, const std::string& mensaje) override {
std::cout << "Enviando email a " << destinatario << ": " << mensaje << std::endl;
}
};
class TarjetaCredito : public MetodoPago {
public:
std::string procesar(double monto) override {
return "Procesando $" + std::to_string(monto) + " con tarjeta de crédito";
}
};
class PayPal : public MetodoPago {
public:
std::string procesar(double monto) override {
return "Procesando $" + std::to_string(monto) + " con PayPal";
}
};
class CalculadorDescuentosEstándar : public CalculadorDescuentos {
public:
double calcular_descuento(double total) override {
if (total > 100) {
return total * 0.1;
}
return 0;
}
};
class ValidadorStock : public Validador {
private:
std::unordered_map<std::string, int> inventario_;
public:
ValidadorStock(const std::unordered_map<std::string, int>& inventario)
: inventario_(inventario) {}
void validar(const std::vector<std::pair<std::string, double>>& items) override {
for (const auto& item : items) {
if (inventario_.find("prod1") == inventario_.end() || inventario_["prod1"] < 1) {
throw std::runtime_error("Stock insuficiente para producto prod1");
}
}
}
};
// OCP: Fácil extensión
class TiendaOnline {
private:
std::unique_ptr<Repositorio> repositorio_;
std::unique_ptr<ServicioNotificaciones> notificador_;
std::unique_ptr<CalculadorDescuentos> calculador_descuentos_;
std::vector<std::unique_ptr<Validador>> validadores_;
std::unordered_map<std::string, std::unique_ptr<MetodoPago>> metodos_pago_;
public:
TiendaOnline(std::unique_ptr<Repositorio> repositorio,
std::unique_ptr<ServicioNotificaciones> notificador,
std::unique_ptr<CalculadorDescuentos> calculador_descuentos,
std::vector<std::unique_ptr<Validador>> validadores)
: repositorio_(std::move(repositorio)),
notificador_(std::move(notificador)),
calculador_descuentos_(std::move(calculador_descuentos)),
validadores_(std::move(validadores)) {}
void registrar_metodo_pago(const std::string& nombre, std::unique_ptr<MetodoPago> metodo) {
metodos_pago_[nombre] = std::move(metodo);
}
void agregar_validador(std::unique_ptr<Validador> validador) {
validadores_.push_back(std::move(validador));
}
std::string procesar_compra(const Usuario& usuario,
const std::vector<std::pair<std::string, double>>& items,
const std::string& metodo_pago) {
// Validar
for (const auto& validador : validadores_) {
validador->validar(items);
}
// Calcular total
double total = 0;
for (const auto& item : items) {
total += item.second;
}
double descuento = calculador_descuentos_->calcular_descuento(total);
double total_final = total - descuento;
// Procesar pago
auto it = metodos_pago_.find(metodo_pago);
if (it == metodos_pago_.end()) {
throw std::invalid_argument("Método de pago no soportado: " + metodo_pago);
}
std::string resultado_pago = it->second->procesar(total_final);
// Guardar y notificar
std::string compra_info = "Compra de " + usuario.get_nombre() + " por $" +
std::to_string(total_final);
std::string compra_id = repositorio_->guardar(compra_info);
notificador_->enviar(usuario.get_email(), "Compra procesada: $" +
std::to_string(total_final));
return compra_id;
}
};
// Configuración
std::unique_ptr<TiendaOnline> configurar_tienda() {
std::unordered_map<std::string, int> inventario = {
{"prod1", 10}, {"prod2", 5}
};
auto repositorio = std::make_unique<RepositorioMySQL>();
auto notificador = std::make_unique<ServicioEmail>();
auto calculador = std::make_unique<CalculadorDescuentosEstándar>();
std::vector<std::unique_ptr<Validador>> validadores;
validadores.push_back(std::make_unique<ValidadorStock>(inventario));
auto tienda = std::make_unique<TiendaOnline>(
std::move(repositorio),
std::move(notificador),
std::move(calculador),
std::move(validadores)
);
// Registrar métodos de pago (OCP)
tienda->registrar_metodo_pago("tarjeta", std::make_unique<TarjetaCredito>());
tienda->registrar_metodo_pago("paypal", std::make_unique<PayPal>());
return tienda;
}
// Uso
int main() {
auto tienda = configurar_tienda();
Usuario usuario("Ana García", "[email protected]");
std::vector<std::pair<std::string, double>> items = {
{"prod1", 1200.0}, // Laptop
{"prod2", 90.0} // 2 x Mouse
};
try {
std::string compra_id = tienda->procesar_compra(usuario, items, "paypal");
std::cout << "✅ Compra exitosa: " << compra_id << std::endl;
} catch (const std::exception& e) {
std::cout << "❌ Error: " << e.what() << std::endl;
}
return 0;
}
Paso 9: Cuando NO aplicar SOLID
Aunque los principios SOLID son importantes, hay situaciones donde su aplicación estricta puede ser contraproducente.
Casos donde aplicar SOLID con cuidado
-
Proyectos pequeños y simples
// ❌ Over-engineering para un script simple class CalculadoraSimple { public: int sumar(int a, int b) { return a + b; } int restar(int a, int b) { return a - b; } }; // ✅ Mejor mantenerlo simple int sumar(int a, int b) { return a + b; } int restar(int a, int b) { return a - b; } -
Prototipos y pruebas de concepto
// Durante el prototyping, focus en funcionalidad, no en arquitectura class PrototipoRapido { public: void hacer_todo() { // Código rápido y sucio para validar idea std::cout << "Prototipo funcionando" << std::endl; } }; -
Performance crítica
// En algunos casos, las abstracciones pueden afectar el performance // Evaluar trade-off entre clean code y performance // Ejemplo: usar funciones inline en lugar de interfaces virtuales inline int suma_rapida(int a, int b) { return a + b; // Zero-cost abstraction } -
Cuando añade complejidad innecesaria
// Si el beneficio no justifica la complejidad añadida // Mantener el principio KISS (Keep It Simple, Stupid) // Ejemplo: no crear interfaces abstractas para funciones de una sola línea
Regla general
Aplica SOLID cuando:
- El proyecto tiene vida larga
- Requiere mantenimiento frecuente
- Tiene múltiples desarrolladores
- Necesita alta testabilidad
Considera alternativas cuando:
- Es un proyecto pequeño/script
- Es un prototipo temporal
- El performance es crítico
- La simplicidad es más importante
Conclusión
¡Has dominado los principios SOLID de diseño de software en C++! Estos principios te proporcionan un framework sólido para crear software mantenible, extensible y de alta calidad, aprovechando al máximo las características únicas de C++ como la seguridad de tipos, el rendimiento y la gestión eficiente de memoria.
Beneficios específicos de aplicar SOLID en C++
- Seguridad de tipos en tiempo de compilación: Los principios SOLID en C++ aprovechan el sistema de tipos fuerte para detectar errores en tiempo de compilación
- Rendimiento optimizado: Las abstracciones bien diseñadas permiten optimizaciones del compilador y zero-cost abstractions
- Gestión de memoria segura: RAII y smart pointers se integran perfectamente con los principios SOLID
- Multi-threading seguro: Los principios SOLID facilitan el diseño de código thread-safe
- Templates y metaprogramación: C++ permite aplicar SOLID de manera genérica y eficiente
Recuerda que SOLID son guías, no reglas absolutas. La clave está en entender el contexto y aplicar estos principios de manera pragmática, considerando las características específicas de C++.
Practica aplicando estos principios en tus proyectos y siempre evalúa el trade-off entre clean code y simplicidad, aprovechando las fortalezas únicas de C++.
Para más tutoriales sobre principios de diseño y patrones en C++, visita nuestra sección de tutoriales.
¡Sigue practicando y aplicando estos principios en proyectos reales de C++!
💡 Tip Importante
📝 Mejores Prácticas con SOLID
- Empieza simple: No apliques todos los principios desde el inicio
- Refactoriza incrementalmente: Mejora el código existente aplicando SOLID gradualmente
- Entiende el contexto: Aplica SOLID según las necesidades del proyecto
- Mide el impacto: Evalúa si los beneficios justifican la complejidad added
- Mantén el balance: Encuentra el equilibrio entre SOLID y simplicidad
- Aprende de ejemplos: Estudia código bien diseñado en proyectos open source
- Practica constantemente: La maestría viene con la práctica repetida
- Busca feedback: Comparte tu código con otros desarrolladores
📚 Recursos Recomendados para C++:
- Design Patterns in Modern C++ - Dmitri Nesteruk
- Modern C++ Design - Andrei Alexandrescu
- Effective Modern C++ - Scott Meyers
- Clean Code - Robert C. Martin
- Clean Architecture - Robert C. Martin
- C++ Core Guidelines - Guías oficiales de C++
- Refactoring.Guru - SOLID - Ejemplos interactivos
- C++ Best Practices - Mejores prácticas de la comunidad
📚 Recursos en Español:
- Programación en C++ Moderno - Ivor Horton
- C++ Avanzado - Bjarne Stroustrup
- Patrones de Diseño en C++ - Alan Shalloway
¡Estos principios transformarán la forma en que diseñas y construyes software en C++!
No hay comentarios aún
Sé el primero en comentar este tutorial.