Categorías
Desarrollo

Principios SOLID: la importancia de un código de calidad

Vamos a abordar los principios de una buena práxis a la hora de trabajar con cualquier tipo de código. Algo tan sencillo que es fácil no percatarnos de su utilidad.

¿Cuántas veces nos hemos encontrado un código realizado de forma diferente al nuestro? ¿Cuál es mejor? ¿Cómo «escribir» código que en un futuro lo «leerá» y trabajará sobre él otro desarrollador? Puede que los principios SOLID te ayuden en este momento.

¿Qué son los principios SOLID?

Los principios SOLID son un conjunto de normas o pautas de diseño de software que se utilizan para desarrollar código de alta calidad, fácil de entender, reutilizar y extender.

Crear una buena arquitectura software de calidad, siguiendo unos sencillos pasos en la organización y estructura de los componentes y sus relaciones, es posible. Vamos a desgranarlos con un par de ejemplos que nos resulte más fácil su comprensión.

1. Principio de responsabilidad única (SRP)

Una clase debería tener una sola responsabilidad y un solo motivo para cambiar.

INCORRECTO:

public class Usuario{ 
   private String nombre; 
   private String email; 

   public Usuario(String nombre, String email){ 
      this.nombre = nombre; 
      this.email = email; } 

   // getters/setters 

   public void enviarEmail(String asunto, String cuerpo){ 
      // envío de email 
   }

   public void insertarUsuario(){
      // insertar registro en BBDD
   } 

   public void modificarUsuario(){
      // modificar registro en BBDD
   } 

   public void eliminarUsuario(){
      // eliminar registro en BBDD
   } 
}

En este ejemplo, se viola el principio SRP, ya que esta clase tiene varias responsabilidades: enviar emails y realizar operaciones de base de datos. Para cumplir con este principio, sería mejor separar dichas responsabilidades en clases separadas.

CORRECTO:

public class UsuarioEmail { 
   private String nombre; 
   private String email; 

   public Usuario(String nombre, String email) { 
      this.nombre = nombre; 
      this.email = email; } 

   // getters/setters 

   public void enviarEmail(String asunto, String cuerpo) { 
      // envío de email 
   }
}
public class Usuario { 
   private String nombre; 
   private String email; 

   public Usuario(String nombre, String email) { 
      this.nombre = nombre; 
      this.email = email; } 

   // getters/setters 

   public void insertarUsuario() {
      // insertar registro en BBDD
   } 

   public void modificarUsuario() {
      // modificar registro en BBDD
   } 

   public void eliminarUsuario() {
      // eliminar registro en BBDD
   } 
}

Este ejemplo cumple con el principio SRP, ya que cada clase tiene una única función.

2. Principio de abierto/cerrado (OCP)

Las clases deben estar abiertas para la extensión, pero cerradas para la modificación.

INCORRECTO:

public class calcularArea {
   public double calcular(String forma, double... argumentos) {
      if(forma.equals("circulo")) {
         double radio = argumento[0];
         return Math.PI * Math.pow(radio, 2);
      } else if(forma.equals("rectangulo")) {
         double alto = argumento[0];
         double ancho = argumento[1];
         return alto * ancho;
      } else {
         throw new IllegalArgumentException("Forma incorrecta");
      }
   }
}

En este ejemplo, para agregar una nueva figura geométrica, debemos modificar la clase «calcularArea», además que le pasamos el tipo de figura a través de un ‘String’ lo que dificulta aún más el mantenimiento y la extensión del código. Para solucionar esto, podemos crear una interfaz «Forma» que contenga las clases de las diferentes figuras geométricas, y la clase independiente a ellas para calcular el área.

CORRECTO:

public interface Forma {
   double area();
}

public class Circulo implements Forma {
   private double radio;

   public Circulo(double radio) {
      this.radio = radio;
   }
   
   // getter/setter

   public double area() {
      return Math.PI * Math.pow(radio, 2);
   }
}

public class Rectangulo implements Forma {
   private double alto;
   private double ancho;

   public Rectangulo(double alto, double ancho) {
      this.alto = alto;
      this.ancho = ancho;
   }
   
   // getter/setter

   public double area() {
      return alto * ancho;
   }
}

public class calcularArea {
   public double calcular(Forma forma) {
      return forma.area();
   }
}

3. Principio de sustitución de Liskov (LSP)

Los objetos de una clase derivada deberían poder ser sustituidos por objetos de su clase base sin interrumpir la aplicación.

INCORRECTO:

public class Pajaro {
   public void volar() {
      System.out.println("Voy volando");
   }
}

public class Avestruz extends Pajaro {
   public void fly() {
      throw new UnsupportOperationException("Avestruz no puede volar");
   }
}

public class ObservadorAves {
   public void observarAves(Pajaro pajaro) {
      pajaro.fly();
   }
}

En este ejemplo, la clase «Avestruz» extiende la clase «Pajaro», pero en el método «volar» lanza una excepción. Por lo tanto, cuando se le quiera pasar un objeto «Avestruz» a la función «ObservadorAves», se lanzará dicha excepción en lugar de ver un ave volar. Esto viola el tercer principio SOLID ya que la subclase «Avestruz» no es compatible con el comportamiento de la clase base «volar».

Para corregir esta situación, podemos definir una interfaz «Volador» que pueda describir el comportamiento esperado de las aves. Tanto la clase «Pajaro» como la clase «Avestruz» pueden implementar dicha interfaz, proporcionando cada una implementación adecuada del método «volar».

CORRECTO:

public interface Volador {
   void volar();
}

public class Pajaro implements Volador {
   public void volar() {
      System.out.println("Voy volando");
   }
}

public class Avestruz implements Volador {
   public void volar() {
      System.out.println("Voy corriendo");
   }
}

public class ObservadorAves {
   public void observarAves(Volador pajaro) {
      pajaro.fly();
   }
}

4. Principio de segregación  de interfaces (ISP)

Los clientes no deberán verse obligados a depender de interfaces que no utilizan.

Siguiendo el ejemplo con animales, podemos exponer el siguiente ejemplo.

INCORRECTO:

public interface Animal {
   void andar();
   void nadar();
   void volar();
}

public class Perro implements Animal {
   public void andar() {
      System.out.println("Estoy andando");
   }

   public void nadar() {
      System.out.println("Estoy nadando");
   }

   public void volar() {
      throw new UnsupportOperationException("Los perros no pueden volar");
   }
}

En este ejemplo, se define la interfaz «Animal» que contiene tres métodos con tres acciones diferentes. La clase «Perro» implementa esta interfaz y proporciona implementaciones para los tres métodos, pero los perros no pueden volar. Si no se proporciona una implementación vacía, se lanza una excepción cuando se llama al método. Esto viola el principio ISP ya que la clase «Perro» no debería estar obligada a introducir una implementación que no debería tener.

CORRECTO:

public interface Animal {
   void andar();
   void nadar();
}

public interface AnimalVolador {
   void volar();
}

public class Perro implements Animal {
   public void andar() {
      System.out.println("Estoy andando");
   }

   public void nadar() {
      System.out.println("Estoy nadando");
   }
}

public class Pajaro implements Animal, AnimalVolador {
   public void andar() {
      System.out.println("Estoy andando");
   }

   public void nadar() {
      System.out.println("Estoy nadando");
   }

   public void volar() {
      System.out.println("Estoy volando");
   }
}

Para subsanar el ejemplo incorrecto anterior, tenemos una interfaz «Animal», que la implementará solo la clase «Perro», y si alguna clase (en este caso la clase «Pajaro») necesita la acción «volar», ésta implementará además de la interfaz «Animal», la interfaz «AnimalVolador». De este modo, cada clase solo implementa las acciones que son relevantes para ellas, sin obligación de implementar acciones innecesarias.

5. Principio de inversión de dependencia (DIP)

Las clases de nivel superior no deberían depender de las clases de nivel inferior. En su lugar, ambas deben depender de abstracciones.

INCORRECTO:

public class Coche {
   private MotorGasolina motor;

   public Coche() {
      motor = new MotorGasolina();
   }

   public void arrancar() {
      motor.arrancar();
   }
}

public class MotorGasolina {
   public void arrancar() {
      // código para arranca un motor de gasolina
   }
}

En este ejemplo, la clase «Coche» depende directamente de la implementación de «MotorGasolina», violando el principio DIP. Si quisiéramos cambiar el tipo de motor, por uno eléctrico, tendríamos que modificar la clase «Coche», lo que podría provocar efectos secundarios en el resto del sistema.

CORRECTO:

public interface Motor {
   void arrancar();
}

public class Coche {
   private Motor motor;

   public Coche(Motor motor) {
      this.motor = motor;
   }

   public void arrancar() {
      motor.arrancar();
   }
}

public class MotorGasolina implements Motor {
   public void arrancar() {
      // código para arranca un motor de gasolina
   }
}

public class MotorElectrico implements Motor {
   public void arrancar() {
      // código para arranca un motor eléctrico
   }
}

Organizando el código de esta manera, la interfaz «Motor» define la operación de «arrancar», y la clase «Coche» depende de dicha interfaz, en lugar de aplicar una implementación concreta. De esta forma, podemos inyectar diferentes implementaciones de «Motor» en la clase «Coche», sin tener que modificar dicha clase, cumpliendo con el principio DIP.

Una comunicación efectiva

Como hemos podido observar en todos los ejemplos anteriores, los principios SOLID son una gran herramienta para mejorar la calidad del código. «Escribir» menos no siempre significa que el código sea mejor. Con una buena estructuración de los componentes, podemos crear un código de calidad, inteligible, sencillo de comprender y fácil de  extender aunque no seas el creador del código.

Espero que sirva de ayuda para entendernos todos un poco mejor en nuestro «idioma» diario.