Uso exótico del Switch para refactorizar código

14 Oct 2013

Introduccion

Cuando trabajamos en Javascript con estructuras de control que requieren de cierta envergadura, podemos terminar con trozos de código muy enredados que presentan una complejidad ciclomática muy alta. Esto da como resultado que esas partes sean difíciles de leer y de mantener tanto por nosotros mismos pasado un tiempo, como por otros desarrolladores de nuestro equipo.

Por lo general, cuando tenemos una función o un método con una gran cantidad de ‘IFs’, es señal de que tenemos que refactorizar, dividir responsabilidades y crear nuevos métodos más pequeños que se encarguen de evaluar esas expresiones de forma independiente. Sin embargo, cuando trabajamos sobre una estructura muy rígida de ficheros, esto no siempre es fácil. Es ahí cuando conocer cómo funciona la estructura SWITCH puede ser interesante para resolver un problema.

Escenario

Tomemos por ejemplo este objeto foo, que se autoinicia haciendo público tres métodos: test, test2 y notest.

var foo = ( function () {
  return {
    test : function () {
        return true;
    },
    test2 : function () {
        return true;
    },
    notest : function () {
        return false;
    }
  }  
} ) ();

En nuestros programas, cada método por lo habitual devuelve algo: tiene una lógica y un comportamiento. Aquí, para simplificar, hemos reducido esa lógica a TRUE (tanto para test como para test2) y FALSE (para notest).

Pasemos ahora a una parte del código donde necesitamos evaluar lo que devuelven esos métodos para continuar. Podríamos tener algo similar a:

NOTA: Para este ejemplo, voy a utilizar una estructura de tipo Backbone ya que podría ser éste un escenario realista de estructura rígida que hace difícil el refactorizado de métodos.

