Larga lista de declaraciones if en Java

101

Lo siento, no puedo encontrar una pregunta que responda a esto, estoy casi seguro de que alguien más la ha planteado antes.

Mi problema es que estoy escribiendo algunas bibliotecas del sistema para ejecutar dispositivos integrados. Tengo comandos que se pueden enviar a estos dispositivos a través de transmisiones de radio. Esto solo se puede hacer mediante texto. dentro de las bibliotecas del sistema, tengo un hilo que maneja los comandos que se ven así

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

El problema es que hay muchos comandos que rápidamente se convertirán en algo fuera de control. Horrible de mirar, doloroso de depurar y alucinante de entender en unos meses.

Steve
fuente
16
Solo un comentario: le recomiendo encarecidamente que elija el libro de patrones Gang of Four, o si es nuevo en los patrones, el libro Head First Design Patterns in Java (que es una lectura bastante fácil y una gran introducción a una serie de patrones comunes ). Ambos son recursos valiosos y ambos me han salvado el tocino más de una vez.
aperkins
2
Sí, de hecho los poseía, pero faltan :) Por eso estaba seguro de que lo que estaba haciendo estaba mal :) ¡Aunque no pude encontrar una solución correcta! Tal vez esto obtenga una buena posición en Google
Steve
2
¡Aquí es solo Command Pattern Monday!
Nick Veys

Respuestas:

171

usando el patrón de comando :

public interface Command {
     void exec();
}

public class CommandA() implements Command {

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

// etc etc

luego construye un Map<String,Command>objeto y complétalo con Commandinstancias:

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

entonces puede reemplazar su cadena if / else if con:

commandMap.get(value).exec();

EDITAR

también puede agregar comandos especiales como UnknownCommando NullCommand, pero necesita un CommandMapque maneje estos casos de esquina para minimizar los controles del cliente.

dfa
fuente
1
... con la verificación adecuada de que commandMap.get () no devuelve nulo :-)
Brian Agnew
3
por supuesto, he omitido un código repetitivo por simplicidad
dfa
10
En lugar de un HashMap, puede usar una enumeración de Java, que le brinda un conjunto bien definido de comandos en lugar de un mapa blando. Podría tener un getter en la enumeración: Command getCommand (); o incluso implementar exec () como un método abstracto en la enumeración, que implementa cada instancia (enum como comando).
JeeBee
2
esto obligará a implementar todos los comandos en la enumeración ... que está lejos de ser ideal. Con una interfaz, puede aplicar también el patrón Decorator (por ejemplo, DebugCommandDecorator, TraceCommandDecorator), hay mucha más flexibilidad incorporada en una interfaz Java simple
dfa
5
Ok, para un conjunto de comandos pequeño y que nunca crece, una enumeración es una solución viable.
dfa
12

Mi sugerencia sería una especie de combinación ligera de enumeración y objeto Command. Este es un modismo recomendado por Joshua Bloch en el artículo 30 de 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();
}

Por supuesto, puede pasar parámetros a doCommand o tener tipos de retorno.

Es posible que esta solución no sea realmente adecuada si las implementaciones de doCommand no "encajan" realmente en el tipo enum, que es, como es habitual cuando hay que hacer una compensación, un poco difuso.

jens
fuente
7

Tener una enumeración de comandos:

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

Si tiene más de unos pocos comandos, considere usar el patrón Command, como se respondió en otro lugar (aunque puede retener la enumeración e incrustar la llamada a la clase de implementación dentro de la enumeración, en lugar de usar un HashMap). Consulte la respuesta de Andreas o jens a esta pregunta para ver un ejemplo.

JeeBee
fuente
5
para cada nuevo comando que agregue, debe editar el conmutador: este código no sigue el principio de abierto / cerrado
dfa
Depende de si los comandos son pocos o muchos, ¿no es así? Además, este sitio es tan terriblemente lento en estos días que se necesitan 5 intentos para editar una respuesta.
JeeBee
esto no es óptimo, consulte stackoverflow.com/questions/1199646/… sobre cómo hacerlo de manera más óptima.
Andreas Petersson
Sí, gracias por dedicar el tiempo a implementar lo que escribí al final de mi comentario: Java Enum as Command Pattern. Si pudiera editar mi publicación, mencionaría esto, pero este sitio está muriendo.
JeeBee
¡Creo que esta pregunta está pidiendo a gritos una declaración de Switch!
Michael Brown
7

La implementación de una interfaz como la demuestra dfa simple y llanamente es limpia y elegante (y es compatible "oficialmente"). Para esto está destinado el concepto de interfaz.

En C #, podríamos usar delegados para programadores a los que les gusta usar punteros de función en c, pero la técnica de DFA es la forma de usar.

