Los Principios SOLID explicados

14 minutos de lecturaOctubre de 2024
portada snippetsVer en YouTube

Open-closed (O)

Ahora que ya tienes a tus especialistas, ¿qué pasaría si un día decides que quieres agregar una piscina? No derrumbarías toda la casa, ¿cierto? Simplemente añadirías la piscina sin alterar lo que ya funciona.

El principio de abierto/cerrado nos dice que nuestras clases deben estar abiertas para la extensión, pero cerradas para la modificación. Es decir, podemos agregar nuevas funcionalidades sin tener que cambiar el código existente. Esto protege el código de futuras roturas.

Puntos clave:

  • Código extensible
  • Evite modificar el código para casos específicos
  • Evite cambiar el comportamiento original del código
  • Polimorfismo

Diagrama UML:

Figura 2: Videogame. Open-Close

Código de implementación correcta:

videogame.java
package co.com.sebastianagudelo.solid.o

// Clase base abstracta que representa un personaje con acciones comunes
public abstract class Character {
    // Método para atacar
    public void attack() {
        System.out.println("Character attacks!");
    }

    // Método para defender
    public void defend() {
        System.out.println("Character defends!");
    }

    // Método para moverse
    public void move() {
        System.out.println("Character moves!");
    }
}

// Subclase que representa un dragón, que hereda las acciones comunes de Character
public class Dragon extends Character {
    // Método específico para el dragón que le permite volar
    public void fly() {
        System.out.println("Dragon flies!");
    }
}

// Subclase que representa un mago, que hereda las acciones comunes de Character
public class Wizard extends Character {
    // Método específico para el mago que le permite lanzar hechizos
    public void castSpell() {
        System.out.println("Wizard casts a spell!");
    }
}

// Subclase que representa un espadachín, que hereda las acciones comunes de Character  
public class SwordsMan extends Character {
    // Método específico para el espadachín que le permite usar dos espadas
    public void useTwoSwords() {
        System.out.println("Swords Man uses two swords!");
    }
}

// Clase principal para demostrar el uso de diferentes personajes
public class Main {
    public static void main(String[] args) {
        // Crear un personaje de tipo Dragon y realizar acciones específicas
        Character dragon = new Dragon();
        dragon.attack(); // Acción común de atacar
        ((Dragon) dragon).fly(); // Acción específica de volar del dragón

        // Crear un personaje de tipo Wizard y realizar acciones específicas
        Character wizard = new Wizard();
        wizard.defend(); // Acción común de defender
        ((Wizard) wizard).castSpell(); // Acción específica de lanzar hechizos del mago

        // Crear un personaje de tipo SwordsMan y realizar acciones específicas
        Character swordsMan = new SwordsMan();
        swordsMan.move(); // Acción común de moverse
        ((SwordsMan) swordsMan).useTwoSwords(); // Acción específica de usar dos espadas del espadachín
    }
}
Código 2: Videogame. Open-Close

Liskov substitution (L)

En esta etapa del viaje, imagina que necesitas reemplazar a un trabajador por otro. Esperarías que el nuevo trabajador pueda desempeñar la misma función sin problemas, ¿verdad?

El principio de sustitución de Liskov establece que las clases derivadas deben poder reemplazar a sus clases base sin alterar el funcionamiento del programa. Esto asegura que las jerarquías de herencia funcionen de manera coherente.

Puntos clave:

  • Abstracción
  • Contratos o interfaces
  • Identificar comportamientos comunes

Diagrama UML:

Figura 3: Vehicles. Liskov substitution

Código de implementación correcta:

vehicles.java
package co.com.sebastianagudelo.solid.l

// Clase base que representa un vehículo
abstract class Vehicle {
    // Método que indica que el vehículo puede moverse
    public void move() {
        System.out.println("Can move");
    }
}

// Subclase para vehículos con motor
class EngineVehicle extends Vehicle {
    // Método que permite arrancar el motor del vehículo
    public void startEngine() {
        System.out.println("Can start engine");
    }
}

// Subclase para vehículos voladores
class FlyingVehicle extends EngineVehicle {
    // Método que permite que el vehículo vuele
    public void fly() {
        System.out.println("Can fly");
    }
}

// Clase para bicicleta (no tiene motor ni puede volar)
class Bicycle extends Vehicle {
    // No necesita métodos adicionales; usa el método move() de Vehicle
}

// Clase para carro (tiene motor pero no puede volar)
class Car extends EngineVehicle {
    // Usa el método startEngine() de EngineVehicle
}

// Clase para avión (tiene motor y puede volar)
class Airplane extends FlyingVehicle {
    // Usa los métodos startEngine() y fly() de FlyingVehicle
}

// Ejemplo de uso: clase principal que muestra el uso de diferentes tipos de vehículos
public class Main {
    public static void main(String[] args) {
        Bicycle bike = new Bicycle();
        bike.move(); // La bicicleta puede moverse

        Car car = new Car();
        car.move(); // El carro puede moverse
        car.startEngine(); // El carro puede arrancar el motor

        Airplane plane = new Airplane();
        plane.move(); // El avión puede moverse
        plane.startEngine(); // El avión puede arrancar el motor
        plane.fly(); // El avión puede volar
    }
}
Código 3: Vehicles, Liskov substitution

Interface segregation (I)

Aquí, tenemos otra lección importante: no sobrecargues a tus trabajadores con tareas innecesarias. Si un trabajador solo necesita una herramienta específica para hacer su trabajo, ¿por qué forzarle a cargar con un maletín completo de herramientas que no va a usar?