define( [
  'jquery',
  'underscore',
  'backbone',
], function ( $, _, Backbone ) {
 
  /**
   * Router
   * Main router for the app.
   * @extends Backbone.Router
   */
  var AppRouter = Backbone.Router.extend(    
    /** Creating Router */
    {
 
      requestFoo: function () {
        // Evaluating foo
        if ( foo ) {
          if ( foo.test() ) {
            // Do something is foo.test is true
          } else if ( foo.test2() ) {
            // Do something is foo.test2 is true
          } else {
            // Do something is foo.test and foo.test2 is false
          }
        }
 
      }
 
      // ...
    }
  );
}

Este ejemplo no es inventado. Es real; se trata de un código que podemos encontrar en las tripas de una famosa aplicación en producción.

Si analizamos el fragmento desde el punto de vista de su complejidad, vemos que tenemos una complejidad ciclomática muy alta: la función anónima principal de requestFoo, el IF principal que determina si foo existe y luego los condicionales que evalúan cada método: test, test2 y un else. En total cinco condicionales y una estructura un tanto compleja de seguir.

Refactorizando que es gerundio

A priori, hay dos formas sencillas de reescribir lo anterior que dan buen resultado: utilizar SWITCH o descomponer los condicionales en un objeto nuevo.

Dado que en este artículo únicamente quiero tratar la primera, pasemos directamente a verla:

requestFoo: function () {
  // Evaluating foo
 
  // Quick exit if foo is falsy
  if ( !foo ) { return false; }
 
  switch ( true ) {  
    case foo.test():
      // Do something when foo.test is true
      break;
    case foo.test2():
      // Do something when foo.test2 is true
      break;
    default:
      // Do something when all above is false
      break;
  }
 
}

Veamos esto en detalle y saquemos sus ventajas.

Una cosa que siempre me gusta hacer cuando en un método interviene una variable que hay que manipular, es forzar rápidamente una salida en caso de que no exista, o que traiga un valor de tipo ‘falsy’:

if ( !foo ) { return false; }

Con esto, aprovecho la evaluación por cortocircuito de Javascript para interrumpir lo que quede por evaluar del método ganando algún milisegundo y ahorrando memoria: si foo no existe, devolvemos el control a quien llamó a este método de manera inmediata. Además, con esta técnica evitamos posibles errores de tipo foo is undefined en el caso de que lleguemos aquí sin foo, pasando la responsabilidad de algún posible error a quién llama a este método que hemos llamado requestFoo.

A continuación, he cambiado la batería de IFs por un SWITCH tuneado. Dado que esta estructura no permite evaluar código, el valor de referencia que le establezco es ‘true‘, que actúa aquí como constante. Esto quiere decir que cada ‘case‘ deberá comprobar si el valor o expresión que se le indica es igual a dicho valor (true). Por lo tanto, si foo.test() es true, se ejecutará el correspondente código.

Personalmente, este uso de switch en lugar de los IFs lo encuentro más legible y ordenado, además de que me reduce el nivel de indentación. Algunas pruebas en cuanto a rendimiento en JSPerf parecen dar una ligera ventaja a switch sobre if, pero es tan insignificante que podemos concluir con que son idénticas. Podéis comprobarlo aquí:

http://jsperf.com/switch-vs-if-over-true-condition

NOTA: Me ha llamado mucho la atención la enorme diferencia que hay en cuanto al número de operaciones que realiza IE en comparación con Chrome o Firefox. Al menos, en mi entorno de pruebas, mientras que Firefox 24 me da unas 8.500 operaciones por segundo, Explorer 10 realiza más de 1.300.000 Eso es mucha diferencia en favor del navegador de Microsoft….

Conclusión

Refactorizar es siempre una tarea compleja pero muy gratificante. Cogemos un código que a priori se ve complejo y difícil de mantener y lo reescribimos de tal forma que conservamos la funcionalidad y comportamiento pero haciéndolo más legible y escalable.

Si bien la mejor receta frente a un trozo de código (función o método) demasiado grande es fragmentarla en pequeñas funciones que busquen ese principio de responsabilidad única, no siempre es posible hacer esto de una manera sencilla: puede que nuestro entorno de trabajo presente una estructura muy rígida, como es frecuentemente el escenario que nos pintan algunos frameworks de tipo MVxx en Javascript (Backbone, Angular,…), o que estemos trabajando dentro de un proyecto donde los ficheros se tocan por muchas manos al mismo tiempo y donde ponerse a refactorizar puede suponer un problema cara al sistema de control de versiones. Para esos casos ‘forzados’, siempre puede venir bien conocer alguna forma exótica de utilizar una estructura tradicional que puede dar claridad a un fragmento en un momento dado.

No obstante, todo lo anterior es cuestión de preferencias personales y puede que varios de vosotros, se encuentre más cómodo usando cadenas de IFs… Como siempre digo, lo genial de este lenguaje es que un mismo objetivo se puede alcanzar desde muchas perspectivas diferentes. Lo interesante es conocer el mayor número de ellas para poder escoger la que mejor nos convenga.

Más:

{7} Comentarios.

  1. josejuan

    Puede quedar bien en muchos casos, pero en otros, sobre todo en aquellos en que precisamente los if anidados tienen sentido juntos, podría quedar incluso menos legible.

    Por ejemplo, los if anidados suelen surgir al necesitar generar un valor en base a otros intermedios (que por cuestión de rendimiento o imposibilidad no deben calcularse hasta que sea preciso).

    En tal caso (no se como se verá el código…) se vería algo como:

    var r, a = A(r);
    if( cond(a) ) {
        var b = B(r, a);
        r = cond(b) ? R1(r, a, b) : R2(r, a);
    } else {
        var c = C(r);
        r = cond(c) ? R3(r, c) : R4(r);
    }
    

    Por otro lado, aunque no es una práctica «clean code», a mi me gusta resolver los «if / switch» como el de tu ejemplo haciendo «return», como:

    function takeValue(x...) {
      if(cond1) return result1;
      if(cond2) {
         ...
         return result2;
      }
      if(cond3) return result3;
      return defaultResult;
    }
    

    Viene a ser tu «switch» pero de otra forma, cuestión de gustos supongo 😀

    Por último, ¿no has metido el !foo en el switch por algún motivo?.

    • Carlos Benítez

      Si, para casos donde hay muchos niveles de anidación, habría que replantear alguna solución más limpia.

      El caso de los multiples ‘returns’ lo utilizo también con frecuencia dejando al igual que en tu ejemplo, un último ‘return’ para el comportamiento ‘default’ en caso de que no se cumpla nada de lo anterior. Sin embargo, si veo que meto más de tres, ya comienzo a verlo enredado y considero otras soluciones.

      En cuanto al !foo, no lo puedo incluir en el switch porque daría error si no está definido:

      console.log( !noVar ); // Error
      console.log( !!noVar ); // Error
      console.log( typeof noVar ); // "undefined"
      

      La forma de conseguir la evaluación sin que arroje error es utilizando el typeof y queda algo bizarra:

      switch ( true ) {
        case ( typeof foo === "undefined" ):
          // Foo is undefined. Do something...
          break; 
      }
      

      Ahí estaríamos sacando el tipo, evaluándolo y comprobando si realmente existe o no… me resulta feo y por eso prefiero sacarlo como una condición previa fuera incluso del switch.

      Saludos!

  2. acido69

    Personalmente veo poco legible este método y si lo hacemos por el aumento de rendimiento creo que es totalmente irrisorio el aumento por rendimiento.

    Me explico por partes:

    *return*
    Personalmente creo que después de un return no debería de haber nada, encontrarte un return en las primeras líneas de una función no es estético; esto imagino que es algo muy personal. En este punto si que puede haber mucha diferencia de rendimiento dependiendo de la cantidad de operaciones innecesarias que se vayan a hacer en esa función. Pero creo que hay muchas maneras de evitar esas evaluaciones innecesarias.

    *switch*
    Si nuestra función es sólo un wrapper de otras funciones, es que algo no va bien o no hemos sabido plantear el programa. Aquí la verdad es que ya depende mucho de la estructura del programa y cada caso sería muy particular.

    Personalmente creo que el aumento de rendimiento no compensa la legibilidad de este uso. Ahora bien, puede que no me esté imaginando bien los escenarios de uso.

    Un saludo

    • Carlos Benítez

      Si; cuestión de gustos, pero no estoy de acuerdo con tu argumento del ‘return’. Personalmente no creo que tenga ser el final de una función y no dejar nada por debajo.
      De hecho, es una práctica frecuente su uso en estructuras de control para reducir precisamente complejidad:

      if ( foo === true ) {
        return true;
      }
      
      return false;
      

      Eso nos elimina un ‘else’ innecesario para el caso en que foo no se cumpla la primera igualdad. Es cierto también que para el caso anterior, donde solo hay una instrucción, un ternario quedaría más bonito:

      return ( foo === true ) ? true : false;
      

      Además de que, para evaluar a ‘true’, nos sobra el valor de la constante y el paréntesis:

      return foo ? true : false;
      

      Pero bueno, comentario interesante y, como siempre, es cuestión de gustos!!
      Saludos!

  3. josejuan

    «pero no estoy de acuerdo con tu argumento del ‘return’»

    Por lo visto además de la prosa tenemos también gustos «coding» similares 😀 😀

    Los ejemplos que acabas de poner son muy de mi gusto, pero @acido69 tiene «razón» en que no son estilos «limpios» según los cánones del «clean code».

    La programación estructurada exige que sólo haya return al final de la función. Luego ya el «clean code» diría que no es recomendable poner la expresión en el return, que mejor asignar primero a una variable (no sólo porque el nombre de la variable mejoraría la lectura, también para poder depurar cómodamente, etc…).

    Pero que carajo, a mi esos códigos me parecen feos y aburridos XD XD

  4. juan

    Hola buenas, llevo tiempo siguiendo tu blog, aunque comento mas bien poco, y observando el ultimo ejemplo hay algo que no acabo de entender con el switch que has indicado:

    switch ( true ) {
    case foo.test():
    // Do something when foo.test is true
    case foo.test2():
    // AND do something is foo.test2 is true
    default:
    // Do something when all above is false
    break;
    }
    Si el primer foo.test() es true, segun el switch, entrara en el resto de cases hasta que no encuentre un break, por lo tanto entrara en foo.test2() independientemente de que este sea true o sea false y tmb en el default.
    Si el primer foo.test() es false, si evaluara foo.test2() y solo entrara siempre y cuando este sea true, y en caso de que sea true tmb entrara en el default.
    Y en el default entrara siempre.
    Asi pues, si eliminas el break, no haces que se evalue la siguiente condicion, sino que das por validas el resto mientras no encuentres un break, incluso el default.

    O eso, o me estoy perdiendo algo xD

    Este seria un ejemplo
    switch ( 4 ) {
    case 4:
    console.log(‘igual a 4’);
    case 2:
    console.log(‘igual a 2 o 4’);
    break;
    default:
    console.log(‘diferente de 2 o 4’);
    break;
    }

    • Carlos Benítez

      Hola Juan; tienes toda la razón, he eliminado esa parte del artículo porque efectivamente no se comporta como había descrito.

      Gracias por tu comentario y reporte.

      Saludos!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *