Maneiras de eliminar a troca de código [fechado]


175

Quais são as maneiras de eliminar o uso da opção no código?


18
Por que você deseja eliminar os switches, quando usados ​​adequadamente? Por favor, você pode elaborar sua pergunta.
RB.

Eu voto a esta pergunta ser feita editável comunidade, porque todo mundo está dizendo a mesma coisa e que poderia ser bom para consolidar todas as opiniões;)
Josh

2
O interruptor não é tão padrão quanto as outras instruções. Por exemplo, em C ++, é provável que você esqueça a 'quebra' e, em seguida, terá resultados inesperados. Além disso, esse 'intervalo' é muito semelhante a um GOTO. Eu nunca tentei eliminar interruptores, mas com certeza eles não são minha instrução favorita. ;)

2
Eu acho que ele está se referindo ao conceito de switch e não à instrução C ++ ou Java switch. Um switch também pode ser uma cadeia de blocos 'if-else if'.
Outlaw Programmer

2
Você pode se divertir: há um grande número de programadores notáveis da comunidade ágil joinng a campanha anti-SE: antiifcampaign.com/supporters.html
Mike A

Respostas:


267

As instruções de comutação não são um antipadrão por si só, mas se você estiver codificando orientação a objeto, deve considerar se o uso de uma opção é melhor resolvido com polimorfismo, em vez de usar uma instrução de comutação.

Com o polimorfismo, isso:

foreach (var animal in zoo) {
    switch (typeof(animal)) {
        case "dog":
            echo animal.bark();
            break;

        case "cat":
            echo animal.meow();
            break;
    }
}

torna-se o seguinte:

foreach (var animal in zoo) {
    echo animal.speak();
}

2
Estou sendo agredido por causa de sugestões semelhantes em stackoverflow.com/questions/374239/… Muitas pessoas não acreditam em polimorfismo :) Exemplo muito bom.
Nazgob 17/12/08

30
-1: Eu nunca usei uma instrução switch typeof, e esta resposta não sugere maneiras ou motivos para contornar as instruções switch em outras situações.
Kevin

3
Eu concordo com o @ Kevin - o exemplo dado não mostra como eliminar a troca por polimorfismo. Os exemplos simples: obter o nome de enum alterando seus valores ou executar algum código por valores apropriados em algum tipo de algoritmo.
HotJard 13/10

Eu queria saber como eliminá-lo antes que você possa confiar em implementações específicas, ou seja, em uma fábrica. E eu achei um grande exemplo aqui stackoverflow.com/a/3434505/711855
juanmf

1
exemplo muito trivial.
GuardianX

239

Veja o cheiro das instruções do switch :

Normalmente, instruções de opção semelhantes estão espalhadas por um programa. Se você adicionar ou remover uma cláusula em um switch, também precisará localizar e reparar os outros.

A refatoração e a refatoração de padrões têm abordagens para resolver isso.

Se o seu (pseudo) código se parecer com:

class RequestHandler {

    public void handleRequest(int action) {
        switch(action) {
            case LOGIN:
                doLogin();
                break;
            case LOGOUT:
                doLogout();
                break;
            case QUERY:
               doQuery();
               break;
        }
    }
}

Este código viola o Princípio Aberto Fechado e é frágil para cada novo tipo de código de ação que aparece. Para remediar isso, você pode introduzir um objeto 'Comando':

interface Command {
    public void execute();
}

class LoginCommand implements Command {
    public void execute() {
        // do what doLogin() used to do
    }
}

class RequestHandler {
    private Map<Integer, Command> commandMap; // injected in, or obtained from a factory
    public void handleRequest(int action) {
        Command command = commandMap.get(action);
        command.execute();
    }
}

Se o seu (pseudo) código se parecer com:

class House {
    private int state;

