Principios SOLID

Principios solid

PrincipiosSOLIDenProgranacionPorJuanFuente0

Para escribir código que sea fácil de mantener, extender y entender, los principios SOLID son un gran aliado. Estos cinco principios son la base del diseño orientado a objetos y ayudan a evitar el «código espagueti». En este artículo, exploro qué son y cómo aplicarlos en Java con ejemplos prácticos. ¡Vamos a ello!

PrincipiosSOLIDenProgranacionPorJuanFuente1
  1.  S: Principio de Responsabilidad Única (Single Responsibility Principle) 

Qué es: 

Una clase debe tener una sola razón para cambiar, es decir, debe tener una única responsabilidad. Si una clase hace demasiadas cosas, se vuelve difícil de mantener. 

Ejemplo en Java: 

Imaginemos una clase Usuario que gestiona tanto los datos del usuario como la lógica de autenticación:

class Usuario { 

  String nombre; 

  String email; 

  void guardarUsuario() { 

    // Lógica para guardar en la base de datos 

  } 

  void enviarEmail() { 

     // Lógica para enviar un email 

  } 

Esto va contra el principio de responsabilidad única. Mejor separar las responsabilidades: 

class Usuario {       

  String nombre; 

  String email; 

class UsuarioRepository { 

  void guardarUsuario(Usuario usuario) { 

    // Lógica para guardar en la base de datos

   } 

class EmailService {

  void enviarEmail(Usuario usuario) { 

    // Lógica para enviar un email 

  } 

Dividir responsabilidades hace el código más modular y fácil de mantener. 

PrincipiosSOLIDenProgranacionPorJuanFuente2
  1. O: Principio de Abierto/Cerrado (Open/Closed Principle) 

Qué es: 

Las entidades de software (clases, módulos, funciones) deben estar abiertas para extensión pero cerradas para modicación. Es decir, se pueden añadir nuevas funcionalidades sin modicar el código existente. 

Ejemplo en Java: 

Imaginemos un sistema que calcula descuentos para diferentes tipos de clientes. Inicialmente, solo hay dos tipos de clientes: ClienteRegular y ClienteVIP. Al añadir uno nuevo, como ClientePremium, no se debería modicar el código existente. En lugar de eso, podemos usar una interfaz o una clase base para permitir la extensión. 

Sin aplicar el principio: 

class Descuento { 

