Długa lista instrukcji if w Javie

101

Przepraszam, że nie mogę znaleźć odpowiedzi na to pytanie, jestem prawie pewien, że ktoś inny zadał to wcześniej.

Mój problem polega na tym, że piszę biblioteki systemowe do uruchamiania urządzeń wbudowanych. Mam polecenia, które można wysłać do tych urządzeń za pośrednictwem transmisji radiowych. Można to zrobić tylko tekstem. wewnątrz bibliotek systemowych mam wątek obsługujący polecenia, które wyglądają następująco

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

Problem polega na tym, że jest w nim wiele poleceń, które szybko wymkną się spod kontroli. Okropne, aby wyglądać na zewnątrz, bolesne do debugowania i niewyobrażalne, aby zrozumieć za kilka miesięcy.

Steve
źródło
16
Tylko komentarz - zdecydowanie polecam sięgnięcie po książkę z wzorami Gang of Four lub, jeśli nie znasz się na wzorcach, książkę Head First Design Patterns in Java (która jest dość łatwym do przeczytania i świetnym wprowadzeniem do wielu popularnych wzorców ). Oba są cennymi zasobami i niejednokrotnie uratowały mój boczek.
aperkins
2
Tak, faktycznie posiadałem je, ale ich brakuje :) Dlatego byłem pewien, że robię źle :) Nie mogłem jednak znaleźć odpowiedniego rozwiązania! Może to dostanie fajną pozycję w Google
Steve
2
To tylko poniedziałek wzorca poleceń!
Nick Veys

Odpowiedzi:

171

przy użyciu wzorca polecenia :

public interface Command {
     void exec();
}

public class CommandA() implements Command {

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

// etc etc

następnie zbuduj Map<String,Command>obiekt i zapełnij go Commandinstancjami:

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

wtedy możesz zamienić swój łańcuch if / else if na :

commandMap.get(value).exec();

EDYTOWAĆ

możesz także dodać specjalne polecenia, takie jak UnknownCommandlub NullCommand, ale potrzebujesz narzędzia CommandMapobsługującego te narożne przypadki, aby zminimalizować kontrole klienta.

dfa
źródło
1
... z odpowiednim sprawdzeniem, czy polecenieMap.get () nie zwraca wartości null :-)
Brian Agnew
3
oczywiście, ze względu na prostotę pominąłem jakiś standardowy kod
dfa,
10
Zamiast HashMap możesz użyć wyliczenia Java, które daje dobrze zdefiniowany zestaw poleceń zamiast papkowatej mapy. Możesz mieć getter w enum: Polecenie getCommand (); lub nawet zaimplementuj exec () jako metodę abstrakcyjną w enum, którą implementuje każda instancja (wyliczenie jako polecenie).
JeeBee
2
wymusi to implementację wszystkich poleceń w wyliczeniu ... co jest dalekie od ideału. Dzięki interfejsowi możesz również zastosować wzorzec Decorator (np. DebugCommandDecorator, TraceCommandDecorator), jest dużo większa elastyczność wbudowana w prosty interfejs Java
dfa
5
Ok, dla małego i nigdy nie rosnącego zestawu poleceń wyliczenie jest realnym rozwiązaniem.
dfa
12

Moją sugestią byłoby lekkie połączenie obiektu wyliczenia i polecenia. To idiom polecany przez Joshuę Blocha w 30 pozycji Effective Java.

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();
}

Oczywiście możesz przekazać parametry do doCommand lub zwrócić typy.

To rozwiązanie może nie być odpowiednie, jeśli implementacje doCommand tak naprawdę nie „pasują” do typu wyliczenia, co jest - jak zwykle, gdy trzeba dokonać kompromisu - nieco niewyraźne.

jens
źródło
7

Miej wyliczenie poleceń:

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;
}

Jeśli masz więcej niż kilka poleceń, spójrz na użycie wzorca Command, zgodnie z odpowiedzią w innym miejscu (chociaż możesz zachować wyliczenie i osadzić wywołanie klasy implementującej w wyliczeniu, zamiast używać HashMap). Zobacz przykładową odpowiedź Andreasa lub Jensa na to pytanie.

JeeBee
źródło
5
dla każdego nowego polecenia, które dodajesz, musisz edytować przełącznik: ten kod nie jest zgodny z zasadą open / closed
dfa
Zależy od tego, czy poleceń jest niewiele, czy też wiele, prawda? Również ta strona jest obecnie tak przerażająco wolna, że ​​zmiana odpowiedzi zajmuje 5 prób.
JeeBee
to nie jest optymalne, zobacz stackoverflow.com/questions/1199646/… aby dowiedzieć się, jak to zrobić bardziej optymalnie.
Andreas Petersson
Tak, dziękuję za poświęcenie czasu na wdrożenie tego, co napisałem na dole mojego komentarza - Java Enum jako wzorzec polecenia. Gdybym mógł edytować swój post, wspomniałbym o tym, ale ta strona umiera.
JeeBee
Myślę, że to pytanie domaga się oświadczenia Switcha!
Michael Brown
7

Implementacja interfejsu, jak w prosty i przejrzysty sposób zademonstrowała dfa, jest przejrzysta i elegancka (oraz "oficjalnie" obsługiwana). Właśnie do tego służy koncepcja interfejsu.

W C # moglibyśmy użyć delegatów dla programistów, którzy lubią używać wskaźników funkcji w c, ale metoda DFA jest sposobem na użycie.

Możesz też mieć tablicę

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

Następnie możesz wykonać polecenie według indeksu

commands[7].exec();

Plagiat z DFA, ale posiadający abstrakcyjną klasę bazową zamiast interfejsu. Zwróć uwagę na cmdKey, który będzie używany później. Z doświadczenia wiem, że często polecenie sprzętowe ma również podkomendy.

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

Skonstruuj swoje polecenia w ten sposób,

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

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

Następnie możesz rozszerzyć ogólny HashMap lub HashTable, udostępniając funkcję zasysania pary klucz-wartość:

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);
    }
  }
}

Następnie stwórz swój magazyn poleceń:

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

Teraz możesz obiektywnie przesyłać kontrolki

commands.get("A").exec();
commands.get(condition).exec();
Błogosławiony Geek
źródło
+1 za wspomnienie o delegatach, na wypadek gdyby ktoś .NET zobaczył to pytanie i oszalał z interfejsami jednoprocesowymi. Ale tak naprawdę nie są one porównywalne ze wskaźnikami funkcji. Są bliżej obsługiwanej przez język wersji wzorca poleceń.
Daniel Earwicker,
5

Cóż, proponuję utworzyć obiekty poleceń i umieścić je w tablicy mieszającej, używając ciągu jako klucza.

keuleJ
źródło
3

Nawet jeśli uważam, że podejście wzorcowe jest bardziej ukierunkowane na najlepsze praktyki i możliwe do utrzymania w dłuższej perspektywie, oto jedna opcja dla Ciebie:

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

svachon
źródło
2

zazwyczaj staram się to rozwiązać w ten sposób:

public enum Command {

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

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

abstract void exec();
 }

ma to wiele zalet:

1) nie można dodać wyliczenia bez zaimplementowania exec. więc nie przegapisz A.

2) nie będziesz musiał nawet dodawać go do żadnej mapy poleceń, więc nie ma standardowego kodu do budowy mapy. tylko abstrakcyjna metoda i jej implementacje. (co jest zapewne również standardowym, ale nie będzie krótsze ..)

3) zaoszczędzisz wszystkie zmarnowane cykle procesora, przeglądając długą listę if lub obliczając hashCodes i wykonując wyszukiwania.

edycja: jeśli nie masz wyliczeń, ale ciągi znaków jako źródło, po prostu użyj Command.valueOf(mystr).exec()do wywołania metody exec. Zauważ, że musisz użyć modyfikatora public na execif, który chcesz wywołać z innego pakietu.

Andreas Petersson
źródło
2

Prawdopodobnie najlepiej będzie, jeśli użyjesz mapy poleceń.

Ale czy masz ich zestaw, aby sobie poradzić, skończysz z mnóstwem map. W takim razie warto przyjrzeć się temu, jak zrobić to z Enumami.

Możesz to zrobić za pomocą Enum bez używania przełączników (prawdopodobnie nie potrzebujesz metod pobierających w przykładzie), jeśli dodasz metodę do Enum w celu rozwiązania dla „value”. Następnie możesz po prostu zrobić:

Aktualizacja: dodano mapę statyczną, aby uniknąć iteracji przy każdym wywołaniu. Bezwstydnie uszczypnął tę odpowiedź .

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();
    }
}
Bogaty sprzedawca
źródło
2

Odpowiedź udzielona przez @dfa jest moim zdaniem najlepszym rozwiązaniem.

Podam tylko kilka fragmentów na wypadek, gdybyś korzystał z Java 8 i chciał używać Lambdas!

Polecenie bez parametrów:

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();

(możesz użyć Runnable zamiast Command, ale nie uważam tego za poprawne semantycznie):

Polecenie z jednym parametrem:

Jeśli spodziewasz się parametru, którego możesz użyć 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);

W powyższym przykładzie doSomethingXjest to metoda obecna w myObjklasie 's, która przyjmuje dowolny obiekt (nazwany paramw tym przykładzie) jako argument.

Marlon Bernardes
źródło
1

jeśli masz wiele wbudowanych instrukcji „if”, jest to wzorzec do korzystania z silnika reguł . Zobacz na przykład JBOSS Drools .

Pierre
źródło
0

gdyby można było mieć tablicę procedur (tak zwanych poleceń), które byłyby przydatne.

ale możesz napisać program do pisania kodu. To wszystko jest bardzo systematyczne, jeśli (wartość = 'A') commandA (); inaczej, jeśli (........................ itd

user147042
źródło
0

Nie jestem pewien, czy masz jakiekolwiek nakładanie się zachowań różnych poleceń, ale możesz również rzucić okiem na wzorzec Łańcucha odpowiedzialności , który może zapewnić większą elastyczność, umożliwiając wielu poleceniom obsługę niektórych wartości wejściowych.

tinyd
źródło
0

Wzorzec poleceń jest drogą do zrobienia. Oto jeden przykład użycia java 8:

1. Zdefiniuj interfejs:

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

2. Zaimplementuj interfejs z każdym rozszerzeniem:

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

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

i

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

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

i tak dalej .....

3. Zdefiniuj klienta:

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. A oto przykładowy wynik:

  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
źródło
-1

Jeśli robi wiele rzeczy, będzie dużo kodu, naprawdę nie można od tego uciec. Po prostu ułatw śledzenie, nadaj zmiennym bardzo znaczące nazwy, komentarze też mogą pomóc ...

znak
źródło