Código limpio: el switch es código sospechoso


sospecha cuando utilices un switch


Cuando un cliente me explica una funcionalidad suele utilizar frases del tipo 'si tenemos esta situación entonces haz X, pero si es esta otra haz Y'. Uno de los reflejos inmediatos es intentar plasmar esta misma lógica en el código utilizando cláusulas switch.

En este post voy a explicar por qué debes sospechar de los switch de tu código y qué puedes hacer para minimizarlos.

IntroducciÓn

Detrás de las estructuras switch-if-else suele haber conceptos abstractos por definir. Pensar y encontrar dichos conceptos requiere un pequeño esfuerzo adicional que "por falta de tiempo" a veces no estamos dispuestos a buscar. 

Intenta buscar esos conceptos y plasmarlos en el código. Cuesta al principio, pero una vez los tienes colocados el resto va encajando como por arte de magia.

Una manera de encontrar dichos conceptos es sospechar de los switch.

Sospecha de los switch

Es difícil crear una función utilizando switch y que además sea pequeña. Por definición, switch está hecho para realizar N cosas y en consecuencia la función que lo utilice acabará siendo mayor de lo deseado y realizará más de una cosa.

Lo ideal es no utilizar switch else-if. Sería el equivalente a avanzar por un camino recto sin bifurcaciones. Fácil de seguir y fácil de recordar. Si el camino tiene bifurcaciones es necesario memorizar los cruzes y saber cuál tomar en cada momento. 

Lamentablemente en programación es inevitable tener switch else-if. Sino no sería programación ;). Lo que podemos hacer es ocultar/encapsular estas estructuras en los niveles de detalle más bajos y conseguir que no se repitan nunca.

Tarea nada fácil porque dichos bloques tienden a reproducirse con suma facilidad. Encuentra el concepto, crea una clase y busca la manera de ocultar el switch en una función o repartiéndolo en funciones de diferentes clases.

Habrá diferentes soluciones según el caso, pero como regla general empieza por intentar aplicar polimorfismo

Imagina este ejemplo:

        public Dinero CalcularPago(Empleado empleado)
        {
            switch (empleado.TipoPago)
            {
                case "PorComision":
                    return CalcularPagoDeComercial(empleado);
                case "PorHoras":
                    return CalcularPagoPorHoras(empleado);
                case "Salario":
                    return CalcularPagoAsalariado(empleado);
                default:
                      throw new ExcepcionTipoPagoEmpleadoInvalida(empleado.TipoPago);
            }
        }

A simple vista no lo parece, pero la función tiene varios problemas:

  • Aunque ahora no es muy larga, a medida que vayan apareciendo nuevos tipos de pagos la función crecerá
  • La función realiza más de una sola cosa
  • Hay más de una razón por la que podría ser modificada, por tanto viola el Single Responsability Principle (SRP)
  • Necesitará ser modificada cada vez que se añada un nuevo tipo de pago, por tanto también viola el Open Closed Principle (OCP)
  • Y el peor de todos: vas a repetir este mismo switch en diferentes funciones, y con cada nuevo switch el código será más rígido.

Por ejemplo podrías tener esta función en la que no hay más remedio que repetir la misma estructura:

        public bool EsDiaDePago(Empleado empleado, DateTime fecha)
        {
            switch (empleado.TipoPago)
            {
                case "PorComision":
                    return EsDiaDePagoComisiones(empleado, fecha);
                case "PorHoras":
                    return  EsDiaDePagoDeHoras(empleado, fecha);
                case "Salario":
                    return EsDiaDePagoDeSalario(empleado, fecha);
                default:
                    throw new ExcepcionTipoPagoEmpleadoInvalida(empleado.TipoPago);
            }
        }

O esta otra:

        public bool RecibirPago(Empleado empleado, Dinero cantidad)
        {
            switch (empleado.TipoPago)
            {
                case "PorComision":
                    return RecibirComisiones(empleado, cantidad);
                case "PorHoras":
                    return RecibirPagoHoras(empleado, cantidad);
                case "Salario":
                    return RecibirSalario(empleado, cantidad);
                default:
                    throw new ExcepcionTipoPagoEmpleadoInvalida(empleado.TipoPago);
            }
        }

