Posts
Examinando o Caso das Declarações Switch
6 de dezembro de 2015 • 10 min de leitura

Por quase 50 anos, a declaração switch (também conhecida como declaração case) tem sido uma parte integral da programação. Nos últimos anos, no entanto, alguns estão alegando que a declaração switch já não é mais útil. Outros vão ainda mais longe rotulando a declaração switch como um code-smell.
Em 1952, Stephen Kleene concebeu a declaração switch em seu artigo, Introduction to Metamathematics. A primeira implementação notável foi em ALGOL 58 em 1958. Mais tarde, a declaração switch foi incluída na indelével linguagem de programação C, que, como sabemos, influenciou a maioria das linguagens de programação modernas.
Avançando para os dias atuais, virtualmente toda linguagem tem uma declaração switch. No entanto, algumas linguagens omitiram a declaração switch. A mais notável sendo Smalltalk.
Isso despertou minha curiosidade, por que a declaração switch foi excluída do Smalltalk?
Andy Bower, um dos criadores/proponentes por trás do Dolphin Smalltalk compartilhou seus pensamentos sobre por que Smalltalk excluiu a declaração switch:
Quando vim pela primeira vez para Smalltalk vindo de C++, não conseguia entender como uma linguagem supostamente completa não suportava uma construção switch/case. Afinal, quando me mudei pela primeira vez para “programação estruturada” vindo do BASIC, pensei que switch era uma das melhores coisas desde o pão fatiado. No entanto, porque Smalltalk não suportava um switch, tive que procurar e entender como superar essa deficiência. A resposta correta é, claro, usar polimorfismo e fazer os próprios objetos despacharem para o pedaço correto de código. Então percebi que não era uma “deficiência” de forma alguma, mas Smalltalk estava me forçando a um design OOP muito mais refinado do que eu tinha me acostumado em C++. Se houvesse uma declaração switch disponível, teria levado muito mais tempo para aprender isso ou, pior, eu ainda poderia estar programando no estilo pseudo-objeto C++/Java em Smalltalk.
Eu argumentaria que na OOP normal não há necessidade real de uma declaração switch. Às vezes, ao fazer interface com um mundo não-OOP (como receber e despachar mensagens WM_XXXX do Windows que não são objetos, mas apenas inteiros), então uma declaração switch seria útil. Nessas situações, há alternativas (como despachar de um Dictionary) e o número de vezes que elas aparecem não justifica a inclusão de sintaxe adicional.
Andy estava certo? Estamos melhor sem a declaração switch? Outras linguagens também se beneficiariam excluindo a declaração switch?
Para esclarecer essa questão, montei uma comparação entre uma declaração switch, um dicionário e polimorfismo. Vamos chamar isso de confronto. Que vença a melhor implementação!
Cada implementação tem um método que recebe um parâmetro, um inteiro, e retorna uma string. Usaremos complexidade ciclomática e índice de manutenibilidade para examinar cada implementação. Em seguida, faremos uma visão holística de todas as três implementações.
O código.
Declaração Switch
Índice de Manutenibilidade | 72 |
---|---|
Complexidade 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;
}
}
Dicionário
Índice de Manutenibilidade | 73 |
---|---|
Complexidade 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 Manutenibilidade Total | 94 |
---|---|
Complexidade Ciclomática Total | 15 |
Interface
Índice de Manutenibilidade | 100 |
---|---|
Complexidade Ciclomática | 1 |
public interface IColor
{
string ColorName { get; }
}
Factory
Índice de Manutenibilidade | 76 |
---|---|
Complexidade 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()}
};
}
}
Implementação
Índice de Manutenibilidade | 97 |
---|---|
Complexidade 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";
}
Os Resultados
Antes de mergulhar nos resultados, vamos definir Complexidade Ciclomática e Índice de Manutenibilidade:
- Complexidade Ciclomática é a medida de ramificação lógica. Quanto menor o número, melhor.
- Índice de Manutenibilidade mede a manutenibilidade do código. É uma escala entre 0 e 100. Quanto maior o número, melhor.
Complexidade Ciclomática | Índice de Manutenibilidade | |
---|---|---|
Declaração Switch | 6 | 72 |
Dicionário | 3 | 73 |
Polimorfismo | 15 | 94 |
Examinaremos primeiro a complexidade ciclomática.
Os resultados para complexidade ciclomática são diretos. A implementação com dicionário é a mais simples. Isso significa que é a melhor solução? Não, como veremos quando avaliarmos o índice de manutenibilidade.
A maioria pensaria como eu pensei, a implementação com a menor complexidade ciclomática é a mais manutenível — como poderia ser de outra forma?
Em nosso cenário, a implementação com a menor complexidade ciclomática não é a mais manutenível. Na verdade, em nosso cenário, é o oposto. A implementação mais complexa é a mais manutenível! Mente explodida!
Se você se lembra, quanto maior a pontuação do índice de manutenibilidade, melhor. Indo direto ao ponto, polimorfismo tem a melhor pontuação do índice de manutenibilidade — mas também tem a maior complexidade ciclomática. O que está acontecendo? Isso não parece certo.
Por que a implementação mais complexa é a mais manutenível? Para responder isso, devemos entender o índice de manutenibilidade.
O índice de manutenibilidade consiste em 4 métricas: complexidade ciclomática, linhas de código, número de comentários e o volume de Halstead. As primeiras três métricas são relativamente bem conhecidas, mas a última, o Volume de Halstead, é relativamente desconhecida. Como a complexidade ciclomática, o Volume de Halstead tenta medir objetivamente a complexidade do código.
Em termos simples, o Volume de Halstead mede o número de partes móveis (variáveis, chamadas do sistema, aritmética, construções de codificação, etc.) no código. Quanto maior o número de partes móveis, maior a complexidade. Quanto menor o número de partes móveis, menor a complexidade. Isso explica por que a implementação polimórfica pontua alto no índice de manutenibilidade; as classes têm poucas ou nenhuma parte móvel. Outra forma de ver o Volume de Halstead é que ele mede a densidade de “partes móveis”.
O que é software, senão para mudar? Para refletir o mundo real, estamos introduzindo mudança. Adicionei uma nova cor a cada implementação.
Abaixo estão os resultados revisados.
Complexidade Ciclomática | Índice de Manutenibilidade | |
---|---|---|
Declaração Switch | 7 | 70 |
Dicionário | 3 | 73 |
Polimorfismo | 17 | 95 |
A declaração switch e as abordagens polimórficas aumentaram em complexidade ciclomática em uma unidade, mas interessantemente, o dicionário não aumentou. A princípio fiquei intrigado com isso, mas então percebi que o dicionário considera as cores como dados e as outras duas implementações tratam as cores como código. Vou direto ao ponto.
Voltando nossa atenção ao índice de manutenibilidade, apenas uma, a declaração switch, diminuiu em manutenibilidade. A pontuação de manutenibilidade do polimorfismo melhorou e ainda assim a complexidade também aumenta (preferiríamos que diminuísse). Como mencionei acima, isso é contra-intuitivo.
Nossa comparação mostra que dicionários podem, do ponto de vista da complexidade, escalar infinitamente. A abordagem polimórfica é de longe a mais manutenível e parece aumentar em manutenibilidade conforme mais cenários são adicionados. A declaração switch aumenta em complexidade e diminui em manutenibilidade quando o novo cenário foi adicionado. Mesmo antes de adicionarmos o novo cenário, ela tinha as piores medidas de complexidade ciclomática e índice de manutenibilidade.
Jem Finch do Google compartilhou seus pensamentos sobre as deficiências das declarações switch:
1. Implementações de métodos polimórficos são lexicamente isoladas umas das outras. Variáveis podem ser adicionadas, removidas, modificadas, e assim por diante sem qualquer risco de impactar código não relacionado em outro ramo da declaração switch.
2. Implementações de métodos polimórficos são garantidas de retornar ao lugar correto, assumindo que terminam. Declarações switch em uma linguagem de fall through como C/C++/Java requerem uma declaração “break” propensa a erros para garantir que retornem à declaração após o switch em vez do próximo bloco case.
3. A existência de uma implementação de método polimórfico pode ser imposta pelo compilador, que se recusará a compilar o programa se uma implementação de método polimórfico estiver faltando. Declarações switch não fornecem tal verificação de exaustividade.
4. Despacho de método polimórfico é extensível sem acesso a (ou recompilação de) outro código fonte. Adicionar outro case a uma declaração switch requer acesso ao código de despacho original, não apenas em um lugar mas em todo lugar onde o enum relevante está sendo switchado.
5. … você pode testar métodos polimórficos independentemente do aparato de switching. A maioria das funções que fazem switch como o exemplo que o autor deu conterá outro código que não pode então ser testado separadamente; chamadas de método virtual, por outro lado, podem ser.
6. Chamadas de método polimórfico garantem despacho em tempo constante. Nenhum compilador suficientemente inteligente é necessário para converter o que é naturalmente uma construção de tempo linear (a declaração switch com fall through) em uma construção de tempo constante.
Infelizmente, ou felizmente, dependendo do seu campo, a maioria das linguagens tem uma declaração switch, e elas não vão desaparecer tão cedo. Com isso em mente, é bom saber o que está acontecendo por baixo dos panos ao compilar declarações switch.
Há três otimizações de declaração switch que podem ocorrer:
- Declarações If-elseif – Quando uma declaração switch tem um pequeno número de cases ou cases esparsos (valores não incrementais, como 10, 250, 1000) ela é convertida para uma declaração if-elseif.
- Jump Table – Em conjuntos maiores de cases adjacentes (1, 2, 3, 4, 5) o compilador converte a declaração switch para uma jump table. Uma Jump Table é essencialmente uma Hashtable com um ponteiro (pense declaração goto) para a função na memória.
- Busca Binária – Para grandes conjuntos de cases esparsos o compilador pode implementar uma busca binária para identificar o case rapidamente, similar a como um índice funciona em um banco de dados. Em casos extraordinários onde cases são um grande número de cases esparsos e adjacentes, o compilador usará uma combinação das três otimizações.
Resumo
Em um mundo orientado a objetos, a declaração switch, concebida em 1952, é um pilar do engenheiro de software. Uma exceção notável é Smalltalk onde os designers optaram por excluir a declaração switch.
Quando comparada a implementações alternativas equivalentes, o dicionário e polimorfismo, a declaração switch não se saiu tão bem.
A declaração switch está aqui para ficar, mas como nossa comparação mostrou, há melhores alternativas à declaração switch.
As implementações estão disponíveis no Github.
Autor: Chuck Conway é especialista em engenharia de software e IA Generativa. Conecte-se com ele nas redes sociais: X (@chuckconway) ou visite-o no YouTube.