Imagen destacada del tutorial: Principios SOLID de Diseño de Software en C++
Patrones de Diseño

Principios SOLID de Diseño de Software en C++

José Elías Romero Guanipa
01 Sep 2025

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.

solid principios diseño srp ocp lsp +6 más

¡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?

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

  1. 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;
    }
  2. 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;
       }
    };
  3. 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
    }
  4. 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++:

📚 Recursos en Español:

¡Estos principios transformarán la forma en que diseñas y construyes software en C++!

Toca los botones para interactuar

Comentarios

Comentarios

Inicia sesión para dejar un comentario.

No hay comentarios aún

Sé el primero en comentar este tutorial.

Tutoriales Relacionados

Descubre más tutoriales relacionados que podrían ser de tu interés

Imagen destacada del tutorial relacionado: Patrones de Diseño de Comportamiento en C++
Patrones de Diseño

Patrones de Diseño de Comportamiento en C++

Aprende patrones de diseño de comportamiento como Observer, Strategy, Command, State, Template Method, Iterator, Mediator y Memento con ejemplos prácticos en C++. Descubre cómo implementar estos patrones aprovechando las fortalezas del lenguaje C++ como type safety, performance y memory management.

José Elías Romero Guanipa
02 Sep 2025
Imagen destacada del tutorial relacionado: Patrones de Diseño Creacionales en C++
Patrones de Diseño

Patrones de Diseño Creacionales en C++

Aprende patrones de diseño creacionales como Singleton, Factory, Builder y Prototype con ejemplos prácticos en C++ moderno, aprovechando smart pointers, RAII y características del lenguaje.

José Elías Romero Guanipa
03 Sep 2025
Imagen destacada del tutorial relacionado: Patrones de Diseño Estructurales en C++
Patrones de Diseño

Patrones de Diseño Estructurales en C++

Aprende patrones de diseño estructurales como Adapter, Decorator, Facade y Proxy con ejemplos prácticos en C++, aprovechando las fortalezas del lenguaje para estructuras robustas y eficientes.

José Elías Romero Guanipa
04 Sep 2025
Imagen destacada del tutorial relacionado: Patrones de Diseño - Aplicaciones Avanzadas
Patrones de Diseño

Patrones de Diseño - Aplicaciones Avanzadas

Aprende aplicaciones avanzadas de patrones de diseño en arquitecturas empresariales como microservicios, CQRS, event sourcing y más con ejemplos prácticos en C++.

José Elías Romero Guanipa
05 Sep 2025
Foto de perfil del autor José Elías Romero Guanipa
José Elías Romero Guanipa
Autor

🌟 Nube de Etiquetas

Descubre temas populares en nuestros tutoriales

python
python 25 tutoriales
poo
poo 8 tutoriales
ciencia de datos
ciencia de datos 8 tutoriales
patrones diseño
patrones diseño 7 tutoriales
matplotlib
matplotlib 7 tutoriales
pandas
pandas 6 tutoriales
visualizacion
visualizacion 6 tutoriales
principiante
principiante 5 tutoriales
numpy
numpy 5 tutoriales
c++
c++ 5 tutoriales
estadistica
estadistica 4 tutoriales
cpp
cpp 4 tutoriales
bases de datos
bases de datos 4 tutoriales
dataframe
dataframe 4 tutoriales
csv
csv 3 tutoriales
json
json 3 tutoriales
machine learning
machine learning 3 tutoriales
rendimiento
rendimiento 3 tutoriales
mysql
mysql 3 tutoriales
postgresql
postgresql 3 tutoriales
analisis de datos
analisis de datos 3 tutoriales
graficos
graficos 3 tutoriales
excepciones
excepciones 2 tutoriales
algoritmos
algoritmos 2 tutoriales
estructuras datos
estructuras datos 2 tutoriales

Las etiquetas más grandes y brillantes aparecen en más tutoriales

logo logo

©2024 ViveBTC