¿Hay una manera más inteligente de hacer esto además de una larga cadena de declaraciones if o switch?

20

Estoy implementando un bot IRC que recibe un mensaje y lo reviso para determinar a qué funciones llamar. ¿Hay alguna forma más inteligente de hacer esto? Parece que rápidamente se saldría de control después de que me dieron 20 comandos.

Tal vez hay una mejor manera de abstraer esto?

 public void onMessage(String channel, String sender, String login, String hostname, String message){

        if (message.equalsIgnoreCase(".np")){
//            TODO: Use Last.fm API to find the now playing
        } else if (message.toLowerCase().startsWith(".register")) {
                cmd.registerLastNick(channel, sender, message);
        } else if (message.toLowerCase().startsWith("give us a countdown")) {
                cmd.countdown(channel, message);
        } else if (message.toLowerCase().startsWith("remember am routine")) {
                cmd.updateAmRoutine(channel, message, sender);
        }
    }
Harrison Nguyen
fuente
14
Qué idioma es importante en este nivel de detalle.
mattnz
3
@mattnz cualquier persona familiarizada con Java lo reconocerá en el ejemplo de código que proporciona.
Jwenting
66
@jwenting: también es una sintaxis válida de C #, y apuesto a que hay más lenguajes.
phresnel
44
@phresnel sí, pero ¿tienen exactamente la misma API estándar para String?
Jwenting
3
@jwenting: ¿es relevante? Pero incluso si: Se pueden construir ejemplos válidos, como por ejemplo una Biblioteca de ayuda de interoperabilidad Java / C #, o echar un vistazo a Java para .net: ikvm.net. El lenguaje siempre es relevante. El interlocutor puede no estar buscando lenguajes específicos, puede haber cometido errores de sintaxis (convirtiendo accidentalmente Java a C #, por ejemplo), pueden surgir nuevos lenguajes (o haber surgido en la naturaleza, donde hay dragones) - editar : Mis comentarios anteriores fueron a dicky, lo siento.
phresnel

Respuestas:

45

Use una tabla de despacho . Esta es una tabla que contiene pares ("parte del mensaje", pointer-to-function). El despachador se verá así (en pseudocódigo):

for each (row in dispatchTable)
{
    if(message.toLowerCase().startsWith(row.messagePart))
    {
         row.theFunction(message);
         break;
    }
}

( equalsIgnoreCasese puede manejar como un caso especial en algún lugar antes, o si tiene muchas de esas pruebas, con una segunda tabla de despacho).

Por supuesto, lo que pointer-to-functiondebe verse depende de su lenguaje de programación. Aquí hay un ejemplo en C o C ++. En Java o C #, probablemente usará expresiones lambda para ese propósito, o simulará "puntero a funciones" usando el patrón de comando. El libro en línea gratuito " Higher Order Perl " tiene un capítulo completo sobre las tablas de despacho con Perl.

Doc Brown
fuente
44
Sin embargo, el problema con esto es que no podrá controlar el mecanismo de coincidencia. En el ejemplo de OP, él usa equalsIgnoreCasepara "jugar ahora" pero toLowerCase().startsWithpara los demás.
mrjink
55
@mrjink: No veo esto como un "problema", es solo un enfoque diferente con diferentes pros y contras. El "profesional" de su solución: los comandos individuales pueden tener mecanismos de coincidencia individuales. El "profesional" mío: los Comandos no tienen que proporcionar su propio mecanismo de correspondencia. El OP tiene que decidir qué solución le conviene más. Por cierto, también voté tu respuesta.
Doc Brown
1
FYI - "Puntero para funcionar" es una característica del lenguaje C # llamada delegados. Lambda es más un objeto de expresión que se puede pasar: no se puede "llamar" a un lambda como se puede llamar a un delegado.
user1068
1
Levante la toLowerCaseoperación fuera del bucle.
zwol
1
@HarrisonNguyen: recomiendo docs.oracle.com/javase/tutorial/java/javaOO/… . Pero si no está utilizando Java 8, puede usar la definición de interfaz de mrink sin la parte "coincide", eso es 100% equivalente (eso es lo que quise decir con "simular puntero a función utilizando el patrón de comando). Y user1068's comentario es sólo una observación acerca de los términos diferentes para diferentes variantes de un mismo concepto en diferentes lenguajes de programación.
Doc Brown
31

Probablemente haría algo como esto:

public interface Command {
  boolean matches(String message);

  void execute(String channel, String sender, String login,
               String hostname, String message);
}

Luego puede hacer que cada comando implemente esta interfaz y devuelva verdadero cuando coincida con el mensaje.

List<Command> activeCommands = new ArrayList<>();
activeCommands.add(new LastFMCommand());
activeCommands.add(new RegisterLastNickCommand());
// etc.

for (Command command : activeCommands) {
    if (command.matches(message)) {
        command.execute(channel, sender, login, hostname, message);
        break; // handle the first matching command only
    }
}
mrjink
fuente
Si los mensajes nunca necesitan ser analizados en otra parte (supuse que en mi respuesta) esto es realmente preferible a mi solución, ya que Commandes más autónomo si sabe cuándo llamarse a sí mismo. Produce una ligera sobrecarga si la lista de comandos es enorme, pero probablemente sea insignificante.
jhr
1
Este patrón tiende a encajar muy bien en el comando de consola, pero solo porque separa perfectamente la lógica de comando del bus de eventos y porque es probable que se agreguen nuevos comandos en el futuro. Creo que debe tenerse en cuenta que esta no es una solución para cualquier circunstancia en la que tenga una larga cadena de si ... de lo contrario
Neil
¡+1 por usar un patrón de comando "ligero"! Debe tenerse en cuenta que cuando se trata de no cadenas, puede utilizar, por ejemplo, enumeraciones inteligentes que saben cómo ejecutar su lógica. Esto incluso le ahorra el ciclo for.
LastFreeNickname
3
En lugar del bucle, podríamos usar esto como un mapa si hace que Command sea una clase abstracta que anule equalsy hashCodesea ​​igual a la cadena que representa el comando
Cruncher el
Esto es asombroso y fácil de entender. Gracias por la sugerencia. Es casi exactamente lo que estaba buscando y parece ser manejable para futuras adiciones de comandos.
Harrison Nguyen
15

Estás utilizando Java, así que hazlo hermoso ;-)

