Principios SOLID explicados con ejemplos reales

D
DanisCh
(Actualizado: ) • 8 min de lectura
Principios SOLID explicados con ejemplos reales
Conceptos de Base de Datos

El desarrollo de software no se trata solo de escribir código que funcione; se trata de escribir código que perdure, que sea fácil de mantener y que pueda crecer junto con las necesidades del negocio. Es aquí donde entran en juego los Principios SOLID, una serie de reglas y buenas prácticas que todo desarrollador profesional debería conocer y aplicar.

Fueron promovidos por primera vez por el reconocido ingeniero de software Robert C. Martin (ampliamente conocido como Uncle Bob) a principios de la década del 2000, y el acrónimo fue acuñado más tarde por Michael Feathers. Hoy en día, aplicar los principios SOLID es prácticamente un estándar en la industria, indispensable para dominar la Programación Orientada a Objetos (POO).

En este artículo, vamos a desglosar qué significa cada una de las letras de SOLID y, lo más importante, los explicaremos con ejemplos reales y prácticos en PHP, para que puedas aplicarlos en tu próximo proyecto y huir del temido "código espagueti".

¿Por qué son importantes los principios SOLID?

Antes de entrar de lleno a cada principio, es importante entender qué problemas resuelven. Cuando no aplicamos estas reglas, nuestro código tiende a volverse frágil. Pequeños cambios en una parte del sistema terminan rompiendo otra funcionalidad aparentemente desconectada. Además, el código se vuelve difícil de leer, imposible de testear y muy tedioso de escalar.

Los beneficios de aplicar SOLID incluyen:

  • Mantenibilidad: Es mucho más fácil encontrar y corregir errores cuando cada clase tiene una función clara.
  • Escalabilidad: Añadir nuevas características no significa reescribir lo que ya funciona.
  • Testabilidad: El código desacoplado y dependiente de abstracciones es infinitamente más fácil de someter a pruebas unitarias.
  • Legibilidad: Otros programadores (o tú mismo en 6 meses) entenderán la arquitectura mucho más rápido.

1. Single Responsibility Principle (SRP) - Principio de Responsabilidad Única

La definición formal dice: "Una clase debe tener una y solo una razón para cambiar". En otras palabras, una clase debe encargarse de una única responsabilidad dentro del sistema.

Uno de los errores más comunes al empezar a programar es crear "clases Dios" (God Classes) que saben demasiado y hacen de todo: conectarse a la base de datos, validar reglas de negocio y formatear la salida HTML.

Ejemplo de violación del SRP

Imagina que estamos creando un sistema de facturación. Tenemos una clase Factura:

class Factura {
    public function calcularTotal($items) {
        // Lógica matemática
    }
    
    public function generarPdf() {
        // Lógica de conversión a PDF
    }
    
    public function enviarPorEmail() {
        // Lógica de envío por SMTP
    }
}

Si mañana decidimos cambiar la librería que genera el PDF o cambiar de proveedor de correo, tendremos que modificar la clase Factura. Esta clase tiene al menos tres razones diferentes para cambiar.

Cómo aplicar SRP correctamente

Para cumplir con SRP, debemos separar estas responsabilidades en clases más pequeñas y enfocadas:

class Factura {
    public function calcularTotal($items) {
        // Solo lógica de la factura
    }
}

class FacturaPdfGenerator {
    public function generar(Factura $factura) {
        // Solo lógica de PDF
    }
}

class NotificadorEmail {
    public function enviarFactura(Factura $factura, $emailDestino) {
        // Solo lógica de email
    }
}

Ahora, si cambia el proveedor de email, solo tocamos NotificadorEmail. La clase Factura sigue intacta.

2. Open/Closed Principle (OCP) - Principio de Abierto/Cerrado

La definición formal dice: "Las entidades de software (clases, módulos, funciones) deben estar abiertas para la extensión, pero cerradas para la modificación".

Esto significa que deberías poder añadir nueva funcionalidad a tu aplicación sin tener que modificar el código fuente existente. Modificar código existente conlleva el riesgo de introducir nuevos bugs en características que ya estaban probadas y funcionando.

Ejemplo de violación del OCP

Supongamos que tenemos un procesador de pagos. Al principio solo aceptábamos tarjetas de crédito, pero el negocio creció y ahora queremos aceptar PayPal.

class ProcesadorPagos {
    public function procesar($tipo, $monto) {
        if ($tipo === 'tarjeta') {
            // Conectar con Stripe y cobrar
        } elseif ($tipo === 'paypal') {
            // Conectar con la API de PayPal y cobrar
        }
    }
}

Cada vez que añadamos un nuevo método de pago (Criptomonedas, Transferencia), tendremos que abrir esta clase y añadir más elseif. Estamos modificando una clase existente constantemente.

Cómo aplicar OCP correctamente

Usamos interfaces o clases abstractas (polimorfismo) para extender el comportamiento:

interface MetodoPago {
    public function procesarPago($monto);
}

class PagoTarjeta implements MetodoPago {
    public function procesarPago($monto) {
        // Lógica de Stripe
    }
}

class PagoPayPal implements MetodoPago {
    public function procesarPago($monto) {
        // Lógica de PayPal
    }
}

class ProcesadorPagos {
    public function procesar(MetodoPago $metodo, $monto) {
        $metodo->procesarPago($monto);
    }
}

Si mañana agregamos PagoBitcoin, simplemente creamos una nueva clase que implemente MetodoPago. La clase ProcesadorPagos permanece cerrada a modificaciones pero está abierta a extender su funcionalidad.

3. Liskov Substitution Principle (LSP) - Principio de Sustitución de Liskov