    public void enter() {
        switch (state) {
            case INSIDE:
                throw new Exception("Cannot enter. Already inside");
            case OUTSIDE:
                 state = INSIDE;
                 ...
                 break;
         }
    }
    public void exit() {
        switch (state) {
            case INSIDE:
                state = OUTSIDE;
                ...
                break;
            case OUTSIDE:
                throw new Exception("Cannot leave. Already outside");
        }
    }

Então você pode introduzir um objeto 'State'.

// Throw exceptions unless the behavior is overriden by subclasses
abstract class HouseState {
    public HouseState enter() {
        throw new Exception("Cannot enter");
    }
    public HouseState leave() {
        throw new Exception("Cannot leave");
    }
}

class Inside extends HouseState {
    public HouseState leave() {
        return new Outside();
    }
}

class Outside extends HouseState {
    public HouseState enter() {
        return new Inside();
    }
}

class House {
    private HouseState state;
    public void enter() {
        this.state = this.state.enter();
    }
    public void leave() {
        this.state = this.state.leave();
    }
}

Espero que isto ajude.


7
Obrigado pelo ótimo exemplo de como refatorar o código. Embora eu poderia dizer no começo é um pouco difícil de ler (porque a pessoa tem que alternar entre vários arquivos para compreendê-lo completamente)
rshimoda

8
Os argumentos contra a troca são válidos desde que você perceba que a solução polimórfica sacrifica a simplicidade do código. Além disso, se você sempre armazenar seus casos de switch em enumerações, alguns compiladores o alertarão que os estados estão ausentes no switch.
Harvey

1
Este é um ótimo exemplo de operações completas / incompletas e, é claro, reestruturando o código no OOP. Muito obrigado. Eu acho que será útil se os proponentes de OOP / Design Patterns sugerirem tratar conceitos de OOP como operadores e não como conceitos . Quero dizer que "estende", "fábrica", "implementa", etc, são usados ​​com tanta frequência em arquivos, classes, ramificações. Eles devem ser simples, pois operadores como "+", "-", "+ =", "?:", "==", "->" etc. Quando um programador os utiliza em sua mente apenas como operadores, apenas então ele pode pensar em toda a biblioteca de classes sobre o estado do programa e (in) concluir as operações.
namespaceform

13
Estou começando a pensar que SWITCH é muito mais compreensível e lógico, comparado a isso. E eu geralmente como OOP muito, mas esta resolução parece muito abstrato para mim
JK

1
Com o objeto Command, o código a ser gerado Map<Integer, Command>não precisaria de uma opção?
precisa saber é

41

Um switch é um padrão, implementado com uma instrução switch, caso contrário, cadeia, tabela de pesquisa, polimorfismo oop, correspondência de padrões ou qualquer outra coisa.

Deseja eliminar o uso da " instrução switch " ou do " padrão switch "? O primeiro pode ser eliminado, o segundo, apenas se outro padrão / algoritmo puder ser usado, e na maioria das vezes isso não é possível ou não é uma abordagem melhor para fazê-lo.

Se você deseja eliminar a instrução switch do código, a primeira pergunta a fazer é onde faz sentido eliminar a instrução switch e usar alguma outra técnica. Infelizmente, a resposta a esta pergunta é específica do domínio.

E lembre-se de que os compiladores podem fazer várias otimizações para alternar instruções. Portanto, por exemplo, se você deseja processar as mensagens com eficiência, uma instrução switch é praticamente o caminho a percorrer. Por outro lado, a execução de regras de negócios com base em uma instrução switch provavelmente não é o melhor caminho a ser percorrido, e o aplicativo deve ser pesquisado novamente.

Aqui estão algumas alternativas para mudar de instrução:


1
Alguém poderia fazer uma comparação do processamento de mensagens usando switch versus outras alternativas?
Mike A

37

Mudar por si só não é tão ruim, mas se você tiver muitos "switch" ou "if / else" nos objetos em seus métodos, pode ser um sinal de que seu design é um pouco "processual" e que seus objetos são apenas valor baldes. Mova a lógica para seus objetos, invoque um método em seus objetos e deixe-os decidir como responder.


Supondo, claro, que ele não está escrevendo no C. :)
Bernard

1
em C, ele pode (? ab) ponteiros de função uso e estruturas para construir algo objeto like-;)
teta

Você pode escrever Java FORT ^ H ^ H ^ H ^ H em qualquer idioma. ; p
Bernard

Concordo plenamente - o switch é uma ótima maneira de reduzir as linhas de código, mas não brinque com ele também.
HotJard 13/10

21

Eu acho que a melhor maneira é usar um bom mapa. Usando um dicionário, você pode mapear quase qualquer entrada para algum outro valor / objeto / função.

seu código ficaria assim (psuedo) assim:

void InitMap(){
    Map[key1] = Object/Action;
    Map[key2] = Object/Action;
}

Object/Action DoStuff(Object key){
    return Map[key];
}

4
Depende do idioma. Ele pode ficar muito menos legível do que um interruptor
Vinko Vrsalovic

Esta é uma boa solução elegent na situação certa. Fiz esse mapeamento de códigos-chave recentemente, e parecia ler bem para esse propósito.
Bernard

Isso é verdade, eu provavelmente não usaria isso para nada simples, mas fornece uma certa flexibilidade em termos de configuração em uma declaração de switch. Um dicionário pode ser preparado em tempo real enquanto um comutador sempre será codificado.
21942 Josh

Pode ser mais limpo em algumas circunstâncias. Também é mais lento, pois requer chamadas de função.
Nick Johnson

1
Isso depende das suas chaves. Um compilador pode compilar uma instrução switch em uma pesquisa simples ou em uma pesquisa binária estática extremamente rápida, em ambos os casos sem exigir nenhuma chamada de função.
Nick Johnson

12

Todo mundo adora ENORME if elseblocos. Tão fácil de ler! Estou curioso para saber por que você deseja remover as instruções do switch. Se você precisar de uma instrução switch, provavelmente precisará de uma instrução switch. Sério, eu diria que depende do que o código está fazendo. Se tudo o que o switch está fazendo é chamar funções (digamos), você pode passar ponteiros de função. Se é uma solução melhor , é discutível.

A linguagem é um fator importante aqui também, eu acho.


13
Im assumindo que era sarcasmo :)
Craig dia

6

Eu acho que o que você está procurando é o Padrão de Estratégia.

Isso pode ser implementado de várias maneiras, que foram mencionadas em outras respostas a esta pergunta, como:

  • Um mapa de valores -> funções
  • Polimorfismo. (o subtipo de um objeto decidirá como ele lida com um processo específico).
  • Funções de primeira classe.

5

switch seria bom substituir as instruções se você adicionar novos estados ou novo comportamento às instruções:

estado int;

String getString () {
   switch (estado) {
     caso 0: // comportamento para o estado 0
           retornar "zero";
     caso 1: // comportamento para o estado 1
           retornar "um";
   }
   lançar novo IllegalStateException ();
}

double getDouble () {

   switch (this.state) {
     caso 0: // comportamento para o estado 0
           retornar 0d;
     caso 1: // comportamento para o estado 1
           retornar 1d;
   }
   lançar novo IllegalStateException ();
}

Adicionar novo comportamento requer copiar switche adicionar novos estados significa adicionar outro casea cada switch instrução.

Em Java, você pode alternar apenas um número muito limitado de tipos primitivos cujos valores você conhece no tempo de execução. Isso apresenta um problema por si só: os estados estão sendo representados como números ou caracteres mágicos.

A correspondência de padrões e vários if - elseblocos podem ser usados, embora realmente tenham os mesmos problemas ao adicionar novos comportamentos e novos estados.

A solução sugerida por outros como "polimorfismo" é uma instância do padrão do Estado :

Substitua cada um dos estados por sua própria classe. Cada comportamento tem seu próprio método na classe:

Estado estatal;

String getString () {
   retornar state.getString ();
}

double getDouble () {
   retornar state.getDouble ();
}

Cada vez que você adiciona um novo estado, é necessário adicionar uma nova implementação da IStateinterface. Em um switchmundo, você adicionaria um casea cada um switch.

Cada vez que você adiciona um novo comportamento, é necessário adicionar um novo método à IStateinterface e a cada uma das implementações. Essa é a mesma carga de antes, embora agora o compilador verifique se você tem implementações do novo comportamento em cada estado preexistente.

Outros já disseram que isso pode ser muito pesado, então é claro que há um ponto em que você chega onde se move de um para outro. Pessoalmente, a segunda vez que escrevo um switch é o ponto em que refatoro.


4

se-mais

Eu refuto a premissa de que o interruptor é inerentemente ruim.


3

Bem, por um lado, eu não sabia que usar o interruptor era um anti-padrão.

