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


10

Ho un'applicazione che accetta un intero come input e basato sull'input chiama metodi statici di diverse classi. 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 nello switch e ogni volta che devo aggiungere un altro caso, rabbrividisco. C'è un modo migliore per farlo.

Ho pensato e mi è venuta questa idea. Uso il modello di strategia. Invece di avere una custodia, ho una mappa di oggetti di strategia con la chiave come intero di input. Una volta invocato il metodo, cercherà l'oggetto e chiamerà il metodo generico per l'oggetto. In questo modo posso evitare di usare il costrutto switch case.

Cosa ne pensi?


2
Qual è il problema reale con il codice corrente?
Philip Kendall,

Cosa succede quando devi apportare una di queste modifiche? Devi aggiungere un switchcaso e chiamare un metodo preesistente nel tuo sistema complesso o devi inventare sia il metodo che la sua chiamata?
Kilian Foth,

@KilianFoth Ho ereditato questo progetto come sviluppatore di manutenzione e non ho ancora apportato modifiche. Prenderò comunque presto delle modifiche, quindi voglio fare il refactoring adesso. Ma per rispondere alla tua domanda, sì a quest'ultima.
Kaushik Chakraborty,

2
Penso che tu debba mostrare un esempio condensato di ciò che sta succedendo.
whatsisname

1
@KaushikChakraborty: quindi crea un esempio dalla memoria. Ci sono situazioni in cui un uber-switch con più di 250 casi è appropriato e ci sono casi in cui switch non funziona, indipendentemente dal numero di casi. Il diavolo è nei dettagli e non abbiamo dettagli.
whatsisname

Risposte:


13

Ora ci sono 50 casi nello switch e ogni volta che devo aggiungere un altro caso, rabbrividisco.

Adoro il polimorfismo. Adoro il SOLID. Adoro la pura programmazione orientata agli oggetti. Odio vederli dare una cattiva reputazione perché vengono applicati dogmaticamente.

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

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

Ha davvero senso solo se si ripetono spesso test condizionali uguali o molto simili. Per test semplici, ripetuti di rado, la sostituzione di un semplice condizionale con la verbosità di più definizioni di classe e il probabile spostamento di tutto ciò lontano dal codice che richiede effettivamente l'attività richiesta condizionatamente, si tradurrebbe in un esempio da manuale di offuscamento del codice. Preferisce la chiarezza sulla purezza dogmatica. - DanMuller

Hai un interruttore con 50 custodie e la tua alternativa è produrre 50 oggetti. Oh e 50 righe di codice di costruzione dell'oggetto. Questo non è progresso. Perchè no? Perché questo refactoring non fa nulla per ridurre il numero da 50. Si utilizza questo refactoring quando si scopre che è necessario creare un'altra istruzione switch sullo stesso input da qualche altra parte. Questo è quando questo refactoring aiuta perché trasforma 100 in 50.

Finché ti riferisci a "l'interruttore" come se fosse l'unico che hai, non lo consiglio. L'unico vantaggio derivante dal refactoring ora è che riduce le possibilità che alcuni goofball copino e incollino il tuo interruttore a 50 casi.

Quello che raccomando è di esaminare attentamente questi 50 casi per elementi comuni che possono essere presi in considerazione. Intendo 50? Veramente? Sei sicuro di aver bisogno di tanti casi? Potresti provare a fare molto qui.


Sono d'accordo con quello che stai dicendo. Il codice ha molti licenziamenti, potrebbe darsi che molti casi non siano nemmeno necessari, ma a prima vista non sembra così. Ciascuno dei casi chiama un metodo che chiama più sistemi e aggrega i risultati e ritorna al codice chiamante. Ogni classe è autonoma, fa un lavoro e temo che violerò il principio dell'alta coesione, se dovessi ridurre il numero di casi.
Kaushik Chakraborty,

2
Posso ottenerne 50 senza violare l'alta coesione e mantenere le cose autonome. Non riesco proprio a farlo con un solo numero. Avrei bisogno di un 2, un 5 e un altro 5. Ecco perché si chiama factoring. Seriamente, guarda tutta la tua architettura e vedi se non riesci a trovare una via d'uscita da questo inferno di 50 casi in cui ti trovi. Rifattorizzare significa annullare decisioni sbagliate. Non perpetuarli in nuove forme.
candied_orange,

Ora, se riesci a vedere un modo per ridurre i 50 usando questo refactoring, fallo. Per sfruttare l'idea di Doc Browns: una mappa di mappe può richiedere due chiavi. Qualcosa a cui pensare.
candied_orange,