La definición formal dice: "Las funciones que utilicen punteros o referencias a clases base deben ser capaces de usar objetos de clases derivadas sin saberlo". Creado por Barbara Liskov, este principio se centra en asegurar que una subclase pueda reemplazar a su superclase sin romper el programa.

Ejemplo de violación del LSP

El ejemplo clásico es el de las aves. Si tienes una clase base Ave con un método volar().

class Ave {
    public function volar() {
        return "Aleteando por el cielo";
    }
}

class Loro extends Ave {}

class Pinguino extends Ave {
    public function volar() {
        throw new Exception("¡Los pingüinos no pueden volar!");
    }
}

Si nuestro programa recibe una colección de objetos Ave y hace que todos vuelen, fallará catastróficamente al llegar al pingüino. El Pinguino no es un sustituto válido para Ave bajo el contexto actual porque cambia drásticamente el comportamiento esperado.

Cómo aplicar LSP correctamente

Debemos replantear nuestras abstracciones. No todas las aves vuelan.

class Ave {
    public function comer() {
        return "Comiendo semillas o pescado";
    }
}

interface AveVoladora {
    public function volar();
}

class Loro extends Ave implements AveVoladora {
    public function volar() {
        return "Volando entre árboles";
    }
}

class Pinguino extends Ave {
    public function nadar() {
        return "Nadando en el océano";
    }
}

4. Interface Segregation Principle (ISP) - Principio de Segregación de la Interfaz

La definición formal dice: "Los clientes no deben verse obligados a depender de interfaces que no utilizan".

Es preferible tener muchas interfaces pequeñas y específicas (roles de cliente) que una interfaz gigante y de propósito general. Las interfaces "gordas" obligan a las clases a implementar métodos vacíos o lanzar excepciones para cosas que simplemente no necesitan hacer.

Ejemplo de violación del ISP

Creamos una interfaz para todos los trabajadores de la empresa.

interface Trabajador {
    public function programar();
    public function tomarDescanso();
    public function asistirReunion();
}

class Programador implements Trabajador {
    // Implementa los 3 sin problema
}

class RobotAutomatizado implements Trabajador {
    public function programar() {
        // Código del robot
    }
    public function tomarDescanso() {
        throw new Exception("Los robots no descansan");
    }
    public function asistirReunion() {
        throw new Exception("Los robots no van a reuniones");
    }
}

Cómo aplicar ISP correctamente

Dividimos la interfaz en partes más pequeñas según las capacidades reales.

interface TrabajadorLaboral {
    public function trabajar();
}

interface TrabajadorHumano {
    public function tomarDescanso();
    public function asistirReunion();
}

class RobotAutomatizado implements TrabajadorLaboral {
    public function trabajar() { /* ... */ }
}

class Programador implements TrabajadorLaboral, TrabajadorHumano {
    public function trabajar() { /* ... */ }
    public function tomarDescanso() { /* ... */ }
    public function asistirReunion() { /* ... */ }
}

5. Dependency Inversion Principle (DIP) - Principio de Inversión de Dependencias

La definición formal dice: "Los módulos de alto nivel no deben depender de los módulos de bajo nivel. Ambos deben depender de abstracciones. Las abstracciones no deben depender de los detalles. Los detalles deben depender de las abstracciones".

Este es quizás el principio más poderoso para crear arquitecturas mantenibles. Promueve el uso de Inyección de Dependencias.

Ejemplo de violación del DIP

Un controlador que maneja el registro de usuarios que instancia directamente la clase de conexión a la base de datos MySQL.

class MySQLConnection {
    public function save($data) { /* ... */ }
}

class UserController {
    private $db;
    
    public function __construct() {
        $this->db = new MySQLConnection(); // Fuerte acoplamiento
    }
    
    public function store($request) {
        $this->db->save($request);
    }
}

Si mañana el cliente dice "vamos a migrar a MongoDB", tendrás que reescribir UserController. El módulo de alto nivel (Controller) depende directamente de un detalle de bajo nivel (MySQL).

Cómo aplicar DIP correctamente

Hacemos que el controlador dependa de una abstracción (una Interfaz).

interface DatabaseInterface {
    public function save($data);
}

class MySQLConnection implements DatabaseInterface {
    public function save($data) { /* Implementación MySQL */ }
}

class MongoConnection implements DatabaseInterface {
    public function save($data) { /* Implementación Mongo */ }
}

class UserController {
    private $db;
    
    // Inyección de dependencias
    public function __construct(DatabaseInterface $db) {
        $this->db = $db; 
    }
    
    public function store($request) {
        $this->db->save($request);
    }
}

Ahora UserController no sabe, ni le importa, qué motor de base de datos se está usando. Solo sabe que recibirá "algo" que puede guardar datos.

Conclusión sobre los Principios SOLID

Adoptar los principios SOLID puede parecer abrumador al principio. Requiere cambiar la forma en la que pensamos al diseñar nuestras aplicaciones y a menudo resulta en la creación de más archivos y clases. Sin embargo, el esfuerzo inicial se ve recompensado con creces.

El código que sigue SOLID es inherentemente más limpio, predecible y resistente al paso del tiempo. Como desarrollador, dominar estos cinco principios te separará de los programadores que solo "hacen que funcione" y te acercará a la categoría de los ingenieros de software que diseñan sistemas para que duren.

 

¿Te ha gustado esta entrada?

Compártela con tus compañeros para que también sigan aprendiendo.

Comunidad y Comentarios

0 COMENTARIOS

No hay comentarios todavía. Sé el primero en compartir tu opinión.

Escribe tu opinión
Respondiendo a