Em segundo lugar, a opção sempre pode ser substituída por instruções if / else if.


exatamente - switch é apenas metadona sintática para vários if / elsifs.
Mike A

3

Por que você quer? Nas mãos de um bom compilador, uma instrução switch pode ser muito mais eficiente do que blocos if / else (além de ser mais fácil de ler), e somente os maiores switches provavelmente serão acelerados se forem substituídos por qualquer tipo da estrutura de dados de pesquisa indireta.


2
Nesse momento, você está adivinhando o compilador e fazendo alterações no seu design com base em componentes internos do compilador. O design deve seguir a natureza do problema, não a natureza do compilador.
Mike A

3

'switch' é apenas uma construção de linguagem e todas as construções de linguagem podem ser pensadas como ferramentas para realizar um trabalho. Como nas ferramentas reais, algumas são mais adequadas a uma tarefa do que a outra (você não usaria uma marreta para colocar um gancho de imagem). A parte importante é como é definido 'fazer o trabalho'. Ela precisa ser mantida, precisa ser rápida, precisa ser dimensionada, precisa ser extensível e assim por diante.

Em cada ponto do processo de programação, geralmente há uma variedade de construções e padrões que podem ser usados: um comutador, uma sequência if-else-if, funções virtuais, tabelas de salto, mapas com ponteiros de função e assim por diante. Com a experiência, um programador saberá instintivamente a ferramenta certa a ser usada em uma determinada situação.

Deve-se supor que qualquer pessoa que mantenha ou revise o código seja pelo menos tão experiente quanto o autor original, para que qualquer construção possa ser usada com segurança.


OK, mas por que precisamos de 5 maneiras diferentes e redundantes de fazer a mesma coisa - execução condicional?
Mike A

@ mike.amy: Como cada método tem benefícios e custos diferentes, trata-se de obter o máximo benefício com o menor custo.
309 Skizz #

1

Se a opção existe para distinguir entre vários tipos de objetos, provavelmente você está perdendo algumas classes para descrever com precisão esses objetos ou alguns métodos virtuais ...


1

Para C ++

Se você está se referindo a um AbstractFactory, acho que um método registerCreatorFunc (..) geralmente é melhor do que precisar adicionar um caso para cada declaração "nova" necessária. Em seguida, deixe todas as classes criarem e registrarem uma função creator (..) que pode ser facilmente implementada com uma macro (se ouso mencionar). Eu acredito que essa é uma abordagem comum que muitos frameworks fazem. Vi pela primeira vez no ET ++ e acho que muitas estruturas que exigem uma macro DECL e IMPL o utilizam.


1

Em uma linguagem processual, como C, o switch será melhor do que qualquer uma das alternativas.

Em uma linguagem orientada a objetos, quase sempre existem outras alternativas disponíveis que utilizam melhor a estrutura do objeto, particularmente o polimorfismo.

O problema com as instruções do comutador surge quando vários blocos de comutadores muito semelhantes ocorrem em vários locais do aplicativo e o suporte a um novo valor precisa ser adicionado. É muito comum um desenvolvedor esquecer de adicionar suporte ao novo valor a um dos blocos de switch espalhados pelo aplicativo.

Com o polimorfismo, uma nova classe substitui o novo valor e o novo comportamento é adicionado como parte da adição da nova classe. O comportamento nesses pontos de comutação é herdado da superclasse, substituído para fornecer um novo comportamento ou implementado para evitar um erro do compilador quando o super método é abstrato.

Onde não existe um polimorfismo óbvio, pode valer a pena implementar o padrão da estratégia .

Mas se sua alternativa for um grande bloco IF ... THEN ... ELSE, esqueça-o.


1

Use um idioma que não vem com uma instrução de switch interna. Perl 5 vem à mente.

Sério, por que você quer evitá-lo? E se você tem um bom motivo para evitá-lo, por que não simplesmente evitá-lo?


1

Os ponteiros de função são uma maneira de substituir uma enorme declaração de switch robusta; eles são especialmente bons em idiomas onde você pode capturar funções por seus nomes e fazer coisas com elas.

Obviamente, você não deve forçar as instruções do switch a sair do seu código, e sempre há uma chance de você estar fazendo tudo errado, o que resulta em trechos redundantes de código estúpidos. (Isso é inevitável às vezes, mas um bom idioma deve permitir que você remova a redundância enquanto permanece limpo.)

