Switch-Case is Evil!

Publicado por Alexandre Cunha
23/6/2011
Categoria:
Tags: ,


Conheço muitas pessoas que discutem e defendem que switch-case é coisa do demônio, que não deve ser utilizado nem que a sua main thread dependa disso. Pois eu defendo o mesmo mas acho que com o tempo acabamos esquecendo os verdadeiros motivos, então vale discutirmos novamente quais são eles. É importante lembrar que o construct switch-case em si não é demoníaco, apenas flerta com satanás. O que eu quero dizer é que não é o construct sozinho que gera um problema, mas sim a forma como ele normalmente é utilizado. Na grande maioria dos casos o switch-case é um sintoma de que existe algo errado no seu código, o famoso code smell. O swtich-case é o tipo de código que funciona, é fácil e rápido de implementar e parece inofensivo. Acredito que a maioria dos desenvolvedores que usam um switch-case no fundo sabem como fazer a solução correta, apenas não a fazem por preguiça ou por falta de tempo. Em sistemas reais que vão receber manutenção por muitos anos, é de extrema importância que esse tipo de código seja eliminado e combatido. A única pessoa que se beneficia desse código foi quem o escreveu, todas as outras pessoas envolvidas apenas sofrem com ele. Podemos identificar basicamente os seguintes problemas decorrentes do uso do switch-case:

  1. Falta do uso adequado de um mecanismo de herança;
  2. Tomada de decisões através de um mecanismo fracamente tipado;
  3. Código difícil de ler;
  4. Pode facilmente esconder efeitos colaterais (propícios para proliferação de bugs);
  5. Código que pode quebrar facilmente durante alterações no sistema (inclusive alterações que aparentemente não estão relacionadas à ele);

O que com certeza mais me chama atenção é a falta do uso de herança e um sistema fracamente tipado, recursos importantíssimo quando estamos falando de programação OO. A repetição de múltiplos switch-cases seria o caso clássico da necessidade de um refactoring. Muitas vezes substituir um mero switch-case por herança pode parecer um overkill, mas não se enganem, essa é uma troca altamente necessária se você quer que seu código seja mantido por outros desenvolvedores. Normalmente este tipo de argumento não passa de pura preguiça, ainda mais na linguagem C# que facilita muito esse refactoring. Code please!

private void Form1_Load(object sender, EventArgs e)
{
	comboBox1.Items.AddRange(new string[] { "A", "B", "C" });
}

private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
	switch ((string)comboBox1.SelectedItem)
	{
		case "A":
			MethodA();
		break;
		case "B":
			MethodB();
		break;
		case "C":
			MethodC();
		break;
	}
}

Já perdi a conta de quantas vezes vi esse código em Produção! Falta nesse código a abstração mais básica de que dentro do combo há uma listagem de uma entidade, que tem algum significado e portanto um comportamento bem definido. O uso de strings no lugar de um tipo adequado é um atentado à todos os desenvolvedores (exceto a turma do VB). A simples adição de um novo valor (“D”) é um problema descomunal. Não existe nenhum mecanismo que permita o desenvolvedor descobrir todos os switch-cases que precisam ser alterados e todos os efeitos colaterais decorrentes. Por ser fracamente tipado não ocorrem erros (ou avisos) em tempo de compilação. Apenas em tempo de execução (muitas vezes já em Produção) o problema será detectado. Caso o valor seja trocado de “A” para “a”, esse código falha miseravelmente, sem nenhum aviso prévio. Isso sem contar que no exemplo acima o código de cada case é apenas uma chamada de um método. Normalmente quem usa switch-case gosta de colocar o código inteiro dentro de um único switch-case. Cada cláusula case fica separada por dezenas, centenas, muitas vezes milhares de linhas de código, tornando absolutamente impossível qualquer possibilidade de manutenção do código. Um código adequado é simples e fácil de escrever.

private void Form1_Load(object sender, EventArgs e)
{
	Item[] items = new Item[]
	{
		new Item() { Name = "A", Action = () => MethodA() },
		new Item() { Name = "B", Action = () => MethodB() },
		new Item() { Name = "C", Action = () => MethodC() },
	};
	comboBox1.Items.AddRange(items);
}

private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
	Item item = (Item)comboBox1.SelectedItem;
	item.Action();
}

private class Item
{
	public string Name { get; set; }
	public Action Action { get; set; }

	public override string ToString() { return Name; }
}

Agora sim. Quando um novo item “D” precisa ser adicionado significa que um novo Item precisa ser criado e na sua criação define-se o nome já atrelado ao método (claro que devemos usar um construtor para realmente garantir que em tempo de compilação nada escape). O uso do delegate Action (parte do .NET 2.0+) é fantástico nesse caso, pois evita a necessidade de ter que implementar uma classe abstrata AbstractItem e classes concretas ItemA, ItemB e ItemC. Algumas vezes essa herança é totalmente necessária e justificada, mas no exemplo acima não é crucial. O mais importante é garantir que através de um novo tipo (classe Item) seja feita a correta associação entre os item listados no combo e o comportamento que ocorre quando cada item é selecionado. Fato é que são raros os casos onde um switch-case é a melhor solução. Um dos poucos casos que considero aceitável é quando valores que vem de fora do sistema precisam ser traduzidos em objetos de dentro do sistema. Por exemplo se uma chamada de Web Service retorna um string status que precisa ser convertido em um objeto do tipo Status. Entretanto é aceitável utilizar um switch-case neste caso apenas uma vez, no ponto de entrada do sistema e dali em diante deve-se usar instâncias da classe Status para tomar deciões futuras, fazendo bom uso da herança, caso necessário. O problema é que é mais fácil não pensar em abstração, simplesmente por o switch-case e falar para sí mesmo “depois eu resolvo”, e lá vai mais um switch-case para Produção. Eu fico impressionado como a grande maioria dos problemas são facilmente resolvidos com simples abstrações e ainda mais impressionado como 90% dos desenvolvedores não se esforça o mínimo para resolver esses pequenos problemas. Se esses, que são simples de resolver, são constantemente negligenciados, o que diremos então dos problemas mais complexos de abstração quando o uso de Design Patterns e soluções muito mais difíceis de serem implementadas são altamente necessárias para manter a sanidade do sistema (o pattern MVP é um bom exemplo disso). Não devemos nunca deixar de lado um código que sabemos que está mal escrito apenas pelo argumento “depois eu resolvo” ou “é um problema localizado” ou qualquer que seja a desculpa. Escrever o código fonte de uma aplicação que vai ser utilizada durante anos e passar na mão de dezenas de desenvolvedores é uma enorme responsabilidade. Varrer a sujeira pra baixo do tapete é fácil quando você sabe que não vai ser o responsável por limpá-la. Se alguém realmente acredita que o primeiro código desse post é o código correto, que não existe problema nenhum com ele, façam-me um favor: voltem aos seus códigos VB e parem de me perturbar! Interessante também é que sem querer acabei fazendo uma solução em .NET que seria uma versão lambda para jump tables, que é um recurso conhecido por alguns programadores avançados de C e C++. Vale a pena verificar esses artigos pois eles discutem basicamente o mesmo problema só que mais aprofundado tecnicamente. Jump Tables via Function Pointer Arrays in C/C+ Be wary of switch statements

2 Comentários





Desenvolvido por hacklab/ com WordPress