Wie kann die Verwendung von Switch im Code vermieden werden?
Wie kann die Verwendung von Switch im Code vermieden werden?
Antworten:
Switch-Anweisungen sind an sich kein Antimuster, aber wenn Sie objektorientiert codieren, sollten Sie überlegen, ob die Verwendung eines Switches besser mit Polymorphismus gelöst werden kann, anstatt eine switch-Anweisung zu verwenden.
Mit Polymorphismus ist dies:
foreach (var animal in zoo) {
switch (typeof(animal)) {
case "dog":
echo animal.bark();
break;
case "cat":
echo animal.meow();
break;
}
}
wird dies:
foreach (var animal in zoo) {
echo animal.speak();
}
typeof
, und diese Antwort schlägt keine Möglichkeiten oder Gründe vor, um switch-Anweisungen in anderen Situationen zu umgehen.
Siehe die Switch-Anweisungen Geruch :
In der Regel sind ähnliche switch-Anweisungen über ein Programm verteilt. Wenn Sie eine Klausel in einem Switch hinzufügen oder entfernen, müssen Sie häufig auch die anderen suchen und reparieren.
Sowohl Refactoring als auch Refactoring zu Mustern haben Ansätze, um dies zu beheben.
Wenn Ihr (Pseudo-) Code so aussieht:
class RequestHandler {
public void handleRequest(int action) {
switch(action) {
case LOGIN:
doLogin();
break;
case LOGOUT:
doLogout();
break;
case QUERY:
doQuery();
break;
}
}
}
Dieser Code verstößt gegen das Open Closed-Prinzip und ist für jede neue Art von Aktionscode anfällig. Um dies zu beheben, können Sie ein 'Command'-Objekt einführen:
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();
}
}
Wenn Ihr (Pseudo-) Code so aussieht:
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");
}
}
Dann könnten Sie ein 'State'-Objekt einführen.
// 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();
}
}
Hoffe das hilft.
Map<Integer, Command>
keinen Schalter benötigen?
Ein Schalter ist ein Muster, unabhängig davon, ob es mit einer switch-Anweisung implementiert ist, ob es sich um eine Kette, eine Nachschlagetabelle, einen OOP-Polymorphismus, einen Mustervergleich oder etwas anderes handelt.
Möchten Sie die Verwendung der " switch-Anweisung " oder des " switch-Musters " vermeiden ? Das erste kann eliminiert werden, das zweite nur, wenn ein anderes Muster / ein anderer Algorithmus verwendet werden kann, und meistens ist dies nicht möglich oder es ist kein besserer Ansatz, dies zu tun.
Wenn Sie die switch-Anweisung aus dem Code entfernen möchten, müssen Sie zunächst die Frage stellen, wo es sinnvoll ist, die switch-Anweisung zu entfernen und eine andere Technik zu verwenden. Leider ist die Antwort auf diese Frage domänenspezifisch.
Und denken Sie daran, dass Compiler verschiedene Optimierungen vornehmen können, um Anweisungen zu wechseln. Wenn Sie beispielsweise die Nachrichtenverarbeitung effizient durchführen möchten, ist eine switch-Anweisung genau das Richtige. Andererseits ist das Ausführen von Geschäftsregeln auf der Grundlage einer switch-Anweisung wahrscheinlich nicht der beste Weg, und die Anwendung sollte erneut durchsucht werden.
Hier sind einige Alternativen zur switch-Anweisung:
Das Wechseln an sich ist nicht so schlimm, aber wenn Sie in Ihren Methoden viele "Schalter" oder "Wenn / Sonst" für Objekte haben, kann dies ein Zeichen dafür sein, dass Ihr Design ein bisschen "prozedural" ist und dass Ihre Objekte nur Wert sind Eimer. Verschieben Sie die Logik auf Ihre Objekte, rufen Sie eine Methode für Ihre Objekte auf und lassen Sie sie entscheiden, wie sie stattdessen reagieren sollen.
Ich denke, der beste Weg ist, eine gute Karte zu verwenden. Mit einem Wörterbuch können Sie fast jede Eingabe einem anderen Wert / Objekt / einer anderen Funktion zuordnen.
Ihr Code würde ungefähr so aussehen (Pseudo):
void InitMap(){
Map[key1] = Object/Action;
Map[key2] = Object/Action;
}
Object/Action DoStuff(Object key){
return Map[key];
}
Jeder liebt riesige if else
Blöcke. So einfach zu lesen! Ich bin gespannt, warum Sie switch-Anweisungen entfernen möchten. Wenn Sie eine switch-Anweisung benötigen, benötigen Sie wahrscheinlich eine switch-Anweisung. Im Ernst, ich würde sagen, es hängt davon ab, was der Code tut. Wenn der Switch nur Funktionen aufruft (z. B.), können Sie Funktionszeiger übergeben. Ob es eine bessere Lösung ist, ist umstritten.
Sprache ist auch hier ein wichtiger Faktor, denke ich.
Ich denke, was Sie suchen, ist das Strategiemuster.
Dies kann auf verschiedene Arten implementiert werden, die in anderen Antworten auf diese Frage erwähnt wurden, wie zum Beispiel:
switch
Anweisungen sollten ersetzt werden, wenn Sie feststellen, dass Sie den Anweisungen neue Zustände oder neues Verhalten hinzufügen:
int state; String getString () { Schalter (Zustand) { Fall 0: // Verhalten für Zustand 0 return "zero"; Fall 1: // Verhalten für Zustand 1 gib "eins" zurück; }} neue IllegalStateException () auslösen; }} double getDouble () { switch (this.state) { Fall 0: // Verhalten für Zustand 0 return 0d; Fall 1: // Verhalten für Zustand 1 return 1d; }} neue IllegalStateException () auslösen; }}
Das Hinzufügen eines neuen Verhaltens erfordert das Kopieren des switch
und das Hinzufügen neuer Zustände bedeutet das Hinzufügen eines weiteren case
zu jeder switch
Anweisung.
In Java können Sie nur eine sehr begrenzte Anzahl von primitiven Typen wechseln, deren Werte Sie zur Laufzeit kennen. Dies stellt an und für sich ein Problem dar: Zustände werden als magische Zahlen oder Zeichen dargestellt.
Mustervergleich und mehrere if - else
Blöcke können verwendet werden, haben jedoch dieselben Probleme beim Hinzufügen neuer Verhaltensweisen und neuer Zustände.
Die Lösung, die andere als "Polymorphismus" vorgeschlagen haben, ist ein Beispiel für das Zustandsmuster :
Ersetzen Sie jeden Status durch eine eigene Klasse. Jedes Verhalten hat eine eigene Methode für die Klasse:
IState Zustand; String getString () { return state.getString (); }} double getDouble () { return state.getDouble (); }}
Jedes Mal, wenn Sie einen neuen Status hinzufügen, müssen Sie eine neue Implementierung der IState
Schnittstelle hinzufügen . In einer switch
Welt, dann würden Sie eine das Hinzufügen case
zu jeder switch
.
Jedes Mal, wenn Sie ein neues Verhalten hinzufügen, müssen Sie der IState
Schnittstelle und jeder Implementierung eine neue Methode hinzufügen . Dies ist die gleiche Belastung wie zuvor, obwohl der Compiler jetzt überprüft, ob Sie Implementierungen des neuen Verhaltens für jeden bereits vorhandenen Status haben.
Andere haben bereits gesagt, dass dies zu schwer sein könnte, also gibt es natürlich einen Punkt, an dem Sie von einem zum anderen wechseln. Persönlich ist das zweite Mal, dass ich einen Schalter schreibe, der Punkt, an dem ich umgestalte.
ansonsten
Ich widerlege die Prämisse, dass der Wechsel von Natur aus schlecht ist.
Warum willst du? In den Händen eines guten Compilers kann eine switch-Anweisung weitaus effizienter sein als if / else-Blöcke (und auch leichter zu lesen), und nur die größten Switches werden wahrscheinlich beschleunigt, wenn sie durch irgendeine Art ersetzt werden der indirekten Suchdatenstruktur.
'switch' ist nur ein Sprachkonstrukt und alle Sprachkonstrukte können als Werkzeuge angesehen werden, um einen Job zu erledigen. Wie bei echten Werkzeugen eignen sich einige Werkzeuge besser für eine Aufgabe als für eine andere (Sie würden keinen Vorschlaghammer verwenden, um einen Bilderhaken anzubringen). Der wichtige Teil ist, wie definiert wird, wie man die Arbeit erledigt. Muss es wartbar sein, muss es schnell sein, muss es skaliert werden, muss es erweiterbar sein und so weiter.
An jedem Punkt des Programmierprozesses gibt es normalerweise eine Reihe von Konstrukten und Mustern, die verwendet werden können: einen Schalter, eine Wenn-Sonst-Wenn-Sequenz, virtuelle Funktionen, Sprungtabellen, Karten mit Funktionszeigern und so weiter. Mit der Erfahrung wird ein Programmierer instinktiv das richtige Werkzeug für eine bestimmte Situation kennen.
Es muss davon ausgegangen werden, dass jeder, der Code verwaltet oder überprüft, mindestens so erfahren ist wie der ursprüngliche Autor, damit jedes Konstrukt sicher verwendet werden kann.
Wenn der Schalter dazu dient, zwischen verschiedenen Arten von Objekten zu unterscheiden, fehlen Ihnen wahrscheinlich einige Klassen, um diese Objekte genau zu beschreiben, oder einige virtuelle Methoden ...
Für C ++
Wenn Sie sich auf eine AbstractFactory beziehen, ist eine registerCreatorFunc (..) -Methode normalerweise besser, als für jede "neue" Anweisung, die benötigt wird, einen Fall hinzuzufügen. Dann lassen Sie alle Klassen eine CreatorFunction (..) erstellen und registrieren, die einfach mit einem Makro implementiert werden kann (wenn ich es wage zu erwähnen). Ich glaube, dies ist ein gängiger Ansatz, den viele Frameworks verfolgen. Ich habe es zum ersten Mal in ET ++ gesehen und ich denke, dass viele Frameworks, die ein DECL- und IMPL-Makro erfordern, es verwenden.
In einer prozeduralen Sprache wie C ist der Wechsel dann besser als jede der Alternativen.
In einer objektorientierten Sprache gibt es fast immer andere Alternativen, die die Objektstruktur besser nutzen, insbesondere den Polymorphismus.
Das Problem mit switch-Anweisungen tritt auf, wenn mehrere sehr ähnliche switch-Blöcke an mehreren Stellen in der Anwendung auftreten und die Unterstützung für einen neuen Wert hinzugefügt werden muss. Es kommt häufig vor, dass Entwickler vergessen, einem der in der Anwendung verteilten Switch-Blöcke Unterstützung für den neuen Wert hinzuzufügen.
Beim Polymorphismus ersetzt eine neue Klasse den neuen Wert, und das neue Verhalten wird als Teil des Hinzufügens der neuen Klasse hinzugefügt. Das Verhalten an diesen Schaltpunkten wird dann entweder von der Oberklasse geerbt, überschrieben, um neues Verhalten bereitzustellen, oder implementiert, um einen Compilerfehler zu vermeiden, wenn die Supermethode abstrakt ist.
Wo es keinen offensichtlichen Polymorphismus gibt, kann es sich lohnen, das Strategiemuster umzusetzen .
Aber wenn Ihre Alternative ein großer WENN ... DANN ... SONST-Block ist, dann vergessen Sie ihn.
Funktionszeiger sind eine Möglichkeit, eine große, klobige switch-Anweisung zu ersetzen. Sie eignen sich besonders gut für Sprachen, in denen Sie Funktionen anhand ihrer Namen erfassen und mit ihnen erstellen können.
Natürlich sollten Sie switch-Anweisungen nicht aus Ihrem Code herauszwingen, und es besteht immer die Möglichkeit, dass Sie alles falsch machen, was zu dummen redundanten Codeteilen führt. (Dies ist manchmal unvermeidlich, aber eine gute Sprache sollte es Ihnen ermöglichen, Redundanz zu beseitigen, während Sie sauber bleiben.)
Dies ist ein großartiges Beispiel für das Teilen und Erobern:
Angenommen, Sie haben einen Dolmetscher.
switch(*IP) {
case OPCODE_ADD:
...
break;
case OPCODE_NOT_ZERO:
...
break;
case OPCODE_JUMP:
...
break;
default:
fixme(*IP);
}
Stattdessen können Sie Folgendes verwenden:
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.
};
Beachten Sie, dass ich nicht weiß, wie ich die Redundanz der opcode_table in C entfernen kann. Vielleicht sollte ich eine Frage dazu stellen. :) :)
Die naheliegendste, sprachunabhängige Antwort ist die Verwendung einer Reihe von "Wenn".
Wenn die von Ihnen verwendete Sprache Funktionszeiger (C) oder Funktionen mit Werten der 1. Klasse (Lua) enthält, können Sie mit einem Array (oder einer Liste) von (Zeigern auf) Funktionen ähnliche Ergebnisse wie bei einem "Schalter" erzielen.
Sie sollten die Sprache genauer kennen, wenn Sie bessere Antworten wünschen.
Switch-Anweisungen können häufig durch ein gutes OO-Design ersetzt werden.
Sie haben beispielsweise eine Kontoklasse und verwenden eine switch-Anweisung, um eine andere Berechnung basierend auf dem Kontotyp durchzuführen.
Ich würde vorschlagen, dass dies durch eine Reihe von Kontoklassen ersetzt wird, die die verschiedenen Kontotypen darstellen und alle eine Kontofläche implementieren.
Der Wechsel wird dann unnötig, da Sie alle Arten von Konten gleich behandeln können und dank Polymorphismus die entsprechende Berechnung für den Kontotyp ausgeführt wird.
Kommt darauf an, warum du es ersetzen willst!
Viele Interpreter verwenden 'berechnete gotos' anstelle von switch-Anweisungen für die Opcode-Ausführung.
Was ich am C / C ++ - Switch vermisse, ist das Pascal-In und die Bereiche. Ich wünschte auch, ich könnte Saiten einschalten. Obwohl dies für einen Compiler trivial ist, ist es harte Arbeit, wenn Strukturen, Iteratoren und Dinge verwendet werden. Im Gegenteil, ich wünschte, ich könnte viele Dinge durch einen Schalter ersetzen, wenn nur Cs switch () flexibler wäre!
Ein Wechsel ist kein guter Weg, da er den Open Close Principal bricht. So mache ich es.
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");
}
}
Und so verwenden Sie es (nehmen Sie Ihren Code):
foreach (var animal in zoo)
{
echo animal.speak();
}
Grundsätzlich delegieren wir die Verantwortung an die Kinderklasse, anstatt die Eltern entscheiden zu lassen, was mit Kindern geschehen soll.
Vielleicht möchten Sie auch das "Liskov-Substitutionsprinzip" nachlesen.
In JavaScript mit assoziativem Array:
dies:
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;
}
}
wird dies:
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);
}
Noch eine Abstimmung für if / else. Ich bin kein großer Fan von case- oder switch-Anweisungen, weil es einige Leute gibt, die sie nicht verwenden. Der Code ist weniger lesbar, wenn Sie case oder switch verwenden. Vielleicht nicht weniger lesbar für Sie, aber für diejenigen, die den Befehl noch nie gebraucht haben.
Gleiches gilt für Objektfabriken.
If / else-Blöcke sind ein einfaches Konstrukt, das jeder bekommt. Sie können einige Dinge tun, um sicherzustellen, dass Sie keine Probleme verursachen.
Erstens: Versuchen Sie nicht, Anweisungen mehr als ein paar Mal einzurücken. Wenn Sie feststellen, dass Sie einrücken, dann machen Sie es falsch.
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
Ist wirklich schlecht - mach das stattdessen.
if a = 1 then
do something
endif
if a = 2 then
do something else
endif
if a = 3 then
do something more
endif
Optimierung sei verdammt. Es macht keinen großen Unterschied für die Geschwindigkeit Ihres Codes.
Zweitens bin ich nicht abgeneigt, aus einem If-Block auszubrechen, solange genügend Unterbrechungsanweisungen über den jeweiligen Codeblock verteilt sind, um dies offensichtlich zu machen
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 : On Switch und warum ich denke, es ist schwer zu groken:
Hier ist ein Beispiel für eine switch-Anweisung ...
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));
}
Für mich ist das Problem hier, dass die normalen Kontrollstrukturen, die in C-ähnlichen Sprachen gelten, vollständig zerstört wurden. Es gibt eine allgemeine Regel, dass Sie, wenn Sie mehr als eine Codezeile in eine Kontrollstruktur einfügen möchten, geschweifte Klammern oder eine begin / end-Anweisung verwenden.
z.B
for i from 1 to 1000 {statement1; statement2}
if something=false then {statement1; statement2}
while isOKtoLoop {statement1; statement2}
Für mich (und Sie können mich korrigieren, wenn ich falsch liege) wirft die Case-Anweisung diese Regel aus dem Fenster. Ein bedingt ausgeführter Codeblock wird nicht in eine Anfangs- / Endstruktur eingefügt. Aus diesem Grund glaube ich, dass Case konzeptionell so unterschiedlich ist, dass es nicht verwendet werden kann.
Hoffe das beantwortet deine Fragen.