Este é um ótimo exemplo de divisão e conquista:

Digamos que você tenha algum intérprete.

switch(*IP) {
    case OPCODE_ADD:
        ...
        break;
    case OPCODE_NOT_ZERO:
        ...
        break;
    case OPCODE_JUMP:
        ...
        break;
    default:
        fixme(*IP);
}

Em vez disso, você pode usar isto:

opcode_table[*IP](*IP, vm);

... // in somewhere else:
void opcode_add(byte_opcode op, Vm* vm) { ... };
void opcode_not_zero(byte_opcode op, Vm* vm) { ... };
void opcode_jump(byte_opcode op, Vm* vm) { ... };
void opcode_default(byte_opcode op, Vm* vm) { /* fixme */ };

OpcodeFuncPtr opcode_table[256] = {
    ...
    opcode_add,
    opcode_not_zero,
    opcode_jump,
    opcode_default,
    opcode_default,
    ... // etc.
};

Observe que não sei como remover a redundância da tabela opcode_table em C. Talvez eu deva fazer uma pergunta sobre isso. :)


0

A resposta mais óbvia, independente da linguagem, é usar uma série de 'se'.

Se o idioma que você está usando tiver ponteiros de função (C) ou funções com valores de 1ª classe (Lua), você poderá obter resultados semelhantes a uma "opção" usando uma matriz (ou uma lista) de funções (ponteiros para).

Você deve ser mais específico no idioma, se quiser melhores respostas.


0

As instruções de chave geralmente podem ser substituídas por um bom design de OO.

Por exemplo, você tem uma classe de conta e está usando um extrato de opção para executar um cálculo diferente com base no tipo de conta.

Eu sugeriria que isso fosse substituído por várias classes de contas, representando os diferentes tipos de conta, e todas implementando uma interface de Conta.

A mudança se torna desnecessária, pois você pode tratar todos os tipos de contas da mesma forma e, graças ao polimorfismo, o cálculo apropriado será executado para o tipo de conta.


0

Depende do motivo pelo qual você deseja substituí-lo!

Muitos intérpretes usam 'gotos computados' em vez de instruções de chave para execução de código de operação.

O que sinto falta no switch C / C ++ é o Pascal 'in' e os intervalos. Eu também gostaria de poder ativar as cordas. Mas estes, embora sejam triviais para um compilador comer, são trabalhosos quando feitos usando estruturas, iteradores e outras coisas. Então, pelo contrário, há muitas coisas que eu gostaria de substituir por um switch, se apenas o switch de C () fosse mais flexível!


0

O switch não é um bom caminho a percorrer, pois quebra o Principal Principal Aberto. É assim que eu faço.

public class Animal
{
       public abstract void Speak();
}


public class Dog : Animal
{
   public virtual void Speak()
   {
       Console.WriteLine("Hao Hao");
   }
}

public class Cat : Animal
{
   public virtual void Speak()
   {
       Console.WriteLine("Meauuuu");
   }
}

E aqui está como usá-lo (usando seu código):

foreach (var animal in zoo) 
{
    echo animal.speak();
}

Basicamente, o que estamos fazendo é delegar a responsabilidade à classe filho, em vez de pedir aos pais que decidam o que fazer com os filhos.

Você também pode ler sobre o "Princípio de substituição de Liskov".


0

Em JavaScript, usando matriz associativa:
this:

function getItemPricing(customer, item) {
    switch (customer.type) {
        // VIPs are awesome. Give them 50% off.
        case 'VIP':
            return item.price * item.quantity * 0.50;

            // Preferred customers are no VIPs, but they still get 25% off.
        case 'Preferred':
            return item.price * item.quantity * 0.75;

            // No discount for other customers.
        case 'Regular':
        case
        default:
            return item.price * item.quantity;
    }
}

torna-se o seguinte:

function getItemPricing(customer, item) {
var pricing = {
    'VIP': function(item) {
        return item.price * item.quantity * 0.50;
    },
    'Preferred': function(item) {
        if (item.price <= 100.0)
            return item.price * item.quantity * 0.75;

        // Else
        return item.price * item.quantity;
    },
    'Regular': function(item) {
        return item.price * item.quantity;
    }
};

    if (pricing[customer.type])
        return pricing[customer.type](item);
    else
        return pricing.Regular(item);
}

