Examinando el Caso de las Declaraciones Switch
6 de diciembre de 2015 • 10 min de lectura

Durante casi 50 años, la declaración switch (también conocida como declaración case) ha sido una parte integral de la programación. En años recientes, sin embargo, algunos afirman que la declaración switch ha superado su utilidad. Otros van aún más lejos etiquetando la declaración switch como un code-smell.
En 1952, Stephen Kleene concibió la declaración switch en su artículo, Introduction to Metamathematics. La primera implementación notable fue en ALGOL 58 en 1958. Posteriormente, la declaración switch fue incluida en el indeleble lenguaje de programación C, que, como sabemos, ha influenciado la mayoría de los lenguajes de programación modernos.
Avanzando rápidamente al presente, virtualmente todos los lenguajes tienen una declaración switch. Sin embargo, algunos lenguajes han omitido la declaración switch. El más notable siendo Smalltalk.
Esto despertó mi curiosidad, ¿por qué fue excluida la declaración switch de Smalltalk?
Andy Bower, uno de los creadores/proponentes detrás de Dolphin Smalltalk compartió sus pensamientos sobre por qué Smalltalk excluyó la declaración switch:
Cuando llegué por primera vez a Smalltalk desde C++, no podía entender cómo un lenguaje supuestamente completo no soportaba una construcción switch/case. Después de todo, cuando me moví por primera vez a la “programación estructurada” desde BASIC pensé que switch era una de las mejores cosas desde el pan rebanado. Sin embargo, porque Smalltalk no soportaba un switch tuve que buscar y entender cómo superar esta deficiencia. La respuesta correcta es, por supuesto, usar polimorfismo y hacer que los objetos mismos despachen al fragmento correcto de código. Entonces me di cuenta de que no era una “deficiencia” en absoluto, sino que Smalltalk me estaba forzando a un diseño OOP mucho más granular del que me había acostumbrado en C++. Si hubiera habido una declaración switch disponible me habría tomado mucho más tiempo aprender esto o, peor, podría seguir programando en estilo pseudo-objeto de C++/Java en Smalltalk.
Yo sostendría que en OOP normal no hay necesidad real de una declaración switch. A veces, cuando se interfaz con un mundo no-OOP (como recibir y despachar mensajes WM_XXXX de Windows que no son objetos sino solo enteros), entonces una declaración switch sería útil. En estas situaciones, hay alternativas (como despachar desde un Dictionary) y el número de veces que surgen no justifica la inclusión de sintaxis adicional.
¿Tenía razón Andy? ¿Estamos mejor sin la declaración switch? ¿Se beneficiarían otros lenguajes también de excluir la declaración switch?
Para arrojar algo de luz sobre esta pregunta, he preparado una comparación entre una declaración switch, un diccionario, y polimorfismo. Llamémoslo un enfrentamiento. ¡Que gane la mejor implementación!
Cada implementación tiene un método que toma un parámetro, un entero, y devuelve una cadena. Usaremos complejidad ciclomática e índice de mantenibilidad para examinar cada implementación. Luego tomaremos una vista holística de las tres implementaciones.
El código.
Declaración Switch
Índice de Mantenibilidad | 72 |
---|---|
Complejidad Ciclomática | 6 |
public class SwitchWithFourCases
{
public string SwitchStatment(int color)
{
var colorString = "Red";
switch (color)
{
case 1:
colorString = "Green";
break;
case 2:
colorString = "Blue";
break;
case 3:
colorString = "Violet";
break;
case 4:
colorString = "Orange";
break;
}
return colorString;
}
}
Diccionario
Índice de Mantenibilidad | 73 |
---|---|
Complejidad Ciclomática | 3 |
public class DictionaryWithFourItems
{
public string Dictionary(int color)
{
var colorString = "Red";
var colors = new Dictionary<int, string> {{1, "Green"}, {2, "Blue"}, {3, "Violet"}, {4, "Orange"}};
var containsKey = colors.ContainsKey(color);
if (containsKey)
{
colorString = colors[color];
}
return colorString;
}
}
Polimorfismo
Índice de Mantenibilidad Total | 94 |
---|---|
Complejidad Ciclomática Total | 15 |
Interfaz
Índice de Mantenibilidad | 100 |
---|---|
Complejidad Ciclomática | 1 |
public interface IColor
{
string ColorName { get; }
}
Factory
Índice de Mantenibilidad | 76 |
---|---|
Complejidad Ciclomática | 4 |
public class ColorFactory
{
public string GetColor(int color)
{
IColor defaultColor = new RedColor();
var colors = GetColors();
var containsKey = colors.ContainsKey(color);
if (containsKey)
{
var c = colors[color];
return c.ColorName;
}
return defaultColor.ColorName;
}
private static IDictionary<int, IColor> GetColors()
{
return new Dictionary<int, IColor>
{
{1, new GreenColor()},
{2, new BlueColor()},
{3, new VioletColor()},
{4, new OrangeColor()},
{5, new MagentaColor()}
};
}
}
Implementación
Índice de Mantenibilidad | 97 |
---|---|
Complejidad Ciclomática | 2 |
public class BlueColor : IColor
{
public string ColorName => "Blue";
}
public class RedColor : IColor
{
public string ColorName => "Red";
}
public class GreenColor : IColor
{
public string ColorName => "Green";
}
public class MagentaColor : IColor
{
public string ColorName => "Magenta";
}
public class VioletColor : IColor
{
public string ColorName => "Violet";
}
Los Resultados
Antes de profundizar en los resultados, definamos Complejidad Ciclomática e Índice de Mantenibilidad:
- Complejidad Ciclomática es la medida de ramificación lógica. Mientras menor sea el número, mejor.
- Índice de Mantenibilidad mide la mantenibilidad del código. Está en una escala entre 0 y 100. Mientras mayor sea el número, mejor.
Complejidad Ciclomática | Índice de Mantenibilidad | |
---|---|---|
Declaración Switch | 6 | 72 |
Diccionario | 3 | 73 |
Polimorfismo | 15 | 94 |
Examinaremos primero la complejidad ciclomática.
Los resultados para la complejidad ciclomática son directos. La implementación con diccionario es la más simple. ¿Significa esto que es la mejor solución? No, como veremos cuando evaluemos el índice de mantenibilidad.
La mayoría pensaría como yo lo hice, que la implementación con la menor complejidad ciclomática es la más mantenible — ¿cómo podría ser de otra manera?
En nuestro escenario, la implementación con la menor complejidad ciclomática no es la más mantenible. De hecho, en nuestro escenario, es lo opuesto. ¡La implementación más compleja es la más mantenible! ¡Mente volada!
Si recuerdas, mientras mayor sea la puntuación del índice de mantenibilidad, mejor. Yendo al grano, el polimorfismo tiene la mejor puntuación de índice de mantenibilidad — pero también tiene la mayor complejidad ciclomática. ¿Qué pasa? Eso no parece correcto.
¿Por qué la implementación más compleja es la más mantenible? Para responder esto, debemos entender el índice de mantenibilidad.
El índice de mantenibilidad consiste en 4 métricas: complejidad ciclomática, líneas de código, el número de comentarios y el volumen de Halstead. Las primeras tres métricas son relativamente bien conocidas, pero la última, el Volumen de Halstead, es relativamente desconocida. Como la complejidad ciclomática, el Volumen de Halstead intenta medir objetivamente la complejidad del código.
En términos simples, el Volumen de Halstead mide el número de partes móviles (variables, llamadas al sistema, aritmética, construcciones de codificación, etc.) en el código. Mientras mayor sea el número de partes móviles, mayor la complejidad. Mientras menor sea el número de partes móviles, menor la complejidad. Esto explica por qué la implementación polimórfica obtiene una puntuación alta en el índice de mantenibilidad; las clases tienen pocas o ninguna parte móvil. Otra forma de ver el Volumen de Halstead es que mide la densidad de “partes móviles”.
¿Qué es el software, si no es para cambiar? Para reflejar el mundo real, estamos introduciendo cambio. He agregado un nuevo color a cada implementación.
Abajo están los resultados revisados.
Complejidad Ciclomática | Índice de Mantenibilidad | |
---|---|---|
Declaración Switch | 7 | 70 |
Diccionario | 3 | 73 |
Polimorfismo | 17 | 95 |
La declaración switch y los enfoques polimórficos ambos aumentaron en complejidad ciclomática por una unidad, pero interesantemente, el diccionario no aumentó. Al principio me desconcertó esto, pero luego me di cuenta de que el diccionario considera los colores como datos y las otras dos implementaciones tratan los colores como código. Iré al grano.
Dirigiendo nuestra atención al índice de mantenibilidad, solo uno, la declaración switch, disminuyó en mantenibilidad. La puntuación de mantenibilidad del polimorfismo mejoró y sin embargo la complejidad también aumenta (preferiríamos que disminuyera). Como mencioné arriba, esto es contra-intuitivo.
Nuestra comparación muestra que los diccionarios pueden, desde un punto de vista de complejidad, escalar infinitamente. El enfoque polimórfico es por mucho el más mantenible y parece aumentar en mantenibilidad a medida que se agregan más escenarios. La declaración switch aumenta en complejidad y disminuye en mantenibilidad cuando se agregó el nuevo escenario. Incluso antes de que agregáramos el nuevo escenario, tenía las peores medidas de complejidad ciclomática e índice de mantenibilidad.
Jem Finch de Google compartió sus pensamientos sobre las deficiencias de las declaraciones switch:
1. Las implementaciones de métodos polimórficos están léxicamente aisladas unas de otras. Las variables pueden ser agregadas, removidas, modificadas, y así sucesivamente sin ningún riesgo de impactar código no relacionado en otra rama de la declaración switch.
2. Las implementaciones de métodos polimórficos están garantizadas de regresar al lugar correcto, asumiendo que terminan. Las declaraciones switch en un lenguaje de caída como C/C++/Java requieren una declaración “break” propensa a errores para asegurar que regresen a la declaración después del switch en lugar del siguiente bloque case.
3. La existencia de una implementación de método polimórfico puede ser forzada por el compilador, que se rehusará a compilar el programa si falta una implementación de método polimórfico. Las declaraciones switch no proporcionan tal verificación de exhaustividad.
4. El despacho de métodos polimórficos es extensible sin acceso a (o recompilación de) otro código fuente. Agregar otro caso a una declaración switch requiere acceso al código de despacho original, no solo en un lugar sino en cada lugar donde el enum relevante está siendo switcheado.
5. … puedes probar métodos polimórficos independientemente del aparato de switching. La mayoría de las funciones que switchean como el ejemplo que dio el autor contendrán otro código que no puede entonces ser probado por separado; las llamadas a métodos virtuales, por otro lado, pueden serlo.
6. Las llamadas a métodos polimórficos garantizan despacho en tiempo constante. No es necesario un compilador suficientemente inteligente para convertir lo que es naturalmente una construcción de tiempo lineal (la declaración switch con caída) en una construcción de tiempo constante.
Desafortunadamente, o afortunadamente, dependiendo de tu bando, la mayoría de los lenguajes tienen una declaración switch, y no van a desaparecer pronto. Con esto en mente, es bueno saber qué está pasando bajo el capó cuando se compilan declaraciones switch.
Hay tres optimizaciones de declaración switch que pueden ocurrir:
- Declaraciones If-elseif – Cuando una declaración switch tiene un pequeño número de casos o casos dispersos (valores no incrementales, como 10, 250, 1000) se convierte a una declaración if-elseif.
- Tabla de Salto – En conjuntos más grandes de casos adyacentes (1, 2, 3, 4, 5) el compilador convierte la declaración switch a una tabla de salto. Una Tabla de Salto es esencialmente un Hashtable con un puntero (piensa en declaración goto) a la función en memoria.
- Búsqueda Binaria – Para conjuntos grandes de casos dispersos el compilador puede implementar una búsqueda binaria para identificar el caso rápidamente, similar a cómo funciona un índice en una base de datos. En casos extraordinarios donde los casos son un gran número de casos dispersos y adyacentes, el compilador usará una combinación de las tres optimizaciones.
Resumen
En un mundo orientado a objetos, la declaración switch, concebida en 1952, es un pilar del ingeniero de software. Una excepción notable es Smalltalk donde los diseñadores optaron por excluir la declaración switch.
Cuando se compara con implementaciones alternativas equivalentes, el diccionario y el polimorfismo, la declaración switch no se desempeñó tan bien.
La declaración switch está aquí para quedarse, pero como nuestra comparación ha mostrado, hay mejores alternativas a la declaración switch.
Las implementaciones están disponibles en Github.
↑ Volver arribaTambién te puede gustar
- Modificar un Archivo Localmente Sin Actualizar el Repositorio Git Remoto 1 min de lectura
- Una Implementación de Búsqueda Binaria 1 min de lectura
- Los Beneficios de Usar un Framework de Construcción 2 min de lectura