Probablemente haría esto usando Anotaciones:

  1. Crear una anotación de método personalizada

    @IRCCommand( String command, boolean perfectmatch = false )
  2. Agregue la anotación a todos los métodos relevantes en la clase, por ejemplo

    @IRCCommand( command = ".np", perfectmatch = true )
    doNP( ... )
  3. En su constructor, use Reflections para crear un HashMap de métodos a partir de todos los métodos anotados en su clase:

    ...
    for (Method m : getDeclaredMethods()) {
    if ( isAnnotationPresent... ) {
        commandList.put(m.getAnnotation(...), m);
        ...
  4. En su onMessageMétodo, solo haga un bucle para commandListtratar de hacer coincidir la Cadena en cada uno y llamar method.invoke()donde encaja.

    for ( @IRCCommand a : commanMap.keyList() ) {
        if ( cmd.equalsIgnoreCase( a.command )
             || ( cmd.startsWith( a.command ) && !a.perfectMatch ) {
            commandMap.get( a ).invoke( this, cmd );
Falco
fuente
No está claro que esté usando Java, aunque esa es una solución elegante.
Neil
Tiene razón: el código se parecía mucho al código Java con formato automático de eclipse ... Pero podría hacer lo mismo con C # y con C ++ podría simular anotaciones con algunas macros inteligentes
Falco
Bien podría ser Java, sin embargo, en el futuro, le sugiero que evite soluciones específicas del idioma si el idioma no está claramente indicado. Solo un consejo amistoso.
Neil
Gracias por esta solución, tenías razón en que estoy usando Java en el código proporcionado, aunque no lo especifiqué, esto se ve muy bien y me aseguraré de intentarlo. Lo siento, no puedo elegir dos mejores respuestas!
Harrison Nguyen
5

¿Qué pasa si define una interfaz, digamos IChatBehaviourcuál tiene un método llamado Executeque toma en messageun cmdobjeto?

public Interface IChatBehaviour
{
    public void execute(String message, CMD cmd);
}

En su código, entonces implementa esta interfaz y define los comportamientos que desea:

public class RegisterLastNick implements IChatBehaviour
{
    public void execute(String message, CMD cmd)
    {
        if (message.toLowerCase().startsWith(".register"))
        {
            cmd.registerLastNick(channel, sender, message);
        }
    }
}

Y así por el resto.

En su clase principal, tiene una lista de comportamientos ( List<IChatBehaviour>) que implementa su bot IRC. Entonces podría reemplazar sus ifdeclaraciones con algo como esto:

for(IChatBehaviour behaviour : this.behaviours)
{
    behaviour.execute(message, cmd);
}

Lo anterior debería reducir la cantidad de código que tiene. El enfoque anterior también le permitiría proporcionar comportamientos adicionales a su clase de bot sin modificar la clase de bot en sí (según el Strategy Design Pattern).

Si desea que solo se active un comportamiento a la vez, puede cambiar la firma del executemétodo para obtener true(el comportamiento se ha activado) o false(el comportamiento no se activó) y reemplazar el bucle anterior con algo como esto:

for(IChatBehaviour behaviour : this.behaviours)
{
    if(behaviour.execute(message, cmd))
    { 
         break;
    }
}

Lo anterior sería más tedioso de implementar e inicializar ya que necesita crear y pasar todas las clases adicionales, sin embargo, debería hacer que su bot sea fácilmente extensible y modificable ya que todas sus clases de comportamiento serán encapsuladas y, con suerte, independientes entre sí.

npinti
fuente
1
¿A dónde fueron los ifs? Es decir, ¿cómo decides que se ejecuta un comportamiento para un comando?
mrjink
2
@mrjink: Lo siento, mi mal. La decisión de si ejecutar o no se delega al comportamiento (omití por error la ifparte del comportamiento).
npinti
Creo que prefiero verificar si un determinado IChatBehaviourpuede manejar un comando dado, ya que permite que la persona que llama haga más con él, como errores de procesamiento si ningún comando coincide, aunque en realidad es solo una preferencia personal. Si eso no es necesario, entonces no tiene sentido complicar innecesariamente el código.
Neil
todavía se necesitaría una forma de generar la instancia de la clase correcta con el fin de ejecutarlo, lo que todavía tienen la misma cadena larga de FI o de instrucción switch masiva ...
jwenting
1

"Inteligente" puede ser (al menos) tres cosas:

Mayor rendimiento

La sugerencia de la tabla de despacho (y sus equivalentes) es buena. Tal tabla se llamaba "CADET" en años anteriores para "No se puede agregar; ni siquiera se intenta". Sin embargo, considere un comentario para ayudar a un mantenedor novato sobre cómo administrar dicha tabla.

Mantenibilidad

"Hacerlo hermoso" no es una advertencia ociosa.

y, a menudo pasado por alto ...

Resistencia

El uso de toLowerCase tiene dificultades en el sentido de que algunos textos en algunos idiomas deben sufrir una reestructuración dolorosa al cambiar entre magiscule y miniscule. Desafortunadamente, existen los mismos escollos para toUpperCase. Solo ten en cuenta.

Nosotros B marcianos
fuente
0

Puede hacer que todos los comandos implementen la misma interfaz. Luego, un analizador de mensajes podría devolverle el comando apropiado que solo ejecutará.

public interface Command {
    public void execute(String channel, String message, String sender) throws Exception;
}

public class MessageParser {
    public Command parseCommandFromMessage(String message) {
        // TODO Put your if/switch or something more clever here
        // e.g. return new CountdownCommand();
    }
}

public class Whatever {
    public void onMessage(String channel, String sender, String login, String hostname, String message) {
        Command c = new MessageParser().parseCommandFromMessage(message);
        c.execute(channel, message, sender);
    }
}

Parece que solo hay más código. Sí, aún necesita analizar el mensaje para saber qué comando ejecutar, pero ahora está en un punto definido correctamente. Puede ser reutilizado en otro lugar. (Es posible que desee inyectar el MessageParser, pero ese es otro asunto. Además, el patrón Flyweight podría ser una buena idea para los comandos, dependiendo de cuántos espera crear.)

jhr
fuente
Creo que esto es bueno para el desacoplamiento y la organización del programa, aunque no creo que esto aborde directamente el problema de tener demasiadas declaraciones if ... elseif.
Neil
0

Lo que haría es esto:

  1. Agrupe los comandos que tiene en grupos. (tienes al menos 20 en este momento)
  2. En el primer nivel, clasifique por grupo, de modo que tenga comandos relacionados con el nombre de usuario, comandos de canciones, comandos de conteo, etc.
  3. Luego ingresa al método de cada grupo, esta vez obtendrá el comando original.

Esto hará que esto sea más manejable. Más beneficio cuando el número de 'más si' crece demasiado.

Por supuesto, a veces tener estos 'si no' no sería un gran problema. No creo que 20 sea tan malo.

InformadoA
fuente