Este principio dice que no debemos forzar a los clientes a depender de interfaces que no usan. Cada interfaz debe ser específica para el propósito que necesita cumplir, lo que hace que el código sea más limpio y manejable.

Puntos clave:

  • Definir interfaces pequeñas
  • Implementar solo las interfaces solicitadas
  • Identificar grupos con métodos comunes

Diagrama UML:

Figura 4: Robots. Interface segregation

Código de implementación correcta:

robots.java
package co.com.sebastianagudelo.solid.i

// Clase base
abstract class Vehicle {
    public void move() {
        System.out.println("Can move");
    }
}

// Interface para vehículos con motor
interface EngineVehicle {
    void startEngine();
}

// Interface para transformación
interface Transformable {
    void transform();
}

// Interface para combate
interface Warrior {
    void attack();
}

// Subclase para vehículos voladores
class FlyingVehicle extends Vehicle {
    public void fly() {
        System.out.println("Can fly");
    }
}

// Clase para Autobot, que implementa EngineVehicle, Transformable, y Warrior
class Autobot extends Vehicle implements EngineVehicle, Transformable, Warrior {
    @Override
    public void startEngine() {
        System.out.println("Autobot can start engine");
    }

    @Override
    public void transform() {
        System.out.println("Autobot can transform");
    }

    @Override
    public void attack() {
        System.out.println("Autobot can attack");
    }
}

// Clase para Decepticon, que implementa EngineVehicle, Transformable, y Warrior, y puede volar
class Decepticon extends FlyingVehicle implements EngineVehicle, Transformable, Warrior {
    @Override
    public void startEngine() {
        System.out.println("Decepticon can start engine");
    }

    @Override
    public void transform() {
        System.out.println("Decepticon can transform");
    }

    @Override
    public void attack() {
        System.out.println("Decepticon can attack");
    }
}

// Ejemplo de uso
public class Main {
    public static void main(String[] args) {
        Autobot autobot = new Autobot();
        autobot.move();
        autobot.startEngine();
        autobot.transform();
        autobot.attack();

        Decepticon decepticon = new Decepticon();
        decepticon.move();
        decepticon.startEngine();
        decepticon.transform();
        decepticon.attack();
        decepticon.fly();
    }
}
Código 4: Robots, Interface segregation

Dependency inversion (D)

Finalmente, llega la parte clave: ¿a quién le das las instrucciones en una obra? No se las das a los martillos o a las herramientas, sino a los capataces. De la misma manera, en el código, las clases de alto nivel no deben depender de los detalles. Ambas deben depender de abstracciones.

Este principio nos enseña a invertir las dependencias: el código debe depender de interfaces o abstracciones, no de implementaciones concretas. Esto asegura que el código sea más flexible y fácil de modificar en el futuro.

Puntos clave:

  • Desacoplamiento
  • Interfaces y clases abstractas
  • Inyección de dependencia o ubicación de servicio

Diagrama UML:

Figura 5: CheckoutService. Dependency inversion

Código de implementación correcta:

CheckoutService.java
package co.com.sebastianagudelo.solid.d

// Servicio de pago abstracto
abstract class AbstractPaymentService {
    public abstract void processPayment(double amount);
}

// Implementación del servicio de pago con tarjeta de crédito
class CreditCardPaymentService extends AbstractPaymentService {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing credit card payment of $" + amount);
    }
}

// Servicio de checkout que utiliza el servicio de pago
class CheckoutService {
    private AbstractPaymentService paymentService;

    public CheckoutService(AbstractPaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public void checkout(double amount) {
        System.out.println("Initiating checkout for amount $" + amount);
        paymentService.processPayment(amount);
    }
}

// Ejemplo de uso
public class Main {
    public static void main(String[] args) {
        AbstractPaymentService paymentService = new CreditCardPaymentService();
        CheckoutService checkoutService = new CheckoutService(paymentService);

        checkoutService.checkout(100.0);
    }
}
Código 5: CheckoutService. Dependency inversion

El Triunfo – ¿Por qué SOLID es Importante?

SOLID te da las herramientas para escribir código limpio, flexible y fácil de mantener. Evita que caigas en la trampa del código espagueti y te asegura que tu software pueda crecer y evolucionar sin colapsar.

Además te facilita los siguientes puntos:

  • Mantenibilidad
  • Legibilidad del código
  • IFacil de probar
  • Reusable
  • Escalable

En definitiva, los principios SOLID son como un conjunto de reglas que mantienen a tu código en forma, preparándolo para cualquier desafío que venga.

Cierre – El Futuro del Héroe

A medida que el software sigue evolucionando, las herramientas que usamos también lo harán. Pero los principios fundamentales como SOLID siempre estarán ahí, listos para guiarte en la creación de código robusto y duradero.

Así que, la próxima vez que empieces un proyecto, pregúntate: ¿está mi código siguiendo los principios SOLID? Si lo está, estás en el camino correcto.

Recuerda suscribirte, y darle a like si te ha gustado el contenido, y comparte para que más desarrolladores eleven su seniority.

Recuerden, todo lo que hagan, haganlo con pasión y amor con pasión y amor. Muchas gracias




Conviértete en un desarrollador senior y mantente actualizado

Nuestro blog te brindará consejos y estrategias para mejorar tus habilidades técnicas, avanzar en tu carrera y mantenerte al día con las últimas tendencias en tecnología.