También podrías tener una matriz

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

Entonces podrías ejecutar un comando por índice

commands[7].exec();

Plagio de DFA, pero con una clase base abstracta en lugar de una interfaz. Observe la cmdKey que se usaría más tarde. Por experiencia, me doy cuenta de que con frecuencia un comando de equipo también tiene subcomandos.

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

Construye tus mandamientos así,

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

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

Luego, podría extender HashMap o HashTable genérico proporcionando una función de succión de par clave-valor:

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

Luego construye tu almacén de comandos:

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

Ahora puedes enviar controles de forma objetiva

commands.get("A").exec();
commands.get(condition).exec();
Bendito Geek
fuente
+1 por mencionar a los delegados en caso de que alguna persona .NET vea esta pregunta y se vuelva loca con las interfaces de un solo método. Pero realmente no son comparables a los punteros de función. Están más cerca de una versión compatible con el idioma del patrón de comando.
Daniel Earwicker
5

Bueno, sugiero crear objetos de comando y ponerlos en un mapa hash usando la Cadena como clave.

keuleJ
fuente
3

Incluso si creo que el enfoque del patrón de comando está más orientado a las mejores prácticas y se puede mantener a largo plazo, aquí hay una opción de una línea para usted:

org.apache.commons.beanutils.MethodUtils.invokeMethod (esto, "doCommand" + valor, nulo);

svachon
fuente
2

Normalmente trato de resolverlo de esa manera:

public enum Command {

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

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

abstract void exec();
 }

esto tiene muchas ventajas:

1) no es posible agregar una enumeración sin implementar exec. para que no te pierdas una A.

2) ni siquiera tendrá que agregarlo a ningún mapa de comando, por lo que no hay código repetitivo para construir el mapa. solo el método abstracto y sus implementaciones. (que podría decirse que también es repetitivo, pero no se acortará ...)

3) Ahorrará cualquier ciclo de CPU desperdiciado revisando una larga lista de if's o calculando hashCodes y haciendo búsquedas.

editar: si no tiene enumeraciones sino cadenas como fuente, solo use Command.valueOf(mystr).exec()para llamar al método exec. tenga en cuenta que debe usar el modificador público en execif al que desea llamar desde otro paquete.

Andreas Petersson
fuente
2

Probablemente sea mejor que utilice un mapa de comandos.

Pero si tienes un conjunto de estos para manejar, terminas con un montón de mapas dando vueltas. Entonces vale la pena intentarlo con Enums.

Puede hacerlo con un Enum sin usar modificadores (probablemente no necesite los getters en el ejemplo), si agrega un método al Enum para resolver el "valor". Entonces puedes simplemente hacer:

Actualización: se agregó un mapa estático para evitar la iteración en cada llamada. Desvergonzadamente pellizcado por esta respuesta .

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();
    }
}
Vendedor rico
fuente
2

La respuesta proporcionada por @dfa es la mejor solución, en mi opinión.

Solo proporciono algunos fragmentos en caso de que esté utilizando Java 8 y quiera usar Lambdas!

Comando sin parámetros:

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

(podría usar un Runnable en lugar de Command, pero no lo considero semánticamente correcto):

Comando con un parámetro:

En caso de que espere un parámetro, puede usar 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);

En el ejemplo anterior, doSomethingXhay un método presente en myObjla clase de que toma cualquier Objeto (nombrado paramen este ejemplo) como argumento.

Marlon Bernardes
fuente
1

si tiene varias declaraciones "if" imbricadas, entonces este es un patrón para usar un motor de reglas . Consulte, por ejemplo, JBOSS Drools .

Pierre
fuente
0

si fuera posible tener una serie de procedimientos (lo que llamó comandos) sería útil ...

pero podrías escribir un programa para escribir tu código. Todo es muy sistemático if (value = 'A') commandA (); más si (........................ etc

user147042
fuente
0

No estoy seguro de si tiene alguna superposición entre el comportamiento de sus diversos comandos, pero es posible que también desee echar un vistazo al patrón de Cadena de responsabilidad que podría proporcionar más flexibilidad al permitir que múltiples comandos manejen algunos valores de entrada.

tinyd
fuente
0

El patrón de comando es el camino a seguir. Aquí hay un ejemplo usando java 8:

1. Defina la interfaz:

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

2. Implemente la interfaz con cada una de las extensiones:

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

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

y

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

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

y así .....

3. Defina el cliente:

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. Y este es el resultado de la muestra:

  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
fuente
-1

Si hace muchas cosas, entonces habrá mucho código, realmente no puedes alejarte de eso. Simplemente hágalo fácil de seguir, asigne nombres muy significativos a las variables, los comentarios también pueden ayudar ...

marca
fuente