SOLID: Guía Completa de los 5 Principios de Diseño Orientado a Objetos

SOLID: Guía Completa de los 5 Principios de Diseño Orientado a Objetos

/* ---- --- */
Alejandro Robles
Alejandro Robles
Por: Alejandro Robles
Publicado el:

SOLID: La guía completa de los 5 principios en la POO

SOLID es un acrónimo de los primeros cinco principios de diseño orientado a objetos (OOD) de Robert C. Martin (también conocido como Uncle Bob).

Nota: Aunque estos principios pueden aplicarse a varios lenguajes de programación, el código de ejemplo en este artículo usará PHP.

Estos principios establecen prácticas para desarrollar software con consideraciones de mantenimiento y extensibilidad a medida que el proyecto crece. Adoptar estas prácticas también puede ayudar a evitar “code smells”, refactorizar código y desarrollar software Ágil o Adaptativo.

SOLID significa:

En este artículo, se presentará cada principio individualmente para entender cómo SOLID puede ayudarte a ser un mejor desarrollador.

Single-responsibility Principle (Principio de Responsabilidad Única)

El Principio de Responsabilidad Única establece que una clase debe tener una sola razón para cambiar, es decir, una única responsabilidad.

Por ejemplo, imagina una aplicación de gestión de facturas donde necesitas calcular el total de una factura y luego generar diferentes formatos de reporte. Para cumplir el SRP, separa estas responsabilidades en dos clases:

class CalculadoraFactura
{
    private $facturas;

    public function __construct(array $facturas = [])
    {
        $this->facturas = $facturas;
    }

    public function calcularTotal(): float
    {
        $total = 0;
        foreach ($this->facturas as $factura) {
            $total += $factura->monto;
        }
        return $total;
    }
}

class GeneradorReporte
{
    private $calculadora;

    public function __construct(CalculadoraFactura $calculadora)
    {
        $this->calculadora = $calculadora;
    }

    public function generarJSON(): string
    {
        return json_encode(['total' => $this->calculadora->calcularTotal()]);
    }

