Volver

Principios SOLID

Introducción

Los principio SOLID son convenciones en cuanto a diseño de software que ayudan a conseguir un código más mantenible, tolerante a cambios, y testeable.

Todos los desarrolladores de un equipo deberían tener nociones de diseño de software para fomentar la autonomía y agilidad del equipo

Huir de STUPID, el enemigo de SOLID

S → Singleton: Hay un objeto que lo contiene todo. No necesita inyección de dependencias. Y se encuentra por todo el programa. Tiene demasiadas resposabilidades.

T → Tight Coupling: Fuertemente acoplado. Conoces la implementación concreta del repositorio de usuario (un mysql por ejemplo), algo que dificulta el cambio de tipo de base de datos. El código no es tolerante a cambios.

U → Untestability: Código intesteable. Muy visto en los singleton. Código muy junto sin ningún tipo de inyección de dependencias que nos lleva a tener un código imposible de testear.

P → Premature Optimization: Realizar mucho más código del necesario atendiendo al futuro. Se debe pensar con vistas a futuro pero a raíz de las posibles necesidades, no de un simple “por si acaso”.

I → Indescriptive Naming: Naming confuso que no refleja intencionalidad o significado alguno.

D → Duplication: Duplicación del mismo código en muchos lados que necesita de una abstracción o extracción a métodos o clases que solo tienen una responsabilidad y pueden ser parte de otra clase.

UML

Connotaciones negativas

Un modelo de diagramas que tiene una metodologia de trabajo en cascada:

Especificación de requisitos → Desarrollo → Testing

Una forma de trabajo muy lineal que no entiende de cambios durante el ciclo de desarrollo (especificación de requisitos), derivada de como se desarrollaba software hace tiempo.

Ventajas

Lenguaje de diagramas ilustrativo para nuestros diseños de software (clases e interacción entre ellas) Riguroso: Permite especificar hasta un nivel de detalle suficiente para identificar acoplamiento entre clases y sus relaciones sin ser verboso Agnóstico del lenguaje: No entra en detalles de implementación si quiera al nivel de qué lenguaje de programación se está usando

¿Qué tipos hay?

  • Casos de uso: se busca definir todos los posibles casos de uso (acciones) que pueda hacer un usuario

  • Secuencia: Se trata de un diagrama que con el que podremos ver el flujo de nuestra aplicación, representando cómo interaccionan las clases (comunicación entre objetos)

  • Clases: Este tipo de diagramas son muy populares y nos permiten ver no solo los atributos y métodos de cada clase, sino también las diferentes relaciones de herencia, interfaces e implementaciones de estas. ¿Ventajas?:
    → Diagrama de clases con 4 garabatos para ponernos de acuerdo u obtener feedback de nuestro equipo antes de implementarlo de forma rápida
    → Documentar implementaciones ya existentes para facilitar la revisión de código. Por ejemplo, a la hora de hacer una nueva Pull Request (PR), generar el diagrama desde IntelliJ/PhpStorm con 2 clics para adjuntar una imagen a la descripción de la PR.

UML con IntelliJ

Con esta herramienta podemos hacer diagramas de clases.

Seleccionamos las que queremos → Click derecho → Diagrams → Show Diagram

Nos encontraremos unas opciones tal que así:

  1. Campos
  2. Constructor
  3. Métodos
  4. Propiedades
  5. Inner clases

También podemos mediante clic derecho sobre una clase abstracta añadir sus implementaciones al esquema:

Otro truco es añadir las clases usando Espacio


Un resumen rápido de lo que podemos hacer sobre diagramas en IntelliJ:

S (Single responsability principle - SRP)

¿Que es?

Una clase = un concepto = una responsabilidad

O lo que es lo mismo una sola razón para cambiar

¿Como?

Clases que funcionen como servicios con pequeños objetivos acotados, entendiéndose un servicio como un orquestador que conecta nuestros modelos con infraestructura (servicios externos).

¿Por qué?

Buscamos la alta cohesión entre la conexión entre componentes de nuestro sistema, robustez antes los cambios y evitamos la duplicidad de código, ya que conseguiremos piezas más reutilizables.

¿Qué tener en cuenta?

  • Los nombres → Un nombre muy general como “OrderProcessor” da lugar a querer reutilizarlo para muchas cosas y acaba teniendo demasiadas funcionalidades, en su lugar busca un nombre más concreto.

Cuando respetamos el principio de responsabilidad única, es más fácil introducir modularidad. Entiéndase modularidad como la propiedad que permite subdividir una aplicación en partes más pequeñas (llamadas módulos), cada una de las cuales debe ser tan independiente como sea posible de la aplicación en sí y de las restantes partes.

Otros ejemplos

Todo empieza en el controller

Con una aplicación basada en una API todo empieza con una petición a un endpoint. Es por ello que tenemos que empezar a cuidar los detalles desde ahí. Un controlador no necesita entender de contruir sentencias SQL, ni mucho menos de interactuar directamente con la base de datos. Un servicio es el encargado de realizar esto de ejecutar lógica de negocio usando infraestructura. Por ello, haz que tu controlador solo reciba llamadas y la redirecione a un servicio dedicado a la causa.

Cuando un servicio y cuando no

Cuando la lógica de negocio no tiene dependencias externas puede ir acoplada al modelo de dominio. Cuando ya existen esas dependencias es mejor tener un servicio externo que se encargue de inyectar por constructor las dependencias que necesite. Esto favorece las testabilidad y la cohesión.

O (Open-Closed Principle OCP)

El software debería estar abierto a extensión y cerrado a modificación

Nos acoplamos a la interfaz, no a la implementación específica, por lo que podremos cambiar en cualquier momento a que objeto “medible” nos referimos.

Lo mismo podríamos hacer con una clase abstracta que desde un principio defina como se calcula el porcentaje, y sus implementaciones extiendan de alguna manera dicho cálculo.

Una clase abstracta es útil cuando las implementaciones van a tener una parte común que siempre se repite, como un sistema de bonificaciones que tienen una general y otras específicas. Si esto no ocurre usaremos interfaces, que permiten desacoplar entre capas, el detalle aparecerá en las implementaciones.

L (Liskov Substitution Principle - LSP)

Cualquier clase hija de otra, debería poder reemplazada sin alterar el sistema por otra clase hija de la misma clase

Ejemplo sencillo

En este ejemplo el cuadrado extiende del rectángulo, porque a groso modo son lo mismo, pero a la hora de la verdad tienen comportamientos diferentes, por lo que cuando vamos a utilizar los métodos de la clase rectángulo en la clase cuadrado, tenemos que hacer unos apaños que aunque funcionen y el código actúe correctamente, no estamos respetando el LSP, ya que la clase hija (cuadrado) necesita una serie de modificaciones en cuanto a comportamiento para poder extender y así no será posible reemplazar fácilmente un cuadrado por otra figura geométrica que extienda del rectángulo.

class Rectangle {

    private Integer length;      
    private Integer width;

    Rectangle(Integer length, Integer width) {  
        this.length = length;
        this.width = width;
    }

    void setLength(Integer length) {
        this.length = length;
    }

    void setWidth(Integer width) {
        this.width = width;
    }

    Integer getArea() {
        return this.length * this.width;
    }
}

final class Square extends Rectangle {
    Square(Integer lengthAndWidth) {
        super(lengthAndWidth, lengthAndWidth);
    }

    @Override
    public void setLength(Integer length) {
      super.setLength(length);
      super.setWidth(length);
    }
    @Override
    public void setWidth(Integer width) {
      super.setLength(width);
      super.setWidth(width);
    }
}

final class SquareShould {
    @Test
    void not_respect_the_liskov_substitution_principle_breaking_the_rectangle_laws_while_modifying_its_length() {
        Integer squareLengthAndWidth = 2;
        Square square = new Square(squareLengthAndWidth);

        Integer newSquareLength = 4;
        square.setLength(newSquareLength);

        Integer expectedAreaTakingIntoAccountRectangleLaws = 8;

        assertNotEquals(expectedAreaTakingIntoAccountRectangleLaws, square.getArea());
	  }
}

Que las subclases respeten el contrato definido en la clase padre es justamente lo que nos permite cumplir con este principio para mantener una correctitud funcional

I (Interface Segregation Principle - ISP)

Ningún cliente debería verse forzado a depender de métodos que no usa

Las interfaces se debe de desarrollar acorde a las necesidades del cliente que las usa, y no de sus implementaciones.

Header Interfaces → Una interfaz que ha sido extraida o basada de una clase (por lo que ahora la interfaz es padre de la clase), y que se crea con todos los métodos que dicha clase tenía o necesitaba.

Role Interface → Una interfaz que ha sido creada a partir de la definición previa de un caso de uso del cliente.

Conseguiremos un código con bajo acoplamiento estructural

D (Dependency inversion principle - DIP)

Módulos de alto nivel no deberían depender de los de bajo nivel. Ambos deberían depender de abstracciones.

Módulo → Clases

Ej.: Un caso de uso no debe depender de una implementación, sino que debería hacerlo de una abstracción como sería la interfaz.

Este principio busca mucho la inyección de dependencias, que sería el acto de recibir parámetros en constructor.

La finalidad es la substitución de implementaciones y mejorar la testabilidad de las clases.

Creado con Hugo
Tema Stack diseñado por Jimmy