Estructuras Switch malignas

Estructuras Switch malignas

Un análisis más profundo a Clean Code

·

3 min read

Analizando las recomendaciones del libro de Robert C. Martin, Clean Code, encontré que para el autor las sentencias switch, no son de su preferencia. Nunca antes había leído sobre esto, incluso ahora que Java "mejoró" las sentencias switch con funciones flecha.

En otras secciones del libro, se nos recomienda que las funciones o métodos sean pequeños, lo suficientemente reducidos para entenderlos con una mirada rápida. El inconveniente que encontramos con los switch, es que no son pequeños, es raro que tengan sólo dos opciones, por lo general tienen muchas más, la sentencia nos permite hacer N cosas, lo que nos dificulta cumplir con los puntos dd tener métodos pequeños y métodos que hagan sólo una cosa y nada más.

Recordemos que todos estas buenas prácticas están orientadas a que un humano pueda leer nuestro código y entenderlo. Hacer que las máquinas entiendan nuestro código es sencillo, hacer que lo entienda un humano es algo más complicado.

Vamos a poner de ejemplo un caso de cálculo de descuentos según el medio de pago que selecciona el cliente:

switch (payment.type) {
  case CASH:
    return calculateDiscountsCash(payment);
    break;
  case DEBIT :
    return calculateDiscountsDebit(payment);
    break;
  case CREDIT : 
    return calculateDiscountsCredit(payment);
    break;
}

Si agregamos un nuevo método de pago con sus respectivos descuentos asociados, tenemos que modificar la sentencia switch y agregar el nuevo medio de pago, así:

switch (payment.type) {
  case CASH:
    return calculateDiscountsCash(payment);
    break;
  case DEBIT :
    return calculateDiscountsDebit(payment);
    break;
  case CREDIT : 
    return calculateDiscountsCredit(payment);
    break;
  case MOBILE: 
    return calculateDiscountsMobile(payment);
    break;
}

El consejo que nos entregan es que cada vez que veamos un switch pensemos inmediatamente en polimorfismo. Excepto cuando:

  • Una sentencia switch ejecute acciones simples, donde no hay razones para modificar. Esto ocurre cuando la lógica es de bajo nivel de abstracción, es decir, el switch está en la última función y no hay más llamadas a funciones dentro de él.
  • Cuando el switch es parte de un patrón de diseño tipo factoría. Que abordaremos en el siguiente punto.

Fábrica Abstracta

Este es un patrón de diseño creacional, nos permite generar familias de objetos que se relacionan sin tener que especificar sus clases concretas.

Podemos tener dos problemáticas:

  1. Una familia de objetos que están relacionados, como: Citycar, SUV, Camioneta.
  2. Variantes de los objetos de una familia, por ejemplo: Gasolina, Diesel, Híbrido, Eléctrico.

Llevando este concepto al ejemplo de nuestro malvado switch, podemos hacer lo siguiente:

public abstract class Payment {
  public abstract Discount calculateDiscounts();
}
public interface PaymentFactory {
  public Payment makePayment(PaymentOperation p) throws UnknownPaymentType;
}
public class PaymentFactoryImpl implements PaymentFactory { 
  public Payment makePayment(PaymentOperation p) throws UnknownPaymentType {
    switch (p.type) {
      case CASH -> { return new CashPayment(p); }
      case DEBIT -> { return new DebitPayment(p); }
      case CREDIT -> { return new CreditPayment(p); }
      // y el nuevo caso:
      case MOBILE -> { return new MobilePayment(p); }
      default -> throw new UnknownPaymentType
    }
  }
}

A simple vista, parece que es lo mismo, ¡no desapareció el switch! Pero en realidad esta es una implementación correcta para la programación orientada a objetos, cumple con el principio Tell don't ask, donde la responsabilidad del cálculo de los descuentos se entrega a la implementación de la clase Payment, esta es quien nos debe informar cómo aplicar el cálculo para entregar al usuario el monto final a pagar.

Gracias por la lectura ;)