    public function generarHTML(): string
    {
        return '

Total de factura: ' . $this->calculadora->calcularTotal() . '

'; } } // Uso: $facturas = [ (object)['monto' => 100.50], (object)['monto' => 200.75], ]; $calculadora = new CalculadoraFactura($facturas); $reporte = new GeneradorReporte($calculadora); echo $reporte->generarHTML(); echo $reporte->generarJSON();

En este ejemplo, CalculadoraFactura se encarga únicamente de calcular el total de las facturas, iterando sobre el arreglo y sumando sus montos. La clase GeneradorReporte recibe una instancia de CalculadoraFactura y es responsable solo de formatear ese resultado en JSON o HTML. De este modo, cada clase tiene una única responsabilidad y cambia por motivos distintos: la calculadora cambia si cambia la lógica de suma, y el generador de reportes cambia si cambia el formato de salida.

Open-closed Principle (Principio de Abierto/Cerrado)

El Principio de Abierto/Cerrado dicta que una entidad debe estar abierta para extensión, pero cerrada para modificación.

Supongamos que tenemos un sistema de descuentos. Mediante el patrón Estrategia, podemos agregar nuevos tipos de descuento sin modificar la clase principal:

interface EstrategiaDescuento
{
    public function calcular(float $monto): float;
}

class DescuentoVerano implements EstrategiaDescuento
{
    public function calcular(float $monto): float
    {
        return $monto * 0.90; // 10% de descuento
    }
}

class DescuentoNavidad implements EstrategiaDescuento
{
    public function calcular(float $monto): float
    {
        return $monto * 0.80; // 20% de descuento
    }
}

class GestorDescuentos
{
    private $estrategia;

    public function __construct(EstrategiaDescuento $estrategia)
    {
        $this->estrategia = $estrategia;
    }

    public function aplicar(float $monto): float
    {
        return $this->estrategia->calcular($monto);
    }
}

// Uso:
$gestor = new GestorDescuentos(new DescuentoVerano());
echo $gestor->aplicar(150.00);

$gestor = new GestorDescuentos(new DescuentoNavidad());
echo $gestor->aplicar(150.00);

La interfaz EstrategiaDescuento define el contrato para las distintas estrategias de descuento. Cada clase (como DescuentoVerano o DescuentoNavidad) extiende este comportamiento sin modificar GestorDescuentos. Cuando quieras un nuevo descuento, simplemente creas otra implementación de EstrategiaDescuento, manteniendo la clase gestionadora inalterada.

Liskov Substitution Principle (Principio de Sustitución de Liskov)

El Principio de Sustitución de Liskov establece que los objetos de una subclase deben poder reemplazar a los objetos de la clase padre sin alterar el correcto funcionamiento del programa.

Por ejemplo, definimos una interfaz Vehiculo con un método avanzar que devuelve kilómetros recorridos:

interface Vehiculo
{
    public function avanzar(): int;
}

class Coche implements Vehiculo
{
    public function avanzar(): int
    {
        return 100;
    }
}

class Bicicleta implements Vehiculo
{
    public function avanzar(): int
    {
        return 20;
    }
}

function recorridoTotal(array $vehiculos): int
{
    $total = 0;
    foreach ($vehiculos as $vehiculo) {
        $total += $vehiculo->avanzar();
    }
    return $total;
}

// Uso:
$vehiculos = [new Coche(), new Bicicleta()];
echo recorridoTotal($vehiculos); // 120

En esta implementación, tanto Coche como Bicicleta cumplen el contrato de Vehiculo. La función recorridoTotal no necesita diferenciar entre ellos: puede sustituir cualquier instancia de Vehiculo sin romperse. Esto demuestra que las subclases son intercambiables con la clase base.

Interface Segregation Principle (Principio de Segregación de Interfaces)

El Principio de Segregación de Interfaces establece que no se debe obligar a una clase a depender de métodos que no utiliza.

En lugar de una única interfaz con todos los métodos, dividimos la funcionalidad en interfaces más pequeñas:

interface Llamable
{
    public function llamar(string $numero): void;
}

interface Fotografiable
{
    public function tomarFoto(): void;
}

class TelefonoBasico implements Llamable
{
    public function llamar(string $numero): void
    {
        // Lógica de llamada
    }
}

class Smartphone implements Llamable, Fotografiable
{
    public function llamar(string $numero): void
    {
        // Lógica de llamada
    }

    public function tomarFoto(): void
    {
        // Lógica de foto
    }
}

// Uso:
$telefono = new TelefonoBasico();
$telefono->llamar('555-1234');

$movil = new Smartphone();
$movil->tomarFoto();

La interfaz Llamable solo contiene el método llamar, mientras que Fotografiable contiene tomarFoto. TelefonoBasico no está forzado a implementar un método de foto que no necesita. Cada clase implementa únicamente las interfaces relevantes a su responsabilidad.

Dependency Inversion Principle (Principio de Inversión de Dependencias)

El Principio de Inversión de Dependencias dicta que las clases de alto nivel no deben depender de clases de bajo nivel, sino de abstracciones.

Veamos un ejemplo de envío de notificaciones por correo:

interface ProveedorCorreo
{
    public function enviar(string $mensaje): bool;
}

class SMTPProveedor implements ProveedorCorreo
{
    public function enviar(string $mensaje): bool
    {
        // Enviar correo vía SMTP
        return true;
    }
}

class EnviadorNotificaciones
{
    private $proveedor;

    public function __construct(ProveedorCorreo $proveedor)
    {
        $this->proveedor = $proveedor;
    }

    public function notificar(string $mensaje): bool
    {
        return $this->proveedor->enviar($mensaje);
    }
}

// Uso:
$proveedor = new SMTPProveedor();
$enviador = new EnviadorNotificaciones($proveedor);
$enviador->notificar('Tu pedido ha sido enviado.');

Aquí EnviadorNotificaciones depende de la abstracción ProveedorCorreo en lugar de una implementación concreta. Si mañana quieres usar otro servicio (por ejemplo, una API REST), solo creas una nueva clase que implemente ProveedorCorreo y la inyectas, sin tocar la lógica de notificaciones.

Conclusión

Hemos visto ejemplos que cumplen al 100% cada principio SOLID usando nombres y variables en español. Aplicar estos principios facilita la mantenibilidad, extensibilidad y testeo de tu código.

Para profundizar, explora prácticas Ágiles en Agile y el desarrollo de software adaptativo en Adaptive software development.

Estamos viviendo una nueva revolución tecnológica. ¡Implementemos juntos soluciones épicas que impulsen tu trabajo, tus clientes o tu negocio! 🚀

Sígueme en mis redes sociales: