/ design patterns

Patrones de diseño y principios de SOLID | De la práctica a la teoría

Revisemos algunos problemas de la vida real a nivel de código y pensemos en los beneficios que los principios de SOLID y patrones de diseño pueden ofrecer

problema

Imaginemos que estamos trabajando con una aplicación existente de comercio electrónico, y nos piden agregar un nuevo método de pago, ahora veamos el código inicial que nuestros colegas han dejado de las iteraciones anteriores de la solución

public class PaymentService {
  public void handlePayment(final String paymentType) {
    if ("credit-card".equals(paymentType)) {
      creditCard();
    } else if ("loyalty-points".equals(paymentType)) {
      loyaltyPoints();
    } else {
      throw new IllegalArgumentException("Payment type [" + paymentType + "] is not supported");
    }
  }
  
  private void creditCard() {}
  
  private void loyaltyPoints() {}
}

Lo más lógico es seguir el mismo patron y agregar más métodos de pago, por ejemplo: cupones, paypal, etc. Sin embargo, los problemas que esta solución generaría son: una clase con muchisimas lineas = dificil de leer = dificil de mantener; pruebas unitarias se vuelven mas complejas porque el código está en un solo lugar para todos los casos

patrones de diseño

Analicemos nuestro código para encontrar algunas características sobre el porqué tendríamos que modificar nuestra implementación:

  • Por cada tipo de pago, hay que agregar un nuevo método -> vuelve difícil de mantener y probar el código
  • Si necesitamos cambiar la integracion con alguno de los metodos de pagos, hay que modificar la misma clase que se usa para todos los pagos (PaymentService) -> genera un riesgo de romper comportamiento existente

Entonces podemos decir que definitivamente tenemos un problema de acoplamiento, así que pensemos en como resolverlo!

Necesitamos crear un componente base o abstracto que indique que indique la estructura de nuestro comportamiento (ya que todos usan la misma firma de sus métodos), y esos componentes en Java son interfaces

Pero antes pensemos en la acción que tenemos que realizar (un pago), y esta acción puede resolverse de distintas formas (tarjeta de crédito, paypal, cupones, etc.), existe un patrón de diseño que precisamente se especializa para estos casos, se llama estrategia, así que usaremos ese nombre para nuestro componente abstracto:

public interface PaymentStrategy {
  void handlePayment();
}

Bien, ahora podemos tomar las mismas implementaciones que tenemos divididos en metodos y las movemos a clases distintas que implementen esta interface:

public class CCPaymentStrategy implements PaymentStrategy {
  public void handlePayment() {}
}

public class LoyaltyPointPaymentStrategy implements PaymentStrategy {
  public void handlePayment() {}
}

Ahora debemos refactorizar nuestro componente principal, donde comunmente agregaríamos las dependencias directas a cada tipo de pago, por ejemplo:

public class PaymentService {
  private PaymentStrategy ccPaymentStrategy;
  private PaymentStrategy loyaltyPointPaymentStrategy;
  // y las demas

Pero antes de hacerlo, tenemos que preguntarnos si existe alguna forma en la que podamos reducir la cantidad de veces que tocamos PaymentService y la respuesta es si, podríamos crear un componente intermedio que se encargue de proveer las instancias necesarias que utilizaremos en la aplicacion, y esto es precisamente un patrón de diseño llamado Factory

public class PaymentFactory {
  public PaymentStrategy getInstance(final String paymentType) {
  
    // XXX: podria ser debatible si usar inyeccion de dependencias
    //  en lugar de generar las instancias manualmente
    //  seria mejor para este caso, pero no es el objetivo principal ahora
    if ("credit-card".equals(paymentType)) {
      return new CCPaymentStrategy();
    } else if ("loyalty-points".equals(paymentType)) {
      return new LoyaltyPointPaymentStrategy();
    } else {
      throw new IllegalArgumentException("Payment type [" + paymentType + "] is not supported");
    }
  }
}

public class PaymentService {
  private final paymentFactory;

  // inyeccion de dependencias ;D
  public PaymentService(final PaymentFactory paymentFactory) {
    this.paymentFactory = paymentFactory;
  }

  public void handlePayment(final String paymentType) {
    final var paymentStrategy = paymentFactory.getInstance(paymentType);
    
    paymentStrategy.handlePayment();
  }
}

SOLID

Este no es un tutorial de SOLID en Java asi que brincaremos la explicación teórica, pero la idea principal es desacoplar los componentes lo más posible, y lo que haremos ahora es mirar hacia nuestro código final y tratar de evaluar que tan alineados están nuestros cambios con la filosofía detrás de SOLID

NOTA: no todas las letras de SOLID van a ser evaluadas porque algunas simplemente no tienen una aplicación directa dentro del alcance de los cambios que aplicamos

Single Responsibility

Las clases cumplen una sola función, cada implementación de nuestra estrategia principal maneja su propio método de pago, el factory se encarga únicamente de generar instancias y el servicio ahora puede extender funcionalidad sin tener que introducir riesgos a las implementaciones de pagos existentes

Open/Closed

En este caso no podemos evaluarlo completamente en funcion de los cambios que hemos agregado, porque realmente no estamos en el escenario donde tengamos que modificar funcionalidad a un componente existente, pero si este fuera el caso, la sugerencia de este principio es extender la clase existente para agregar el cambio, contrario a agregar la modificación directamente a CCPaymentStrategy, por ejemplo:

public class VisaCCPaymentStrategy
  extends CCPaymentStrategy {
  // tus cambios
}

Con la refactorización que hemos hecho, creamos las bases para realizar este tipo de cambios sin problemas en el futuro

Dependency Inversion

Adecuado, ya que el componente de PaymentService hace uso de la dependencia de PaymentFactory quien a su vez retorna la abstraccion de PaymentStrategy

post análisis

Depués de haber refactorizado nuestro código mediante el uso de patrones de diseño, podemos mirar atrás para saber si SOLID fue una buena unidad de medida, y dependiendo de como lo veas podría variar desde no realmente, mas o menos hasta ¡SI, por supuesto! - este comentario se basa en el hecho de que no necesariamente marcamos todas las características dentro de SOLID, sin embargo, el código puede escalar fácilmente a partir de este punto, y esto abre tres discusiones grandes

not-sure-if-i-cant-understand-song-lyrics-cause-theyre-too-deep-or-theyre-too-stupid-to-make-sense

¿es necesario/posible tener un código perfecto?

Esta pregunta es más de estrategia, y la respuesta la mayoría de las veces es un rotundo depende

  • Cantidad de recursos
  • Estado actual del sistema
  • Experiencia del equipo
  • Planes de nuevos requerimientos o cambios en los existentes
  • Practicas como CI/CD para salidas a mercado

En fin, esto puede impactar directamente el tiempo que tengamos para construir nuevos módulos completamente perfectos desde el punto de vista del código

Aún así, podemos acordar que los impactos pueden ser reducidos mediante encapsulamiento y desacoplamiento de componentes (que son los principios de SOLID) mediante el uso de buenas practicas y patrones de diseno

¿SOLID o patrones de diseño?

Nuevamente, depende de algunas variables, por ejemplo: para un developer que recién comienza a desarollar, le puede resultar más sencillo aprender los patrones de diseño ya que tienen ejemplos prácticos sobre escenarios que podemos encontrar en nuestro día a día; por otro lado, un developer más experimentado podrá interiorizar los fundamentos básicos detrás de las ideas usadas en cada patrón de diseño, lo cual es ideal para un code review (por ejemplo)

¿me hace mejor developer conocer SOLID a fondo?

Si.

Screen-Shot-2020-11-30-at-1.22.43-PM

Algunas de las ventajas que trae conocer SOLID a profundidad es:

  • Romper tareas resulta más sencillo, así que la planeación de un proyecto se simplifica
  • Las revisiones de código pueden encontrar problemas con escalabilidad desde el punto de vista de diseño
  • Las pruebas unitarias se vuelven más sencillas de realizar

Aunque sinceramente, nunca he escuchado a alguien comentar algo como Aquí vendría bien utilizar el principio de sustitución de Liskov

Screen-Shot-2020-11-30-at-1.32.22-PM

conclusión

Los patrones de diseño son herramientas que podemos utilizar como base para implementar nuestras soluciones de forma escalable y mantenible

Conceptos como DRY, KISS, SOLID, entre otros expresan las ideas fundamentales de forma atómica, estas ideas son comunmente utilizadas en los patrones de diseño, por lo que es importante familiarizarse con ellos