Como refatorar um aplicativo com vários casos de switch?

10

Eu tenho um aplicativo que leva um número inteiro como entrada e com base na entrada chama métodos estáticos de diferentes classes. Sempre que um novo número é adicionado, precisamos adicionar outro caso e chamar um método estático diferente de uma classe diferente. Agora existem 50 casos no comutador e toda vez que preciso adicionar outro caso, estremeço. Existe uma maneira melhor de fazer isso.

Pensei um pouco e tive essa ideia. Eu uso o padrão de estratégia. Em vez de ter um caso de opção, eu tenho um mapa de objetos de estratégia com a chave como o número inteiro de entrada. Depois que o método é chamado, ele pesquisará o objeto e chamará o método genérico para o objeto. Dessa forma, posso evitar o uso da construção de caso de switch.

O que você acha?

Kaushik Chakraborty
fonte
2
Qual é o problema real com o código atual?
Philip Kendall
O que acontece quando você precisa fazer uma dessas alterações? Você precisa adicionar um switchcaso e chamar um método preexistente em seu sistema complexo ou precisa inventar o método e sua chamada?
Kilian Foth
@KilianFoth Eu herdei este projeto como desenvolvedor de manutenção e ainda não precisei fazer alterações. No entanto, farei alterações em breve, por isso quero refatorar agora. Mas, para responder sua pergunta, sim para o último.
precisa saber é o seguinte
2
Eu acho que você precisa mostrar um exemplo condensado do que está acontecendo.
Whatsisname
11
@ KaushikChakraborty: depois crie um exemplo da memória. Existem situações em que um uber-switch de mais de 250 casos é apropriado e há casos em que o switch é ruim, não importa quantos casos ele tenha. O diabo está nos detalhes e não temos detalhes.
Whatsisname

Respostas:

13

Agora existem 50 casos no comutador e toda vez que preciso adicionar outro caso, estremeço.

Eu amo polimorfismo. Eu amo o SOLID. Eu amo pura programação orientada a objetos. Eu odeio ver esses dados com um péssimo representante, porque eles são aplicados dogmaticamente.

Você não fez um bom argumento para refatorar a estratégia. A refatoração tem um nome a propósito. Chama-se Substituir Condicional pelo Polimorfismo .

Encontrei alguns conselhos pertinentes para você no site c2.com :

Realmente só faz sentido se os testes condicionais iguais ou muito semelhantes forem repetidos com frequência. Para testes simples e raramente repetidos, substituir um condicional simples pela verbosidade de várias definições de classe e provavelmente afastar tudo isso do código que realmente requer a atividade condicionalmente necessária resultaria em um exemplo de ofuscação de código. Prefira clareza a pureza dogmática. - DanMuller

Você tem um switch com 50 caixas e sua alternativa é produzir 50 objetos. Oh e 50 linhas de código de construção do objeto. Isto não é progresso. Por que não? Como essa refatoração não faz nada para reduzir o número de 50. Você usa essa refatoração quando descobrir que precisa criar outra instrução de opção na mesma entrada em outro lugar. É quando essa refatoração ajuda porque transforma 100 em 50 novamente.

Desde que você esteja se referindo à "opção" como se fosse a única que você possui, não recomendo isso. A única vantagem resultante da refatoração agora é que ela reduz as chances de que alguns bobões copiem e colem sua chave de 50 casos.

O que eu recomendo é examinar atentamente esses 50 casos em busca de pontos em comum que podem ser levados em consideração. Quero dizer 50? Realmente? Tem certeza de que precisa de tantos casos? Você pode estar tentando fazer muito aqui.

candied_orange
fonte
Eu concordo com o que voce esta dizendo. O código possui muitas redundâncias, pode ser que muitos dos casos nem sejam necessários, mas, de uma maneira superficial, isso não parece verdade. Cada caso chama um método que chama vários sistemas e agrega os resultados e retorna ao código de chamada. Toda classe é independente, realiza um trabalho e tenho medo de violar o princípio da alta coesão, se eu reduzir o número de casos.
Kaushik Chakraborty
2
Posso conseguir 50 sem violar a alta coesão e manter as coisas independentes. Eu simplesmente não posso fazer isso com um número. Eu precisaria de um 2, um 5 e outro 5. É por isso que é chamado de fatoração. Sério, olhe para toda a sua arquitetura e veja se você não consegue encontrar uma saída para esse inferno de 50 casos em que está. Refatorar significa desfazer más decisões. Não os perpetuando de novas formas.
candied_orange
Agora, se você puder ver uma maneira de reduzir os 50 usando essa refatoração, vá em frente. Para aproveitar a ideia do Doc Browns: Um mapa de mapas pode ter duas chaves. Algo para pensar sobre.
candied_orange
11
Eu concordo com o comentário de Candied. O problema não são os 50 casos na instrução switch, o problema é o design de arquitetura de nível superior que está fazendo com que você chame uma função que precisa decidir entre 50 opções. Eu projetei alguns sistemas muito grandes e complexos e nunca fui forçado a uma situação como essa.
Dunk
@Candied "Você usa essa refatoração quando descobre que precisa criar outra instrução switch na mesma entrada em outro lugar." Você pode elaborar isso? primeiro projeto de autorização, validação, procedimentos CRUD e então dao. Portanto, em todas as camadas existem casos de comutação na mesma entrada, ou seja, um número inteiro, mas executando funções diferentes como auth, válido. então devemos criar uma classe de cada tipo que possua métodos diferentes? Nosso caso se encaixa no que você está tentando dizer "repetindo a mesma opção na mesma entrada"?
Siddharth Trikha
9

