Quali sono i modi per eliminare l'uso dello switch nel codice?
Quali sono i modi per eliminare l'uso dello switch nel codice?
Risposte:
Le istruzioni switch non sono di per sé un antipattern, ma se stai codificando orientato agli oggetti dovresti considerare se l'uso di uno switch è meglio risolto con polimorfismo invece di usare un'istruzione switch.
Con il polimorfismo, questo:
foreach (var animal in zoo) {
switch (typeof(animal)) {
case "dog":
echo animal.bark();
break;
case "cat":
echo animal.meow();
break;
}
}
diventa questo:
foreach (var animal in zoo) {
echo animal.speak();
}
typeof
, e questa risposta non suggerisce modi o ragioni per aggirare le istruzioni switch in altre situazioni.
Vedi l' odore delle dichiarazioni degli interruttori :
In genere, istruzioni di switch simili sono sparse in un programma. Se aggiungi o rimuovi una clausola in un interruttore, spesso devi trovare e riparare anche gli altri.
Sia Refactoring che Refactoring to Patterns hanno approcci per risolvere questo problema.
Se il tuo codice (pseudo) è simile a:
class RequestHandler {
public void handleRequest(int action) {
switch(action) {
case LOGIN:
doLogin();
break;
case LOGOUT:
doLogout();
break;
case QUERY:
doQuery();
break;
}
}
}
Questo codice viola il principio aperto aperto ed è fragile per ogni nuovo tipo di codice di azione che si presenta. Per porre rimedio a ciò, è possibile introdurre un oggetto "Command":
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 il tuo codice (pseudo) è simile a:
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");
}
}
Quindi è possibile introdurre un oggetto "Stato".
// 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();
}
}
Spero che questo ti aiuti.
Map<Integer, Command>
non avrebbe bisogno di un interruttore?
Un interruttore è un modello, sia implementato con un'istruzione switch, se altrimenti catena, tabella di ricerca, polimorfismo di oop, abbinamento di modelli o qualcos'altro.
Vuoi eliminare l'uso della " istruzione switch " o " switch pattern "? Il primo può essere eliminato, il secondo, solo se è possibile utilizzare un altro modello / algoritmo e il più delle volte ciò non è possibile o non è un approccio migliore per farlo.
Se vuoi eliminare l' istruzione switch dal codice, la prima domanda da porsi è dove ha senso eliminare l'istruzione switch e usare qualche altra tecnica. Purtroppo la risposta a questa domanda è specifica del dominio.
E ricorda che i compilatori possono fare varie ottimizzazioni per cambiare le istruzioni. Ad esempio, se si desidera eseguire l'elaborazione dei messaggi in modo efficiente, un'istruzione switch è praticamente la strada da percorrere. D'altra parte, l'esecuzione di regole aziendali basate su un'istruzione switch non è probabilmente il modo migliore per procedere e l'applicazione dovrebbe essere rearchitected.
Ecco alcune alternative per cambiare istruzione:
Switch in sé non è poi così male, ma se hai molti "switch" o "if / else" su oggetti nei tuoi metodi, potrebbe essere un segno che il tuo design è un po '"procedurale" e che i tuoi oggetti sono solo valore secchi. Sposta la logica sui tuoi oggetti, invoca un metodo sui tuoi oggetti e lascia che decidano invece come rispondere.
Penso che il modo migliore sia usare una buona mappa. Usando un dizionario puoi mappare quasi tutti gli input ad altri valori / oggetti / funzioni.
il tuo codice sarebbe simile a (psuedo) come questo:
void InitMap(){
Map[key1] = Object/Action;
Map[key2] = Object/Action;
}
Object/Action DoStuff(Object key){
return Map[key];
}
Tutti adorano i if else
blocchi ENORMI . Così facile da leggere! Sono curioso di sapere perché vorresti rimuovere le istruzioni switch, però. Se hai bisogno di un'istruzione switch, probabilmente hai bisogno di un'istruzione switch. Scherzi a parte, direi che dipende da cosa sta facendo il codice. Se tutto ciò che lo switch sta facendo è chiamare funzioni (diciamo) potresti passare i puntatori a funzione. Se è una soluzione migliore è discutibile.
La lingua è un fattore importante anche qui, penso.
Penso che quello che stai cercando sia il modello strategico.
Questo può essere implementato in diversi modi, che sono stati menzionati in altre risposte a questa domanda, come ad esempio:
switch
le dichiarazioni sarebbero buone da sostituire se ti ritrovi ad aggiungere nuovi stati o nuovi comportamenti alle dichiarazioni:
int state; String getString () { switch (stato) { caso 0: // comportamento per lo stato 0 ritorna "zero"; caso 1: // comportamento per lo stato 1 restituisce "uno"; } lanciare nuovo IllegalStateException (); } doppio getDouble () { switch (this.state) { caso 0: // comportamento per lo stato 0 ritorna 0d; caso 1: // comportamento per lo stato 1 ritorno 1d; } lanciare nuovo IllegalStateException (); }
L'aggiunta di nuovi comportamenti richiede la copia di switch
, e l'aggiunta di nuovi stati significa aggiungere un altro case
a ogni switch
istruzione.
In Java, è possibile cambiare solo un numero molto limitato di tipi primitivi di cui si conoscono i valori in fase di esecuzione. Questo presenta un problema in sé e per sé: gli stati vengono rappresentati come numeri o personaggi magici.
if - else
È possibile utilizzare la corrispondenza dei modelli e più blocchi, sebbene abbiano gli stessi problemi quando si aggiungono nuovi comportamenti e nuovi stati.
La soluzione che altri hanno suggerito come "polimorfismo" è un'istanza del modello statale :
Sostituisci ciascuno degli stati con la propria classe. Ogni comportamento ha il suo metodo sulla classe:
Stato IState; String getString () { return state.getString (); } doppio getDouble () { return state.getDouble (); }
Ogni volta che aggiungi un nuovo stato, devi aggiungere una nuova implementazione IState
dell'interfaccia. In un switch
mondo, aggiungeresti uno case
a ciascuno switch
.
Ogni volta che aggiungi un nuovo comportamento, devi aggiungere un nuovo metodo IState
all'interfaccia e ciascuna delle implementazioni. Questo è lo stesso onere di prima, sebbene ora il compilatore controllerà che tu abbia implementazioni del nuovo comportamento su ogni stato preesistente.
Altri hanno già detto che questo potrebbe essere troppo pesante, quindi ovviamente c'è un punto che raggiungi dove ti sposti da uno all'altro. Personalmente, la seconda volta che scrivo un interruttore è il punto in cui refactoring.
se altro
Respingo la premessa secondo cui l'interruttore è intrinsecamente negativo.
Perché vuoi Nelle mani di un buon compilatore, un'istruzione switch può essere molto più efficiente rispetto ai blocchi if / else (oltre ad essere più facile da leggere) e è probabile che solo gli switch più grandi vengano accelerati se vengono sostituiti da qualsiasi tipo della struttura dei dati di ricerca indiretta.
'switch' è solo un costrutto di linguaggio e tutti i costrutti di linguaggio possono essere pensati come strumenti per svolgere un lavoro. Come con gli strumenti reali, alcuni strumenti si adattano meglio a un'attività piuttosto che a un'altra (non si userebbe una mazza per montare un gancio per foto). La parte importante è come viene definito "portare a termine il lavoro". Deve essere mantenibile, deve essere veloce, deve essere ridimensionato, deve essere estensibile e così via.
Ad ogni punto del processo di programmazione di solito ci sono una serie di costrutti e modelli che possono essere usati: un interruttore, una sequenza if-else-if, funzioni virtuali, tabelle di salto, mappe con puntatori a funzione e così via. Con l'esperienza un programmatore conoscerà istintivamente lo strumento giusto da utilizzare per una determinata situazione.
Si deve presumere che chiunque mantenga o riveda il codice sia esperto almeno quanto l'autore originale in modo che qualsiasi costrutto possa essere usato in sicurezza.
Se l'interruttore è lì per distinguere tra vari tipi di oggetti, probabilmente ti mancheranno alcune classi per descrivere con precisione quegli oggetti o alcuni metodi virtuali ...
Per C ++
Se ti riferisci ad esempio a una AbstractFactory, penso che un metodo registerCreatorFunc (..) di solito sia meglio che richiedere di aggiungere un caso per ogni "nuova" istruzione necessaria. Quindi, lasciando che tutte le classi creino e registrino una creatorFunction (..) che può essere facilmente implementata con una macro (se oso citare). Credo che questo sia un approccio comune a molti framework. L'ho visto per la prima volta in ET ++ e penso che molti framework che richiedono una macro DECL e IMPL lo utilizzino.
In un linguaggio procedurale, come C, quindi passare sarà migliore di una qualsiasi delle alternative.
In un linguaggio orientato agli oggetti, ci sono quasi sempre altre alternative disponibili che utilizzano meglio la struttura dell'oggetto, in particolare il polimorfismo.
Il problema con le istruzioni switch si presenta quando si verificano diversi blocchi switch molto simili in più punti dell'applicazione e deve essere aggiunto il supporto per un nuovo valore. È abbastanza comune per uno sviluppatore dimenticare di aggiungere il supporto per il nuovo valore a uno dei blocchi di switch sparsi nell'applicazione.
Con il polimorfismo, una nuova classe sostituisce il nuovo valore e il nuovo comportamento viene aggiunto come parte dell'aggiunta della nuova classe. Il comportamento in questi punti di commutazione viene quindi ereditato dalla superclasse, sovrascritto per fornire un nuovo comportamento o implementato per evitare un errore del compilatore quando il super metodo è astratto.
Laddove non è in atto un polimorfismo evidente, può valere la pena implementare il modello strategico .
Ma se la tua alternativa è un grande blocco IF ... THEN ... ELSE, allora dimenticalo.
I puntatori a funzione sono un modo per sostituire un'enorme istruzione switch pesante, sono particolarmente buoni nelle lingue in cui è possibile acquisire funzioni con i loro nomi e fare cose con loro.
Ovviamente, non dovresti forzare le istruzioni switch a uscire dal tuo codice, e c'è sempre la possibilità che tu stia sbagliando tutto, il che si traduce in stupide parti ridondanti di codice. (Questo è inevitabile a volte, ma una buona lingua dovrebbe consentire di rimuovere la ridondanza pur rimanendo pulito.)
Questo è un grande esempio di divisione e conquista:
Supponi di avere un interprete di qualche tipo.
switch(*IP) {
case OPCODE_ADD:
...
break;
case OPCODE_NOT_ZERO:
...
break;
case OPCODE_JUMP:
...
break;
default:
fixme(*IP);
}
Invece, puoi usare questo:
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.
};
Si noti che non so come rimuovere la ridondanza di opcode_table in C. Forse dovrei fare una domanda al riguardo. :)
La risposta più ovvia, indipendente dalla lingua, è quella di utilizzare una serie di "if".
Se la lingua che stai utilizzando ha i puntatori a funzione (C) o ha funzioni che sono valori di 1a classe (Lua), puoi ottenere risultati simili a uno "switch" usando un array (o un elenco) di funzioni (puntatori a).
Dovresti essere più specifico sulla lingua se vuoi risposte migliori.
Le istruzioni switch possono spesso essere sostituite da un buon design OO.
Ad esempio, hai una classe Account e stai utilizzando un'istruzione switch per eseguire un calcolo diverso in base al tipo di account.
Vorrei suggerire che questo dovrebbe essere sostituito da un numero di classi di account, che rappresentano i diversi tipi di account e tutti implementano un'interfaccia Account.
Lo switch diventa quindi non necessario, poiché puoi trattare tutti i tipi di account allo stesso modo e grazie al polimorfismo, verrà eseguito il calcolo appropriato per il tipo di account.
Dipende dal motivo per cui si desidera sostituirlo!
Molti interpreti usano "goto calcolate" invece di istruzioni switch per l'esecuzione di opcode.
Quello che mi manca dello switch C / C ++ è Pascal 'in' e intervalli. Vorrei anche poter attivare le stringhe. Ma questi, sebbene banali da mangiare per un compilatore, sono un duro lavoro quando si usano strutture, iteratori e cose. Quindi, al contrario, ci sono molte cose che vorrei poter sostituire con un interruttore, se solo l'interruttore C () fosse più flessibile!
Switch non è un buon modo di procedere poiché interrompe l'Open Close Principal. Questo è come lo faccio.
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");
}
}
Ed ecco come usarlo (prendendo il tuo codice):
foreach (var animal in zoo)
{
echo animal.speak();
}
Fondamentalmente quello che stiamo facendo è delegare la responsabilità alla classe del bambino invece di far decidere al genitore cosa fare dei bambini.
Potresti anche voler leggere su "Principio di sostituzione di Liskov".
In JavaScript utilizzando l'array Associative:
questo:
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;
}
}
diventa questo:
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);
}
Un altro voto per if / else. Non sono un grande fan delle affermazioni sui casi o sui cambi perché ci sono persone che non li usano. Il codice è meno leggibile se si utilizza case o switch. Forse non meno leggibile per te, ma per quelli che non hanno mai avuto bisogno di usare il comando.
Lo stesso vale per le fabbriche di oggetti.
I blocchi if / else sono un costrutto semplice che tutti ottengono. Ci sono alcune cose che puoi fare per assicurarti di non causare problemi.
Primo: non cercare di indentare se le dichiarazioni più di un paio di volte. Se ti ritrovi a rientrare, allora stai sbagliando.
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
È davvero male - fallo invece.
if a = 1 then
do something
endif
if a = 2 then
do something else
endif
if a = 3 then
do something more
endif
L'ottimizzazione sia dannata. Non fa molta differenza per la velocità del tuo codice.
In secondo luogo, non sono contrario a uscire da un blocco If fintanto che ci sono abbastanza istruzioni break sparse attraverso il blocco di codice particolare per renderlo ovvio
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 : su Switch e perché penso sia difficile grok:
Ecco un esempio di un'istruzione 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));
}
Per me il problema qui è che le normali strutture di controllo che si applicano in linguaggi simili a C sono state completamente rotte. Esiste una regola generale che, se si desidera inserire più di una riga di codice all'interno di una struttura di controllo, utilizzare parentesi graffe o un'istruzione iniziale / finale.
per esempio
for i from 1 to 1000 {statement1; statement2}
if something=false then {statement1; statement2}
while isOKtoLoop {statement1; statement2}
Per me (e puoi correggermi se sbaglio), l'istruzione Case getta questa regola fuori dalla finestra. Un blocco di codice eseguito in modo condizionale non viene inserito in una struttura di inizio / fine. Per questo motivo, credo che Case sia concettualmente abbastanza diverso da non essere utilizzato.
Spero che risponda alle tue domande.