Lange Liste von if-Anweisungen in Java

101

Es tut mir leid, dass ich keine Frage finden kann, die diese Frage beantwortet. Ich bin mir fast sicher, dass jemand anderes sie zuvor gestellt hat.

Mein Problem ist, dass ich einige Systembibliotheken schreibe, um eingebettete Geräte auszuführen. Ich habe Befehle, die über Radiosendungen an diese Geräte gesendet werden können. Dies kann nur per Text erfolgen. Innerhalb der Systembibliotheken habe ich einen Thread, der die Befehle behandelt, die so aussehen

if (value.equals("A")) { doCommandA() }
else if (value.equals("B")) { doCommandB() } 
else if etc. 

Das Problem ist, dass es viele Befehle gibt, die schnell zu etwas außer Kontrolle geraten. Es ist schrecklich, herauszuschauen, schmerzhaft zu debuggen und in ein paar Monaten umwerfend zu verstehen.

Steve
quelle
16
Nur ein Kommentar - Ich würde dringend empfehlen, das Buch "Gang of Four Patterns" oder, wenn Sie mit Mustern noch nicht vertraut sind, das Buch "Head First Design Patterns in Java" zu lesen (das ziemlich einfach zu lesen ist und eine gute Einführung in eine Reihe gängiger Muster bietet ). Beide sind wertvolle Ressourcen und beide haben meinen Speck mehr als einmal gerettet.
Aperkins
2
Ja, eigentlich besaß ich sie, aber sie fehlen :) Deshalb war ich mir sicher, dass das, was ich tat, falsch war :) Ich konnte jedoch keine richtige Lösung finden! Vielleicht bekommt dies eine schöne Google-Position
Steve
2
Es ist nur Befehlsmuster Montag hier!
Nick Veys

Antworten:

171

Verwenden des Befehlsmusters :

public interface Command {
     void exec();
}

public class CommandA() implements Command {

     void exec() {
          // ... 
     }
}

// etc etc

Erstellen Sie dann ein Map<String,Command>Objekt und füllen Sie es mit CommandInstanzen:

commandMap.put("A", new CommandA());
commandMap.put("B", new CommandB());

dann können Sie Ihre if / else if- Kette ersetzen durch:

commandMap.get(value).exec();

BEARBEITEN

Sie können auch spezielle Befehle wie UnknownCommandoder hinzufügen NullCommand, benötigen jedoch einen CommandMapBefehl, der diese Eckfälle behandelt, um die Überprüfungen des Clients zu minimieren.

dfa
quelle
1
... mit der entsprechenden Überprüfung, dass commandMap.get () nicht null zurückgibt :-)
Brian Agnew
3
Natürlich habe ich der Einfachheit halber etwas Boilerplate-Code weggelassen
dfa
10
Anstelle einer HashMap können Sie eine Java-Enumeration verwenden, die Ihnen anstelle einer matschigen Map einen genau definierten Befehlssatz bietet. Sie könnten einen Getter in der Aufzählung haben: Command getCommand (); oder implementieren Sie exec () sogar als abstrakte Methode in der Aufzählung, die jede Instanz implementiert (Aufzählung als Befehl).
JeeBee
2
Dies zwingt dazu, alle Befehle in der Aufzählung zu implementieren ... das ist bei weitem ideal. Mit einer Schnittstelle können Sie auch das Decorator-Muster anwenden (z. B. DebugCommandDecorator, TraceCommandDecorator). In eine einfache Java-Schnittstelle ist viel mehr Flexibilität integriert
dfa
5
Ok, für kleine und nie wachsende Befehle ist eine Aufzählung eine praktikable Lösung.
dfa
12

Mein Vorschlag wäre eine Art leichte Kombination aus Aufzählung und Befehlsobjekt. Dies ist eine von Joshua Bloch in Punkt 30 von Effective Java empfohlene Redewendung.

public enum Command{
  A{public void doCommand(){
      // Implementation for A
    }
  },
  B{public void doCommand(){
      // Implementation for B
    }
  },
  C{public void doCommand(){
      // Implementation for C
    }
  };
  public abstract void doCommand();
}

Natürlich können Sie Parameter an doCommand übergeben oder Rückgabetypen haben.

