Come rifattorizzare un'applicazione con più casi di switch?

7

Ho un'applicazione che accetta un intero come input e basato sull'input chiama metodi statici di classi diverse. Ogni volta che viene aggiunto un nuovo numero, è necessario aggiungere un altro caso e chiamare un metodo statico diverso di una classe diversa. Ora ci sono 50 casi nell'interruttore e ogni volta che ho bisogno di aggiungere un altro caso, rabbrividisco. C'è un modo migliore per farlo.

Ho riflettuto e ho avuto questa idea. Io uso il modello di strategia. Invece di avere un caso di scambio, ho una mappa degli oggetti strategia con la chiave che è l'intero di input. Una volta che il metodo è stato richiamato, cercherà l'oggetto e chiamerà il metodo generico per l'oggetto. In questo modo posso evitare di usare il costrutto switch case.

Che ne pensi?

    
posta Kaushik Chakraborty 23.04.2017 - 07:30
fonte

3 risposte

10

There are now 50 cases in the switch and every time I need to add another case, I shudder.

Adoro il polimorfismo. Io amo SOLIDO. Adoro la pura programmazione orientata agli oggetti. Detesto vedere questi dati come una cattiva reputazione perché vengono applicati dogmaticamente.

Non hai fatto un buon caso per il refactoring della strategia. A proposito, il refactoring ha un nome. Si chiama Sostituisci condizionale con polimorfismo .

Ho trovato alcuni consigli pertinenti per te da c2.com :

It really only makes sense if the same or very similar conditional tests are repeated often. For simple, seldom-repeated tests, replacing a simple conditional with the verbosity of multiple class definitions, and likely moving all this far from the code that actually requires the conditionally required activity, would result in a textbook example of code obfuscation. Prefer clarity over dogmatic purity. -- DanMuller

Hai un interruttore con 50 casi e la tua alternativa è produrre 50 oggetti. Oh e 50 linee di codice di costruzione dell'oggetto. Questo non è un progresso. Perchè no? Perché questo refactoring non fa nulla per ridurre il numero da 50. Usi questo refactoring quando trovi che devi creare un'altra istruzione switch sullo stesso input da qualche altra parte. Questo è il momento in cui questo refactoring aiuta perché restituisce 100 indietro in 50.

Finché ti riferisci a "the switch" come se fosse l'unico che hai, non lo consiglio. L'unico vantaggio derivante dal refactoring ora è che riduce le possibilità che un po 'di goofball copierà e incollerà il tuo switch da 50 case.

Quello che raccomando è guardare da vicino questi 50 casi per elementi comuni che possono essere scomposti. Intendo 50? Veramente? Sei sicuro di aver bisogno di tanti casi? Potresti provare a fare molto qui.

    
risposta data 23.04.2017 - 09:28
fonte
9

Una mappa degli oggetti strategia, che è inizializzata in alcune funzioni del tuo codice, dove hai diverse linee di codice che assomigliano a

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

richiede a te e ai tuoi colleghi di implementare le funzioni / strategie da chiamare in classi separate, in un modo più uniforme (dal momento che gli oggetti della strategia dovranno implementare la stessa interfaccia). Tale codice è spesso un po 'più completo di

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

Tuttavia, non ti libererà dal compito di modificare questo file di codice ogni volta che è necessario aggiungere un nuovo numero. I veri vantaggi di questo approccio sono diversi:

  • l'inizializzazione della mappa ora viene separata dal codice di invio che in realtà chiama la funzione associata a un numero specifico, e quest'ultima non contiene quelli 50 ripetizioni, assomiglierà a myMap[number].DoIt(someParameters) . Quindi questo codice di spedizione non deve essere toccato ogni volta che arriva un nuovo numero e può essere implementato secondo il principio di Open-Closed. Inoltre, quando ottieni requisiti in cui è necessario estendere il codice di spedizione stesso, non dovrai più modificare 50 posizioni, ma solo una.

  • il contenuto della mappa viene determinato in fase di esecuzione (mentre il contenuto del costrutto switch viene determinato prima della compilazione), quindi questo ti dà l'opportunità di rendere la logica di inizializzazione più flessibile o estendibile.

Quindi sì, ci sono alcuni vantaggi, e questo è sicuramente un passo avanti verso più codice SOLID. Tuttavia, se paga il refactoring, è qualcosa che tu o il tuo team dovrete decidere da soli. Se non ti aspetti che il codice di spedizione venga modificato, la logica di inizializzazione da modificare e la leggibilità di switch non è un problema reale, il tuo refactoring potrebbe non essere così importante ora.

    
risposta data 23.04.2017 - 08:36
fonte
0

Sono strongmente favorevole alla strategia delineata in la risposta di @DocBrown .

Ho intenzione di suggerire un miglioramento della risposta.

Le chiamate

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

può essere distribuito. Non è necessario tornare allo stesso file per aggiungere un'altra strategia, che aderisce al principio Open-Closed ancora meglio.

Supponi di implementare Strategy1 nel file Strategy1.cpp. Puoi avere il seguente blocco di codice in esso.

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

static Initializer initializer;

Puoi ripetere lo stesso codice in ogni file StategyN.cpp. Come puoi vedere, questo sarà un sacco di codice ripetuto. Per ridurre la duplicazione del codice, puoi utilizzare un modello che può essere inserito in un file accessibile a tutte le classi Strategy .

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

Dopodiché, l'unica cosa che devi usare in Strategy1.cpp è:

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

La riga corrispondente in StrategyN.cpp è:

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

Puoi usare i modelli su un altro livello usando un modello di classe per le classi di strategia concrete.

class Strategy { ... };

template <int N> class ConcreteStrategy;

E poi, anziché Strategy1 , usa ConcreteStrategy<1> .

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

Cambia la classe helper per registrare Strategy s in:

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

Cambia il codice in Strateg1.cpp in:

static StrategyHelper::Initializer<1> initializer;

Cambia il codice in StrategN.cpp in:

static StrategyHelper::Initializer<N> initializer;
    
risposta data 24.04.2017 - 21:05
fonte

Leggi altre domande sui tag