  double calcularDescuento(String tipoCliente, double monto) {

    if (tipoCliente.equals(«Regular»)) {

      return monto * 0.1; // 10% de descuento 

    } else if (tipoCliente.equals(«VIP»)) {

      return monto * 0.2; // 20% de descuento } return 0; 

  } 

Si añadimos un nuevo tipo de cliente, como ClientePremium, habría que modicar el método calcularDescuento, lo que no gusta nada al principio de abierto/cerrado. 

Aplicando el principio: 

interface Cliente {

  double calcularDescuento(double monto); 

class ClienteRegular implements Cliente {

  public double calcularDescuento(double monto) {

    return monto * 0.1; // 10% de descuento 

  } 

class ClienteVIP implements Cliente {

  public double calcularDescuento(double monto) {

    return monto * 0.2; // 20% de descuento

  } 

class ClientePremium implements Cliente {

  public double calcularDescuento(double monto) {

    return monto * 0.3; // 30% de descuento 

  } 

Este principio permite añadir nuevas funcionalidades sin tocar el código existente, reduciendo el riesgo de introducir errores. 

PrincipiosSOLIDenProgranacionPorJuanFuente3
  1. L: Principio de Sustitución de Liskov (Liskov Substitution Principle) 

Qué es: 

Las clases derivadas deben poder sustituir a sus clases base sin alterar el comportamiento del programa. En otras palabras, si tienes una clase A y una clase B que hereda de A, deberías poder usar B en lugar de A sin problemas. 

Ejemplo en Java: 

Imaginemos una clase Rectangulo y una subclase Cuadrado. Un cuadrado es un tipo de rectángulo, pero tiene una restricción adicional: sus lados deben ser iguales. Si no se diseña correctamente, esto puede romper con el principio de Liskov. 

Sin aplicar el principio: 

class Rectangulo {

  protected int ancho; 

  protected int alto; 

  void setAncho(int ancho) {

    this.ancho = ancho; 

  } 

  void setAlto(int alto) {

    this.alto = alto; 

  } 

  int calcularArea() {

    return ancho * alto; 

  } 

class Cuadrado extends Rectangulo {

  void setAncho(int ancho) {

    this.ancho = ancho; 

    this.alto = ancho; // Forzar que el alto sea igual al ancho 

  } 

  void setAlto(int alto) { 

    this.alto = alto; this.ancho = alto; // Forzar que el ancho sea igual al alto 

  }

Aquí, Cuadrado no puede sustituir a Rectangulo sin problemas. Por ejemplo: 

Rectangulo rectangulo = new Cuadrado(); 

rectangulo.setAncho(5); 

rectangulo.setAlto(10); 

System.out.println(rectangulo.calcularArea()); // Imprime 100, pero debería ser 50 

 

Aplicando el principio: 

abstract class Figura {

  abstract int calcularArea(); 

class Rectangulo extends Figura {

  private int ancho; 

  private int alto; 

  Rectangulo(int ancho, int alto) {

    this.ancho = ancho; 

    this.alto = alto; 

  } 

  int calcularArea() {

    return ancho * alto; 

  } 

class Cuadrado extends Figura {

  private int lado; 

  Cuadrado(int lado) {

    this.lado = lado; 

  }  

  int calcularArea() {

    return lado * lado; 

  } 

Ahora, Rectángulo y Cuadrado son clases independientes que extienden Figura. Ambas pueden calcular su área, pero no hay conflictos en su comportamiento. 

PrincipiosSOLIDenProgranacionPorJuanFuente4
  1. I: Principio de Segregación de Interfaces (Interface Segregation Principle) 

Qué es: 

Los clientes no deben depender de interfaces que no usan. Esto, que suena un poco confuso, quiere decir que es mejor tener muchas interfaces pequeñas y especícas que una interfaz grande y genérica. 

Ejemplo en Java: 

Imaginemos una interfaz Animal con métodos para volar, nadar y caminar: 

interface Animal {

  void volar(); 

  void nadar(); 

  void caminar(); 

Esto obligaría a clases como Pinguino a implementar métodos que no usan. Mejor dividir la interfaz: 

interface Volador {

  void volar(); 

interface Nadador {

  void nadar(); 

interface Caminador {

  void caminar(); 

class Pinguino implements Nadador, Caminador {

  public void nadar() {

    /* Implementación */ 

  }

  public void caminar() {

   /* Implementación */ 

  } 

Este principio hace que las interfaces sean más específicas y fáciles de implementar.

PrincipiosSOLIDenProgranacionPorJuanFuente5
  1. D: Principio de Inversión de Dependencias (Dependency Inversion Principle) 

Qué es: 

Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones. Además, las abstracciones no deben depender de los detalles, sino al revés. Vamos a verlo en un ejemplo. 

Ejemplo en Java: 

Imaginemos una aplicación que envía notificaciones a los usuarios. Inicialmente, la aplicación solo envía notificaciones por correo electrónico, pero en el futuro podría enviarlas por SMS o WhatsApp. Aquí es donde el principio de inversión de dependencias entra en juego. 

class EmailService {

  void enviar(String mensaje) {

    System.out.println(«Enviando email: » + mensaje); 

  } 

// Notificador depende directamente de EmailService 

class Notificador {

  private EmailService emailService = new EmailService(); 

  void notificar(String mensaje) {

    emailService.enviar(mensaje); 

  } 

Se usaría así: 

public class Main {

  public static void main(String[] args) {

    Notificador notificador = new Notificador(); 

    notificador.noticias(«Hola, esto es una notificación por email.»);

    }

  }

Pero si se decide añadir soporte para SMS, habría que modificar la clase Notificador para añadir una dependencia a SMSService. Mejor usar una abstracción: 

// Definimos una abstracción (interfaz) 

interface ServicioMensajes {

  void enviar(String mensaje); 

// Implementación concreta para enviar emails 

class EmailService implements ServicioMensajes {

  public void enviar(String mensaje) {

    System.out.println(«Enviando email: » + mensaje); 

  } 

// Implementación concreta para enviar SMS 

class SMSService implements ServicioMensajes {

  public void enviar(String mensaje) {

    System.out.println(«Enviando SMS: » + mensaje); 

  } 

// Notificador depende de la abstracción (ServicioMensajes) 

class Notificador {

  private ServicioMensajes servicio; 

  // Inyectamos la dependencia a través del constructor 

  Notificador(ServicioMensajes servicio) {

    this.servicio = servicio; 

  } 

  void notificar(String mensaje) {

    servicio.enviar(mensaje); 

  } 

Se usaría así: 

public class Main {

  public static void main(String[] args) {

    // Usamos EmailService 

    ServicioMensajes emailService = new EmailService(); 

    Notificador noticadorEmail = new Notificador(emailService);

    notificadorEmail.notificar(«Hola, esto es una notificación por email.»); 

    // Usamos SMSService 

    ServicioMensajes smsService = new SMSService(); 

    Notificador notificadorSMS = new Notificador(smsService); 

    notificadorSMS.notificar(«Hola, esto es una notificación por SMS.»); 

  } 

}

Este principio facilita el cambio de implementaciones y hace el código más flexible. En lugar de depender de implementaciones concretas, dependemos de abstracciones. Esto hace que el código sea más fácil de extender y menos propenso a errores cuando necesitamos hacer cambios. 

 

Resumen: 

  • S: Single. Una clase, una responsabilidad. 
  • O: Open. Abierto para extensión, cerrado para modificación. 
  • L: Liskov. Las subclases deben poder sustituir a sus clases base. 
  • I: Interfaces. Interfaces pequeñas y específicas. 
  • D: Dependencies. Depende de abstracciones, no de implementaciones. 

¿Ya aplicas estos principios en tus proyectos? ¡Cuéntame en los comentarios!