Existe uma maneira mais inteligente de fazer isso além de uma longa cadeia de declarações if ou switch?

20

Estou implementando um bot de IRC que recebe uma mensagem e estou verificando essa mensagem para determinar quais funções chamar. Existe uma maneira mais inteligente de fazer isso? Parece que rapidamente ficaria fora de controle depois que eu conseguia 20 comandos.

Talvez haja uma maneira melhor de abstrair isso?

 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
fonte
14
Que idioma é importante nesse nível de detalhe.
mattnz
3
@mattnz qualquer pessoa familiarizada com Java o reconhecerá no exemplo de código que ele fornecer.
jwenting
6
@ jwenting: Também é uma sintaxe C # válida, e aposto que existem mais idiomas.
Phresnel 13/06
4
@ phresnel sim, mas eles têm exatamente a mesma API padrão para String?
jwenting
3
@jwenting: É relevante? Mas mesmo se: É possível construir exemplos válidos, como, por exemplo, uma biblioteca auxiliar Java / C #, ou dar uma olhada no Java para .net: ikvm.net. A linguagem é sempre relevante. O questionador pode não estar procurando idiomas específicos, ele / ela pode ter cometido erros de sintaxe (acidentalmente convertendo Java em C #, por exemplo), novas linguagens podem surgir (ou surgiram no mundo selvagem, onde há dragões) - editar : Meus comentários anteriores foram para dicky, desculpe.
phresnel

Respostas:

45

Use uma tabela de expedição . Esta é uma tabela que contém pares ("parte da mensagem" pointer-to-function). O expedidor ficará assim (em pseudo código):

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

( equalsIgnoreCasepode ser tratado como um caso especial em algum lugar antes, ou se você tiver muitos desses testes, com uma segunda tabela de despacho).

Obviamente, o que pointer-to-functiondeve parecer depende da sua linguagem de programação. Aqui está um exemplo em C ou C ++. Em Java ou C #, você provavelmente utilizará expressões lambda para esse fim ou simulará "ponteiro para funções" usando o padrão de comando. O livro on-line gratuito " Higher Order Perl " tem um capítulo completo sobre tabelas de expedição usando Perl.