Um mapa de objetos de estratégia sozinho, que é inicializado em alguma função do seu código, em que você tem várias linhas de código parecidas com

     myMap.Add(1,new Strategy1());
     myMap.Add(2,new Strategy2());
     myMap.Add(3,new Strategy3());

requer que você e seus colegas implementem as funções / estratégias a serem chamadas em classes separadas, de uma maneira mais uniforme (já que todos os seus objetos de estratégia terão que implementar a mesma interface). Esse código geralmente é um pouco mais abrangente do que

     case 1:
          MyClass1.Doit1(someParameters);
          break;
     case 2:
          MyClass2.Doit2(someParameters);
          break;
     case 3:
          MyClass3.Doit3(someParameters);
          break;

No entanto, ele ainda não o liberará do fardo de editar esse arquivo de código sempre que um novo número precisar ser adicionado. Os benefícios reais dessa abordagem são diferentes:

  • a inicialização do mapa agora se separa do código de despacho que realmente chama a função associada a um número específico, e o último não contém mais essas 50 repetições, apenas parecerá myMap[number].DoIt(someParameters). Portanto, esse código de despacho não precisa ser tocado sempre que um novo número chegar e pode ser implementado de acordo com o princípio Aberto-Fechado. Além disso, quando você obtém requisitos nos quais precisa estender o próprio código de despacho, não precisará mais alterar 50 locais, mas apenas um.

  • o conteúdo do mapa é determinado em tempo de execução (enquanto o conteúdo da construção do comutador é determinado antes do tempo de compilação), portanto, você tem a oportunidade de tornar a lógica de inicialização mais flexível ou extensível.

Portanto, sim, existem alguns benefícios, e este é certamente um passo em direção a mais código SOLID. Se vale a pena refatorar, no entanto, é algo que você ou sua equipe terão que decidir por si mesmos. Se você não espera que o código de expedição seja alterado, a lógica de inicialização seja alterada e a legibilidade do arquivo switchnão seja um problema real, sua refatoração poderá não ser tão importante agora.

Doc Brown
fonte
Enquanto reluto em substituir cegamente todos os comutadores por polimorfismo, direi que usar um mapa da maneira como Doc Brown sugere aqui funcionou muito bem para mim no passado. Quando você implementar a mesma interface substitua Doit1, Doit2, etc, com um Doitmétodo que tem muitas implementações diferentes.
candied_orange
E se você tiver controle sobre o tipo de símbolo de entrada usado como chave, poderá dar um passo adiante, criando doTheThing()um método para o símbolo de entrada. Então você não precisa manter o mapa.
precisa saber é o seguinte
11
@KevinKrumwiede: o que você sugere significa simplesmente passar os objetos de estratégia pelo programa, como um substituto para os números inteiros. No entanto, quando o programa recebe um número inteiro como entrada de alguma fonte de dados externa, é necessário que haja um mapeamento do número inteiro para a estratégia relacionada pelo menos em um local do sistema.
Doc Brown
Expandindo a sugestão do Doc Brown: você também pode criar uma fábrica que contenha a lógica para criar os objetos de estratégia, caso decida seguir esse caminho. Dito isto, a resposta fornecida por CandiedOrange faz mais sentido para mim.
precisa saber é o seguinte
@DocBrown É com isso que eu estava falando "se você tem controle sobre o tipo do símbolo de entrada".
precisa saber é o seguinte
0

Sou totalmente a favor da estratégia descrita na resposta do @DocBrown .

Vou sugerir uma melhoria para a resposta.

As chamadas

 myMap.Add(1,new Strategy1());
 myMap.Add(2,new Strategy2());
 myMap.Add(3,new Strategy3());

pode ser distribuído. Você não precisa voltar ao mesmo arquivo para adicionar outra estratégia, que adere ao princípio Aberto-Fechado ainda melhor.

Digamos que você implemente Strategy1no arquivo Strategy1.cpp. Você pode ter o seguinte bloco de código nele.

namespace Strategy1_Impl
{
   struct Initializer
   {
      Initializer()
      {
         getMap().Add(1, new Strategy1());
      }
   };
}
using namespace Strategy1_Impl;

static Initializer initializer;

Você pode repetir o mesmo código em todos os arquivos StategyN.cpp. Como você pode ver, haverá muito código repetido. Para reduzir a duplicação de código, você pode usar um modelo que pode ser colocado em um arquivo acessível a todas as Strategyclasses.

namespace StrategyHelper
{
   template <int N, typename StrategyType> struct Initializer
   {
      Initializer()
      {
         getMap().Add(N, new StrategyType());
      }
   };
}

Depois disso, a única coisa que você deve usar no Strategy1.cpp é:

static StrategyHelper::Initializer<1, Strategy1> initializer;

A linha correspondente no StrategyN.cpp é:

static StrategyHelper::Initializer<N, StrategyN> initializer;

Você pode levar o uso de modelos para outro nível usando um modelo de classe para as classes concretas da Strategy.

class Strategy { ... };

template <int N> class ConcreteStrategy;

E então, em vez de Strategy1, use ConcreteStrategy<1>.

template <> class ConcreteStrategy<1> : public Strategy { ... };

Altere a classe auxiliar para registrar Strategys para:

namespace StrategyHelper
{
   template <int N> struct Initializer
   {
      Initializer()
      {
         getMap().Add(N, new ConcreteStrategy<N>());
      }
   };
}

Altere o código em Strateg1.cpp para:

static StrategyHelper::Initializer<1> initializer;

Altere o código no StrategN.cpp para:

static StrategyHelper::Initializer<N> initializer;
R Sahu
fonte