SOLID

Los principios SOLID son un conjunto de cinco principios de diseño de software que fueron introducidos por el ingeniero de software Robert C. Martin para crear sistemas más comprensibles, flexibles y mantenibles.

A continuación, se explican brevemente cada uno de los principios SOLID, junto con ejemplos ilustrativos:

Principio de Responsabilidad Única (Single Responsibility Principle – SRP):

  • Este principio establece que una clase debe tener una sola razón para cambiar, es decir, debe tener una única responsabilidad.
// Mal: Una clase que maneja la lógica de usuario y la conexión a la base de datos.
public class UserAndDatabaseManager {
    public void processUser() {
        // lógica de usuario
    }

    public void connectToDatabase() {
        // conexión a la base de datos
    }
}

// Bien: Separación de responsabilidades en dos clases distintas.
public class UserManager {
    public void processUser() {
        // lógica de usuario
    }
}

public class DatabaseConnector {
    public void connectToDatabase() {
        // conexión a la base de datos
    }
}

Principio de Abierto/Cerrado (Open/Closed Principle – OCP):

  • Este principio sostiene que una clase debe estar abierta para su extensión pero cerrada para su modificación. En otras palabras, se deben poder agregar nuevas funcionalidades sin cambiar el código existente.
// Mal: Necesidad de modificar la clase existente para agregar una nueva forma.
public class AreaCalculator {
    public double calculateRectangleArea(Rectangle rectangle) {
        return rectangle.getWidth() * rectangle.getHeight();
    }
}

// Bien: Extensión mediante una interfaz común.
public interface Shape {
    double calculateArea();
}

public class Rectangle implements Shape {
    // implementación para el rectángulo
}

public class Circle implements Shape {
    // implementación para el círculo
}

Principio de Sustitución de Liskov (Liskov Substitution Principle – LSP):

  • Este principio establece que los objetos de una clase base deben poder ser reemplazados por objetos de una clase derivada sin afectar la corrección del programa.
class Bird {
    public void fly() {
        System.out.println("I can fly");
    }
}

class Penguin extends Bird {
    // Los pingüinos no pueden volar, por lo que no implementamos el método fly
}

// Función que hace volar a un pájaro
void makeBirdFly(Bird bird) {
    bird.fly();
}

// Uso de la función con un objeto de tipo Penguin
Bird penguin = new Penguin();
makeBirdFly(penguin);  // Esto imprimirá "I can fly", lo cual es incorrecto para un pingüino

En este caso, la clase Penguin hereda de Bird, pero no implementa el método fly porque los pingüinos no pueden volar. Aunque desde el punto de vista de la herencia parece correcto, cuando intentamos hacer volar a un pingüino, obtenemos una salida incorrecta. Esto es una violación del LSP, ya que no podemos sustituir correctamente un Penguin por un Bird en todas las situaciones, ya que los pingüinos no vuelan.

El Principio de Sustitución de Liskov (LSP) establece que los objetos de una clase base deben poder ser sustituidos por objetos de una clase derivada sin afectar la corrección del programa. En resumen:

  1. La herencia debe respetar el contrato de la clase base.
  2. Las subclases deben poder ser utilizadas en cualquier lugar donde se espera un objeto de la clase base.
  3. Las subclases no deben alterar el comportamiento de la clase base de una manera que pueda sorprender o introducir errores en el código que las utiliza.

En esencia, el LSP garantiza que las clases derivadas sean verdaderas extensiones de las clases base, preservando la semántica y el comportamiento esperados, y permitiendo que las instancias de subclases sean sustituidas sin problemas en cualquier contexto que utilice la clase base.

Principio de Segregación de Interfaces (Interface Segregation Principle – ISP):

  • Este principio establece que una clase no debe verse obligada a implementar interfaces que no utiliza. En lugar de eso, se deben crear interfaces específicas para cada cliente.
// Mal: Una única interfaz con métodos que no todas las clases implementan.
public interface Worker {
    void work();
    void eat();
}

// Bien: Interfaces segregadas según las necesidades.
public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public class Robot implements Workable {
    @Override
    public void work() {
        // lógica de trabajo para un robot
    }
}

public class Human implements Workable, Eatable {
    @Override
    public void work() {
        // lógica de trabajo para un humano
    }
    
    @Override
    public void eat() {
        // lógica de alimentación para un humano
    }
}

Principio de Inversión de Dependencias (Dependency Inversion Principle – DIP):

  • Este principio establece que los módulos de alto nivel no deben depender de los módulos de bajo nivel, sino de abstracciones. Además, las abstracciones no deben depender de los detalles, sino que los detalles deben depender de las abstracciones.
// Mal: Dependencia directa de una implementación concreta.
public class LightSwitch {
    private LightBulb bulb;
    
    public LightSwitch() {
        this.bulb = new IncandescentBulb();
    }
    
    public void toggle() {
        bulb.turnOnOrOff();
    }
}

// Bien: Dependencia invertida mediante una interfaz.
public interface Switchable {
    void turnOnOrOff();
}

public class LightSwitch {
    private Switchable device;
    
    public LightSwitch(Switchable device) {
        this.device = device;
    }
    
    public void toggle() {
        device.turnOnOrOff();
    }
}

public class IncandescentBulb implements Switchable {
    @Override
    public void turnOnOrOff() {
        // lógica de encendido o apagado para una bombilla incandescente
    }
}

Estos principios SOLID proporcionan pautas valiosas para diseñar software que sea flexible, extensible y fácil de mantener. La aplicación de estos principios contribuye a la creación de sistemas robustos y adaptativos a medida que evolucionan con los requisitos cambiantes del software.