Cortesia


-12

Outro voto para if / else. Eu não sou um grande fã de declarações de caso ou troca, porque existem algumas pessoas que não as usam. O código é menos legível se você usar maiúsculas ou minúsculas. Talvez não seja menos legível para você, mas para aqueles que nunca precisaram usar o comando.

O mesmo vale para fábricas de objetos.

Os blocos if / else são uma construção simples que todos recebem. Há algumas coisas que você pode fazer para garantir que não cause problemas.

Em primeiro lugar - não tente recuar as declarações mais de um par de vezes. Se você está se identificando, está fazendo errado.

 if a = 1 then 
     do something else 
     if a = 2 then 
         do something else
     else 
         if a = 3 then 
             do the last thing
         endif
     endif 
  endif

É muito ruim - faça isso em seu lugar.

if a = 1 then 
   do something
endif 
if a = 2 then 
   do something else
endif 
if a = 3 then 
   do something more
endif 

Otimização seja condenada. Não faz muita diferença na velocidade do seu código.

Em segundo lugar, não sou avesso a interromper um bloco If, desde que haja instruções de interrupção suficientes espalhadas pelo bloco de código específico para torná-lo óbvio

procedure processA(a:int)
    if a = 1 then 
       do something
       procedure_return
    endif 
    if a = 2 then 
       do something else
       procedure_return
    endif 
    if a = 3 then 
       do something more
       procedure_return
    endif 
end_procedure

EDIT : No Switch e por que eu acho difícil grunhir:

Aqui está um exemplo de uma instrução switch ...

private void doLog(LogLevel logLevel, String msg) {
   String prefix;
   switch (logLevel) {
     case INFO:
       prefix = "INFO";
       break;
     case WARN:
       prefix = "WARN";
       break;
     case ERROR:
       prefix = "ERROR";
       break;
     default:
       throw new RuntimeException("Oops, forgot to add stuff on new enum constant");
   }
   System.out.println(String.format("%s: %s", prefix, msg));
 }

Para mim, a questão aqui é que as estruturas de controle normais que se aplicam em linguagens como C foram completamente quebradas. Existe uma regra geral de que, se você quiser colocar mais de uma linha de código dentro de uma estrutura de controle, use chaves ou uma instrução begin / end.

por exemplo

for i from 1 to 1000 {statement1; statement2}
if something=false then {statement1; statement2}
while isOKtoLoop {statement1; statement2}

Para mim (e você pode me corrigir se eu estiver errado), a instrução Case lança essa regra pela janela. Um bloco de código executado condicionalmente não é colocado dentro de uma estrutura de início / fim. Por isso, acredito que Case seja conceitualmente diferente o suficiente para não ser usado.

Espero que responda às suas perguntas.


Uau - obviamente uma resposta controversa. Eu estaria interessado em saber o que eu errei.
seanyboy 24/09/08

Er, mudar como sendo muito complexo? Eu não sei ... parece-me que não haveria muitos recursos de idioma disponíveis para você usar. :) Além disso, no seu exemplo central, não seria mais inteligente se (a == 1 || a == 2 || a == 3) fizesse alguma coisa?
Lars Westergren

Além disso, em seu último exemplo, "break" não fará nada na maioria dos idiomas - que rompe o bloco mais próximo (geralmente um loop), o que, no seu caso, acontece de qualquer maneira na próxima linha (endif). Se você usar um idioma em que o intervalo é "retorno", muitos retornos também serão desaprovados (exceto para "declarações de guarda")
Lars Westergren

4
"Otimização, droga. Não faz muita diferença na velocidade do seu código." Meu código é executado em uma plataforma móvel. Otimizações são importantes. Além disso, os switches podem fazer com que o código pareça MUITO limpo (se comparado a if..elseif..elseif..elseif ...) se usado corretamente. Nunca os viu? Aprenda-os.
Swati

Mudar não é nada complicado, mas sempre tentei minimizar o número de construções usadas no código para reduzir o atrito no entendimento.
seanyboy 24/09/08
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.