1
Sono d'accordo con il commento di Candied. Il problema non sono i 50 casi nell'istruzione switch, il problema è la progettazione architettonica di livello superiore che ti sta facendo chiamare una funzione che deve decidere tra 50 opzioni. Ho progettato alcuni sistemi molto grandi e complessi e non sono mai stato costretto a una situazione del genere.
Dunk

@Candied "Usi questo refactoring quando trovi che devi creare un'altra istruzione switch sullo stesso input da qualche altra parte." Puoi elaborarlo? Dato che ho un caso simile in cui ho casi switch ma su livelli diversi come quelli che abbiamo nel nostro progetto prima autorizzazione, validazione, procedure CRUD poi dao. Quindi in ogni layer sono presenti i casi switch sullo stesso input, ovvero un intero, ma che svolgono funzioni diverse come auth, valido. quindi dovremmo creare una classe per ogni tipo che ha metodi diversi? Il nostro caso si adatta a ciò che stai cercando di dire "ripetendo lo stesso interruttore sullo stesso input"?
Siddharth Trikha,

9

Una mappa di soli oggetti di strategia, che è inizializzata in alcune funzioni del tuo codice, in cui hai diverse righe di codice simili

     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 modo più uniforme (poiché i vostri oggetti di strategia dovranno implementare tutti 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à dall'onere di modificare questo file di codice ogni volta che è necessario aggiungere un nuovo numero. I vantaggi reali 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'ultimo non contiene più quelle 50 ripetizioni, sembrerà solo myMap[number].DoIt(someParameters). Pertanto, non è necessario toccare questo codice di invio ogni volta che arriva un nuovo numero e può essere implementato secondo il principio Open-Closed. Inoltre, quando ottieni requisiti in cui è necessario estendere il codice di spedizione stesso, non dovrai più cambiare 50 posizioni, ma solo una.

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

Quindi sì, ci sono alcuni vantaggi, e questo è sicuramente un passo verso più codice SOLID. Se ripagare il refactoring, tuttavia, è qualcosa che tu o la tua squadra dovrete decidere da soli. Se non ti aspetti che il codice di invio venga modificato, la logica di inizializzazione venga modificata e la leggibilità del switchnon sia un problema reale, il tuo refactoring potrebbe non essere così importante ora.


Mentre sono riluttante a sostituire ciecamente ogni interruttore con il polimorfismo, dirò che usando una mappa il modo in cui Doc Brown suggerisce qui ha funzionato molto bene per me in passato. Quando si implementa la stessa interfaccia si prega di sostituire Doit1, Doit2ecc con un Doitmetodo che ha molte implementazioni diverse.
candied_orange,

E se hai il controllo sul tipo di simbolo di input usato come chiave, puoi fare un ulteriore passo facendo doTheThing()un metodo del simbolo di input. Quindi non è necessario mantenere la mappa.
Kevin Krumwiede,

1
@KevinKrumwiede: ciò che suggerisci significa semplicemente passare gli oggetti di strategia stessi nel programma, in sostituzione degli interi. Tuttavia, quando il programma accetta un numero intero come input da un'origine dati esterna, deve esserci un mapping dall'intero alla strategia correlata almeno in un punto del sistema.
Doc Brown,

Espandendo il suggerimento di Doc Brown: potresti anche creare una fabbrica che contenga la logica per la creazione degli oggetti di strategia, se dovessi decidere di procedere in questo modo. Detto questo, la risposta fornita da CandiedOrange ha molto senso per me.
Vladimir Stokic,

@DocBrown È quello a cui stavo lavorando "se hai il controllo sul tipo di simbolo di input".
Kevin Krumwiede,

0

Sono fortemente a favore della strategia delineata nella risposta di @DocBrown .

Sto per suggerire un miglioramento alla 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 ancora meglio al principio Open-Closed.

Supponi di implementare Strategy1nel 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, sarà un sacco di codice ripetuto. Per ridurre la duplicazione del codice, è possibile utilizzare un modello che può essere inserito in un file accessibile a tutte le Strategyclassi.

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;

È possibile portare l'uso dei modelli a un altro livello utilizzando un modello di classe per le classi di strategia concrete.

class Strategy { ... };

template <int N> class ConcreteStrategy;

E poi, invece di Strategy1, utilizzare ConcreteStrategy<1>.

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

Cambia la classe helper per registrare Strategys in:

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

Modificare il codice in Strateg1.cpp in:

static StrategyHelper::Initializer<1> initializer;

Modificare il codice in StrategN.cpp in:

static StrategyHelper::Initializer<N> initializer;
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.