Doc Brown
fonte
4
O problema com isso, porém, é que você não poderá controlar o mecanismo de correspondência. No exemplo do OP, ele usa equalsIgnoreCasepara "agora jogando", mas toLowerCase().startsWithpara os outros.
mrjink
5
@ Mrjink: Eu não vejo isso como um "problema", é apenas uma abordagem diferente, com diferentes prós e contras. O "profissional" da sua solução: comandos individuais podem ter mecanismos de correspondência individuais. O meu "profissional": os comandos não precisam fornecer seu próprio mecanismo de correspondência. O OP precisa decidir qual solução melhor lhe convém. A propósito, eu também votei na sua resposta.
Doc Brown
1
FYI - "Ponteiro para funcionar" é um recurso da linguagem C # chamado delegados. O Lambda é mais um objeto de expressão que pode ser transmitido - você não pode "chamar" um lambda como pode chamar um delegado.
user1068
1
Tire a toLowerCaseoperação do circuito.
Zwol
1
@HarrisonNguyen: Eu recomendo docs.oracle.com/javase/tutorial/java/javaOO/… . Mas se você não estiver usando o Java 8, você pode simplesmente usar a definição de interface do mrink sem a parte "correspondências", que é 100% equivalente (é isso que eu quis dizer com "simular o ponteiro para a função usando o padrão de comando). comentário é apenas uma observação sobre os diferentes termos para diferentes variantes do mesmo conceito em diferentes linguagens de programação.
Doc Brown
31

Eu provavelmente faria algo assim:

public interface Command {
  boolean matches(String message);

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

Em seguida, todos os comandos podem implementar essa interface e retornar true quando corresponder à mensagem.

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
fonte
Se as mensagens nunca precisam ser analisadas em outro lugar (presumi que na minha resposta) isso é realmente preferível à minha solução, pois Commandé mais autocontida se souber quando deve ser chamada de si mesma. Produz uma pequena sobrecarga se a lista de comandos for enorme, mas provavelmente insignificante.
JHR
1
Esse padrão tende a se encaixar perfeitamente no paradygm de comando do console, mas apenas porque separa perfeitamente a lógica de comando do barramento de eventos e porque é provável que novos comandos sejam adicionados no futuro. Eu acho que deveria ser notado que esta não é uma solução para qualquer circunstância em que você tem uma longa cadeia de if ... elseif
Neil
+1 para usar um padrão de comando "leve"! Deve-se notar que, quando você está lidando com não-Strings, pode usar, por exemplo, enums inteligentes que sabem como executar sua lógica. Isso economiza até o loop for.
LastFreeNickname
3
Em vez do loop, poderíamos usar isso como um mapa se você fizer Command uma classe abstrata que substituições equalse hashCodeser o mesmo que o string que representa o comando
Cruncher
Isso é incrível e fácil de entender. Obrigado pela sugestão. É exatamente o que eu estava procurando e parece administrável para adição futura de comandos.
Harrison Nguyen
15

Você está usando Java - então faça bonito ;-)

Eu provavelmente faria isso usando anotações:

  1. Criar uma anotação de método personalizada

    @IRCCommand( String command, boolean perfectmatch = false )
  2. Adicione a anotação a todos os métodos relevantes da classe, por exemplo

    @IRCCommand( command = ".np", perfectmatch = true )
    doNP( ... )
  3. No seu construtor, use o Reflections para criar um HashMap de Métodos a partir de todos os Métodos anotados em sua classe:

    ...
    for (Method m : getDeclaredMethods()) {
    if ( isAnnotationPresent... ) {
        commandList.put(m.getAnnotation(...), m);
        ...
  4. No seu onMessageMétodo, basta fazer um loop commandListtentando combinar a String em cada uma e chamando method.invoke()onde ela se encaixa.

    for ( @IRCCommand a : commanMap.keyList() ) {
        if ( cmd.equalsIgnoreCase( a.command )
             || ( cmd.startsWith( a.command ) && !a.perfectMatch ) {
            commandMap.get( a ).invoke( this, cmd );
Falco
fonte
Não está claro que ele esteja usando Java, embora seja uma solução elegante.
214 Neil
Você está certo - o código apenas se parecia tanto com eclipse-auto-formatado Java-Code ... Mas você poderia fazer o mesmo com C # e C ++ com você poderia simular anotações com alguns macros inteligentes
Falco
Poderia muito bem ser Java, no entanto, no futuro, sugiro que você evite soluções específicas da linguagem se a linguagem não estiver claramente indicada. Apenas conselhos amigáveis.
214 Neil
Obrigado por esta solução - você estava certo de que estou usando Java no código fornecido, mesmo que não tenha especificado, isso parece muito bom e eu vou ter certeza de que vou tentar. Desculpe, não posso escolher duas melhores respostas!
Harrison Nguyen
5

E se você definir uma interface, diga IChatBehaviourqual possui um método chamado Executeque recebe um messagee um cmdobjeto:

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

No seu código, você implementa essa interface e define os comportamentos que deseja:

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

E assim por diante.

Na sua classe principal, você tem uma lista de comportamentos ( List<IChatBehaviour>) implementados pelo seu bot de IRC. Você pode substituir suas ifinstruções por algo assim:

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

O acima deve reduzir a quantidade de código que você possui. A abordagem acima também permitirá que você forneça comportamentos adicionais à sua classe de bot sem modificar a própria classe de bot (conforme a Strategy Design Pattern).

Se você deseja que apenas um comportamento seja acionado a qualquer momento, você pode alterar a assinatura do executemétodo para produzir true(o comportamento foi acionado) ou false(o comportamento não foi acionado ) e substituir o loop acima por algo assim:

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

O exemplo acima seria mais tedioso para implementar e inicializar, pois você precisa criar e passar todas as classes extras; no entanto, ele deve tornar seu bot facilmente extensível e modificável, já que todas as suas classes de comportamento serão encapsuladas e esperançosamente independentes uma da outra.

npinti
fonte
1
Para onde foram if? Ou seja, como você decide que um comportamento é executado para um comando?
mrjink
2
@ Mrjink: Desculpe meu mal. A decisão de executar ou não é delegada ao comportamento (omiti por engano a ifparte no comportamento).
Npinti
Acho que prefiro verificar se um dado IChatBehaviourpode manipular um determinado comando, pois ele permite que o chamador faça mais com ele, como processar erros se nenhum comando corresponder, embora seja realmente apenas uma preferência pessoal. Se isso não for necessário, não adianta complicar desnecessariamente o código.
Neil
você ainda precisa encontrar uma maneira para gerar a instância da classe correta, a fim de executá-lo, que ainda teria a mesma cadeia longa de ifs ou instrução switch enorme ...
jwenting
1

"Inteligente" pode ser (pelo menos) três coisas:

Maior desempenho

A sugestão da Tabela de expedição (e seus equivalentes) é boa. Essa tabela foi chamada de "CADET" nos últimos anos para "Não é possível adicionar; nem sequer tenta". No entanto, considere um comentário para ajudar um mantenedor iniciante sobre como gerenciar a referida tabela.

Manutenção

"Faça bonito" não é uma advertência ociosa.

e, muitas vezes esquecido ...

Resiliência

O uso do toLowerCase apresenta armadilhas, pois alguns textos em alguns idiomas precisam sofrer uma reestruturação dolorosa ao mudar entre magúsculo e minúsculo. Infelizmente, existem as mesmas armadilhas para o toUpperCase. Apenas esteja ciente.

Nós B marcianos
fonte
0

Todos os comandos podem ser implementados na mesma interface. Em seguida, um analisador de mensagens poderá retornar o comando apropriado que você executará apenas.

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 apenas mais código. Sim, você ainda precisa analisar a mensagem para saber qual comando executar, mas agora está em um ponto definido corretamente. Pode ser reutilizado em outro lugar. (Você pode injetar o MessageParser, mas isso é outra questão. Além disso, o padrão Flyweight pode ser uma boa idéia para os comandos, dependendo de quantos você espera que seja criado.)

jhr
fonte
Eu acho que isso é bom para dissociar e organizar o programa, embora eu não ache que isso lida diretamente com o problema de ter muitas declarações if ... elseif.
Neil
0

O que eu faria é o seguinte:

  1. Agrupe os comandos que você possui em grupos. (você tem pelo menos 20 no momento)
  2. No primeiro nível, categorize por grupo, para ter comandos relacionados ao nome de usuário, comandos de música, comandos de contagem, etc.
  3. Então você segue o método de cada grupo, desta vez você receberá o comando original.

Isso tornará isso mais gerenciável. Mais benefício quando o número de 'else if' cresce demais.

É claro que, às vezes, ter esse 'se mais' não seria um grande problema. Eu não acho que 20 é tão ruim assim.

InformedA
fonte