Cada vez que te topes con un empleado y tengas que hacer una cosa u otra en función de su tipo tendrás que repetir la misma estructura. 

COMO TRATAR LOS SWITCH

Lo primero que hay que hacer es aislarlo. Por ejemplo, si el switch está en una función con líneas antes o después, extrae el método a una función que únicamente contenga el switch. Mueve la función a la clase en la que el método tenga sentido.

Busca el concepto que hay detrás del switch. En el ejemplo utilizado hasta ahora todo apunta a que hay más de un tipo de empleado. Aparte de los empleados asalariados hay quien trabaja por horas y hay comerciales que lo hacen a comisión. En cualquier caso, tanto los trabajadores por horas como los comerciales siguen siendo empleados:

    public class Empleado
    {
        public virtual Dinero CalcularPago()
        {
            /*...*/
        }

        public virtual bool EsDiaPago() 
        { 
            /*...*/ 
        }

        public virtual void RecibirPago()
        {
            /*...*/
        }

    }

    public class Comercial : Empleado
    {
        public override Dinero CalcularPago()
        {
            /*...*/
        }

        public override bool EsDiaPago() 
        { 
            /*...*/ 
        }

        public override void RecibirPago()
        {
            /*...*/
        }
    }

    public class EmpleadoPorHoras : Empleado
    {
        public override Dinero CalcularPago()
        {  
            /*...*/
        }

        public override bool EsDiaPago() 
        { 
            /*...*/ 
        }

        public override void RecibirPago()
        {
            /*...*/
        }
    }

Creando las clases derivadas ya puedes sustituir los fragmentos en que aparecían los switch por este otro:

            var empleado = Empleado.Crear("PorComision");
            var pago = empleado.CalcularPago();
           
            if (empleado.EsDiaDePago())
                empleado.RecibirPago(pago);

La clave de estas líneas es encapsular el switch en un método de factoría (Factory method) de la clase base Empleado cuya única misión sea crear el tipo de empleado adecuado:

        public class Empleado
        {
            public static Empleado Crear(string tipo)
            {
                switch (tipo)
                {
                    case "PorComision":
                        return new Comercial();
                    case "PorHoras":
                        return new EmpleadoPorHoras();
                    case "Salario":
                        return new Empleado();
                    default:
                        throw new ExcepcionTipoPagoEmpleadoInvalida(tipo);
                }
            }

/*...*/

Sigue teniendo alguno de los problemas antes mencionados, pero los posibles efectos dañinos han quedado reducidos al máximo.

  • Será una función que crezca en función de los nuevos tipos de pagos que aparezcan. Este punto no mejora :(
  • La función sólo realiza un cosa, crear Empleado según su tipo. Punto mejorado :)
  • Sólo hay una razón por la que podría ser modificada, por tanto cumple el Single Responsability Principle (SRP). Punto mejorado :)
  • Necesitará ser modificada cada vez que se añada un nuevo tipo de pago, por tanto también viola el Open Closed Principle (OCP). Volvemos a fallar :(
  • El punto más importante: no repetiremos esta misma lógica en ninguna función más del código. Punto mejorado :)

Evidentemente cada código base y cada situación es diferente, por eso no debemos obsesionarnos si no encontramos una solución. A veces, no queda otro remedio que saltarnos algunas recomendaciones. 

Conclusiones

  • Cuando veas un switch en el código, sospecha
  • Intenta buscar los conceptos que esconde y plásmalos en el código
  • Lleva los switch al nivel de detalles más bajo y aíslalo lo máximo que puedas
  • El método más habitual para tratar los switch es usar el polimorfismo

Fuentes

Libros




Quizá algun día empiece a enviar una newsletter, si te gustaría recibirla subscríbete aquí

Archivo