Diese Lösung ist möglicherweise nicht wirklich geeignet, wenn die Implementierungen von doCommand nicht wirklich zum Aufzählungstyp "passen", was - wie üblich, wenn Sie einen Kompromiss eingehen müssen - etwas unscharf ist.

jens
quelle
7

Haben Sie eine Aufzählung von Befehlen:

public enum Commands { A, B, C; }
...

Command command = Commands.valueOf(value);

switch (command) {
    case A: doCommandA(); break;
    case B: doCommandB(); break;
    case C: doCommandC(); break;
}

Wenn Sie mehr als ein paar Befehle haben, prüfen Sie die Verwendung des Befehlsmusters, wie an anderer Stelle beantwortet (obwohl Sie die Aufzählung beibehalten und den Aufruf der implementierenden Klasse in die Aufzählung einbetten können, anstatt eine HashMap zu verwenden). Ein Beispiel finden Sie in der Antwort von Andreas oder jens auf diese Frage.

JeeBee
quelle
5
Für jeden neuen Befehl, den Sie hinzufügen, müssen Sie den Schalter bearbeiten: Dieser Code folgt nicht dem Open / Closed-Prinzip
dfa
Kommt darauf an, ob es nur wenige oder viele Befehle gibt, nicht wahr? Auch diese Seite ist heutzutage so erschreckend langsam, dass 5 Versuche erforderlich sind, um eine Antwort zu bearbeiten.
JeeBee
Dies ist nicht optimal. Weitere Informationen dazu finden Sie unter stackoverflow.com/questions/1199646/… .
Andreas Petersson
Ja, danke, dass Sie sich die Zeit genommen haben, das zu implementieren, was ich am Ende meines Kommentars geschrieben habe - Java Enum als Befehlsmuster. Wenn ich meinen Beitrag bearbeiten könnte, würde ich dies erwähnen, aber diese Seite stirbt.
JeeBee
Ich denke, dass diese Frage nach einer Switch-Aussage schreit!
Michael Brown
7

Die Implementierung einer Schnittstelle, wie sie von dfa einfach und klar demonstriert wird, ist sauber und elegant (und "offiziell" unterstützt). Dafür ist das Schnittstellenkonzept gedacht.

In C # könnten wir Delegaten für Programmierer verwenden, die gerne Funktionszeiger in c verwenden, aber die DFA-Technik ist die Art zu verwenden.

Sie könnten auch ein Array haben

Command[] commands =
{
  new CommandA(), new CommandB(), new CommandC(), ...
}

Dann könnten Sie einen Befehl per Index ausführen

commands[7].exec();

Plagiieren von DFAs, aber mit einer abstrakten Basisklasse anstelle einer Schnittstelle. Beachten Sie den cmdKey, der später verwendet wird. Aus Erfahrung stelle ich fest, dass ein Gerätebefehl häufig auch Unterbefehle enthält.

abstract public class Command()
{
  abstract public byte exec(String subCmd);
  public String cmdKey;
  public String subCmd;
}

Konstruieren Sie Ihre Befehle so:

public class CommandA
extends Command
{
  public CommandA(String subCmd)
  {
    this.cmdKey = "A";
    this.subCmd = subCmd;
  }

  public byte exec()
  {
    sendWhatever(...);
    byte status = receiveWhatever(...);
    return status;
  }
}

Sie können dann die generische HashMap oder HashTable erweitern, indem Sie eine Schlüssel-Wert-Paar-Saugfunktion bereitstellen:

public class CommandHash<String, Command>
extends HashMap<String, Command>
(
  public CommandHash<String, Command>(Command[] commands)
  {
    this.commandSucker(Command[] commands);
  }
  public commandSucker(Command[] commands)
  {
    for(Command cmd : commands)
    {
      this.put(cmd.cmdKey, cmd);
    }
  }
}

Erstellen Sie dann Ihren Befehlsspeicher:

CommandHash commands =
  new CommandHash(
  {
    new CommandA("asdf"),
    new CommandA("qwerty"),
    new CommandB(null),
    new CommandC("hello dolly"),
    ...
  });

Jetzt können Sie Steuerelemente objektiv senden

commands.get("A").exec();
commands.get(condition).exec();
Gesegneter Geek
quelle
+1 für die Erwähnung von Delegierten für den Fall, dass .NET-Benutzer diese Frage sehen und mit den Ein-Methoden-Schnittstellen verrückt werden. Aber sie sind wirklich nicht mit Funktionszeigern vergleichbar. Sie sind näher an einer sprachunterstützten Version des Befehlsmusters.
Daniel Earwicker
5

Nun, ich schlage vor, Befehlsobjekte zu erstellen und sie mit dem String als Schlüssel in eine Hashmap einzufügen.

keuleJ
quelle
3

Auch wenn ich glaube, dass der Befehlsmusteransatz eher auf die besten Praktiken ausgerichtet ist und auf lange Sicht gewartet werden kann, ist hier eine Einzeiler-Option für Sie:

org.apache.commons.beanutils.MethodUtils.invokeMethod (this, "doCommand" + value, null);

Svachon
quelle
2

Normalerweise versuche ich es so zu lösen:

public enum Command {

A {void exec() {
     doCommandA();
}},

B {void exec() {
    doCommandB();
}};

abstract void exec();
 }

Das hat viele Vorteile:

1) Es ist nicht möglich, eine Aufzählung hinzuzufügen, ohne exec zu implementieren. Sie werden also kein A verpassen.

2) Sie müssen es nicht einmal zu einer Befehlskarte hinzufügen, also keinen Boilerplate-Code zum Erstellen der Karte. nur die abstrakte Methode und ihre Implementierungen. (Das ist wohl auch Boilerplate, aber es wird nicht kürzer ..)

3) Sie sparen alle verschwendeten CPU-Zyklen, indem Sie eine lange Liste von ifs durchgehen oder hashCodes berechnen und nachschlagen.

Bearbeiten: Wenn Sie keine Aufzählungen, sondern Zeichenfolgen als Quelle haben, Command.valueOf(mystr).exec()rufen Sie einfach die exec-Methode auf. Beachten Sie, dass Sie den öffentlichen Modifikator für execif verwenden müssen, den Sie von einem anderen Paket aus aufrufen möchten.

Andreas Petersson
quelle
2

Am besten verwenden Sie eine Befehlskarte.

Aber haben Sie eine Reihe von Karten, mit denen Sie fertig werden müssen? Am Ende klopfen viele Karten herum. Dann lohnt es sich, es mit Enums zu machen.

Sie können dies mit einer Aufzählung tun, ohne Schalter zu verwenden (wahrscheinlich benötigen Sie die Getter im Beispiel nicht), wenn Sie der Aufzählung eine Methode hinzufügen, um nach "Wert" aufzulösen. Dann können Sie einfach tun:

Update: Statische Zuordnung hinzugefügt, um Iterationen bei jedem Aufruf zu vermeiden. Schamlos von dieser Antwort gekniffen .

Commands.getCommand(value).exec();

public interface Command {
    void exec();
}

public enum Commands {
    A("foo", new Command(){public void exec(){
        System.out.println(A.getValue());
    }}),
    B("bar", new Command(){public void exec(){
        System.out.println(B.getValue());
    }}),
    C("barry", new Command(){public void exec(){
        System.out.println(C.getValue());
    }});

    private String value;
    private Command command;
    private static Map<String, Commands> commandsMap;

    static {
        commandsMap = new HashMap<String, Commands>();
        for (Commands c : Commands.values()) {
            commandsMap.put(c.getValue(), c);    
        }
    }

    Commands(String value, Command command) {
        this.value= value;
        this.command = command;
    }

    public String getValue() {
        return value;
    }

    public Command getCommand() {
        return command;
    }

    public static Command getCommand(String value) {
        if(!commandsMap.containsKey(value)) {
            throw new RuntimeException("value not found:" + value);
        }
        return commandsMap.get(value).getCommand();
    }
}
Reicher Verkäufer
quelle
2

Die Antwort von @dfa ist meiner Meinung nach die beste Lösung.

Ich stelle nur einige Schnipsel zur Verfügung, falls Sie Java 8 verwenden und Lambdas verwenden möchten!

Befehl ohne Parameter:

Map<String, Command> commands = new HashMap<String, Command>();
commands.put("A", () -> System.out.println("COMMAND A"));
commands.put("B", () -> System.out.println("COMMAND B"));
commands.put("C", () -> System.out.println("COMMAND C"));
commands.get(value).exec();

(Sie könnten ein Runnable anstelle von Command verwenden, aber ich halte es nicht für semantisch richtig):

Befehl mit einem Parameter:

Wenn Sie einen Parameter erwarten, können Sie Folgendes verwenden java.util.function.Consumer:

Map<String, Consumer<Object>> commands = new HashMap<String, Consumer<Object>>();
commands.put("A", myObj::doSomethingA);
commands.put("B", myObj::doSomethingB);
commands.put("C", myObj::doSomethingC);
commands.get(value).accept(param);

Im obigen Beispiel doSomethingXist eine Methode in myObjder Klasse vorhanden, die ein beliebiges Objekt ( paramin diesem Beispiel benannt) als Argument verwendet.

Marlon Bernardes
quelle
1

Wenn Sie mehrere imbrizierte 'if'-Anweisungen haben, ist dies ein Muster für die Verwendung einer Regelengine . Siehe zum Beispiel JBOSS Drools .

Pierre
quelle
0

Wenn es möglich wäre, eine Reihe von Prozeduren (was Sie Befehle nannten) zu haben, wäre das nützlich.

Sie könnten jedoch ein Programm schreiben, um Ihren Code zu schreiben. Es ist alles sehr systematisch, wenn (value = 'A') commandA (); sonst wenn (........................ etc.

user147042
quelle
0

Ich bin mir nicht sicher, ob sich das Verhalten Ihrer verschiedenen Befehle überschneidet, aber Sie sollten sich auch das Muster der Verantwortungskette ansehen , das mehr Flexibilität bietet, indem mehrere Befehle einige Eingabewerte verarbeiten können.

Tinyd
quelle
0

Befehlsmuster ist der richtige Weg. Hier ist ein Beispiel mit Java 8:

1. Definieren Sie die Schnittstelle:

public interface ExtensionHandler {
  boolean isMatched(String fileName);
  String handle(String fileName);
}

2. Implementieren Sie die Schnittstelle mit jeder der Erweiterungen:

public class PdfHandler implements ExtensionHandler {
  @Override
  public boolean isMatched(String fileName) {
    return fileName.endsWith(".pdf");
  }

  @Override
  public String handle(String fileName) {
    return "application/pdf";
  }
}

und

public class TxtHandler implements ExtensionHandler {
  @Override public boolean isMatched(String fileName) {
    return fileName.endsWith(".txt");
  }

  @Override public String handle(String fileName) {
    return "txt/plain";
  }
}

und so weiter .....

3. Definieren Sie den Client:

public class MimeTypeGetter {
  private List<ExtensionHandler> extensionHandlers;
  private ExtensionHandler plainTextHandler;

  public MimeTypeGetter() {
    extensionHandlers = new ArrayList<>();

    extensionHandlers.add(new PdfHandler());
    extensionHandlers.add(new DocHandler());
    extensionHandlers.add(new XlsHandler());

    // and so on

    plainTextHandler = new PlainTextHandler();
    extensionHandlers.add(plainTextHandler);
  }

  public String getMimeType(String fileExtension) {
    return extensionHandlers.stream()
      .filter(handler -> handler.isMatched(fileExtension))
      .findFirst()
      .orElse(plainTextHandler)
      .handle(fileExtension);
  }
}

4. Und das ist das Beispielergebnis:

  public static void main(String[] args) {
    MimeTypeGetter mimeTypeGetter = new MimeTypeGetter();

    System.out.println(mimeTypeGetter.getMimeType("test.pdf")); // application/pdf
    System.out.println(mimeTypeGetter.getMimeType("hello.txt")); // txt/plain
    System.out.println(mimeTypeGetter.getMimeType("my presentation.ppt")); // "application/vnd.ms-powerpoint"
  }
nxhoaf
quelle
-1

Wenn es viele Dinge tut, dann wird es viel Code geben, davon kann man nicht wirklich wegkommen. Machen Sie es einfach, zu folgen, geben Sie den Variablen sehr aussagekräftige Namen, Kommentare können auch helfen ...

Kennzeichen
quelle