Esiste un modo più intelligente per farlo oltre a una lunga catena di istruzioni if ​​o switch?


20

Sto implementando un bot IRC che riceve un messaggio e sto controllando quel messaggio per determinare quali funzioni chiamare. C'è un modo più intelligente di farlo? Sembra che mi sfuggirebbe rapidamente di mano dopo essermi alzato a 20 comandi.

Forse c'è un modo migliore per astrarre questo?

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

14
Quale lingua, il suo tipo di importante a questo livello di dettaglio.
Mattnz,

3
Chiunque abbia familiarità con Java lo riconoscerà nell'esempio di codice che fornisce.
jwenting

6
@jwenting: è anche una sintassi C # valida e scommetto che ci sono più lingue.
phresnel,

4
@phresnel sì, ma quelli hanno esattamente la stessa API standard per String?
jwenting

3
@jwenting: è rilevante? Ma anche se: si possono costruire esempi validi, come ad esempio una libreria helper Java / C # -Interop, o dare un'occhiata a Java per .net: ikvm.net. La lingua è sempre pertinente. L'interrogante potrebbe non essere alla ricerca di lingue specifiche, potrebbe aver commesso errori di sintassi (conversione accidentale di Java in C #, ad es.), Potrebbero sorgere nuove lingue (o essersi diffuse in natura, dove ci sono draghi) - modifica : I miei precedenti commenti sono stati dicky, scusa.
galleria

Risposte:


45

Utilizzare una tabella di invio . Questa è una tabella contenente coppie ("parte del messaggio", pointer-to-function). Il dispatcher avrà quindi questo aspetto (in pseudo codice):

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

(il equalsIgnoreCasepuò essere gestito come un caso speciale da qualche parte prima, o se hai molti di questi test, con una seconda tabella di spedizione).

Naturalmente, ciò che pointer-to-functiondeve apparire dipende dal tuo linguaggio di programmazione. Ecco un esempio in C o C ++. In Java o C # probabilmente userete espressioni lambda a tale scopo, oppure simulerete "puntatore a funzioni" utilizzando il modello di comando. Il libro online gratuito " Ordine superiore Perl " ha un capitolo completo sulle tabelle di spedizione che utilizzano Perl.


4
Il problema con questo, tuttavia, è che non sarai in grado di controllare il meccanismo di abbinamento. Nell'esempio di OP, usa equalsIgnoreCaseper "giocare ora" ma toLowerCase().startsWithper gli altri.
Mrjink,

5
@mrjink: non lo vedo come un "problema", è solo un approccio diverso con pro e contro diversi. Il "pro" della tua soluzione: i singoli comandi possono avere meccanismi di corrispondenza individuali. Il mio "pro": i Comandi non devono fornire il proprio meccanismo di abbinamento. L'OP deve decidere quale soluzione gli si adatta meglio. A proposito, ho anche votato la tua risposta.
Doc Brown,

1
Cordiali saluti - "Puntatore a funzione" è una funzione del linguaggio C # chiamata delegati. Lambda è più un oggetto espressione che può essere passato in giro - non puoi "chiamare" un lambda come puoi chiamare un delegato.
user1068

1
Solleva l' toLowerCaseoperazione dal circuito.
zwol,

1
@HarrisonNguyen: raccomando docs.oracle.com/javase/tutorial/java/javaOO/… . Ma se non stai usando Java 8, puoi semplicemente usare la definizione dell'interfaccia di mrink senza la parte "match", che è equivalente al 100% (questo è ciò che intendevo per "simulare il puntatore alla funzione usando il modello di comando). E user1068's il commento è solo un'osservazione sui diversi termini per diverse varianti dello stesso concetto in diversi linguaggi di programmazione.
Doc Brown

31

Probabilmente farei una cosa del genere:

public interface Command {
  boolean matches(String message);

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

Quindi puoi avere tutti i comandi implementare questa interfaccia e restituire true quando corrisponde al messaggio.

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

Se i messaggi non hanno mai bisogno di essere analizzati altrove (ho ipotizzato che nella mia risposta) questo sia davvero preferibile alla mia soluzione perché Commandè più autonomo se sa quando essere chiamato se stesso. Produce un leggero sovraccarico se l'elenco dei comandi è enorme ma è probabilmente trascurabile.
jhr

1
Questo modello tende ad adattarsi bene al paradigma dei comandi della console, ma solo perché separa ordinatamente la logica dei comandi dal bus degli eventi e perché è probabile che nuovi comandi vengano aggiunti in futuro. Penso che dovrebbe essere notato che questa non è una soluzione per qualsiasi circostanza in cui hai una lunga catena di if ... elseif
Neil

+1 per l'utilizzo di un modello di comando "leggero"! Va notato che quando hai a che fare con le non stringhe puoi usare ad esempio enumerazioni intelligenti che sanno come eseguire la loro logica. Questo ti fa persino risparmiare il for-loop.
LastFreeNickname

3
Piuttosto che il ciclo, potremmo usarlo come una mappa se rendi Command una classe astratta che sovrascrive equalse hashCodesia la stessa della stringa che rappresenta il comando
Cruncher

Questo è fantastico e facile da capire. Grazie per il suggerimento È praticamente esattamente quello che stavo cercando e sembra essere gestibile per l'aggiunta futura di comandi.
Harrison Nguyen,

15

Stai usando Java - quindi rendilo bellissimo ;-)

Probabilmente lo farei usando Annotations:

  1. Creare un'annotazione del metodo personalizzata

    @IRCCommand( String command, boolean perfectmatch = false )
  2. Aggiungi l'annotazione a tutti i metodi pertinenti nella classe, ad es

    @IRCCommand( command = ".np", perfectmatch = true )
    doNP( ... )
  3. Nel tuo costruttore usa Reflections per creare una HashMap di metodi da tutti i metodi annotati nella tua classe:

    ...
    for (Method m : getDeclaredMethods()) {
    if ( isAnnotationPresent... ) {
        commandList.put(m.getAnnotation(...), m);
        ...
  4. Nel tuo onMessagemetodo, fai semplicemente un ciclo commandListcercando di abbinare la stringa su ognuno e chiamando method.invoke()dove si adatta.

    for ( @IRCCommand a : commanMap.keyList() ) {
        if ( cmd.equalsIgnoreCase( a.command )
             || ( cmd.startsWith( a.command ) && !a.perfectMatch ) {
            commandMap.get( a ).invoke( this, cmd );

Non è chiaro che stia usando Java, anche se questa è una soluzione elegante.
Neil,

Hai ragione: il codice assomigliava molto al codice Java con formattazione automatica dell'eclipse ... Ma potresti fare lo stesso con C # e con C ++ puoi simulare le annotazioni con alcuni macro intelligenti
Falco,

Potrebbe benissimo essere Java, tuttavia in futuro, suggerisco di evitare soluzioni specifiche per la lingua se la lingua non è chiaramente indicata. Solo un consiglio amichevole.
Neil,

Grazie per questa soluzione - avevi ragione sul fatto che sto usando Java nel codice fornito anche se non l'ho specificato, questo aspetto è davvero bello e sarò sicuro di provarlo. Mi dispiace non posso scegliere due risposte migliori!
Harrison Nguyen,

5

Che cosa succede se si definisce un'interfaccia, dire IChatBehaviourquale ha un metodo chiamato Executeche accetta in messagee un cmdoggetto:

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

Nel tuo codice, quindi implementare questa interfaccia e definire i comportamenti desiderati:

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

E così via per il resto.

Nella tua classe principale, hai quindi un elenco di comportamenti ( List<IChatBehaviour>) implementati dal tuo bot IRC. È quindi possibile sostituire le ifdichiarazioni con qualcosa del genere:

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

Quanto sopra dovrebbe ridurre la quantità di codice che hai. L'approccio sopra ti consentirebbe anche di fornire comportamenti aggiuntivi alla tua classe di bot senza modificare la stessa classe di bot (come da Strategy Design Pattern).

Se si desidera attivare un solo comportamento alla volta, è possibile modificare la firma del executemetodo in modo da produrre true(il comportamento è stato attivato) o false(il comportamento non è stato attivato) e sostituire il ciclo precedente con qualcosa del genere:

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

Quanto sopra sarebbe più noioso da implementare e inizializzare poiché è necessario creare e superare tutte le classi extra, tuttavia, dovrebbe rendere il bot facilmente estensibile e modificabile poiché tutte le classi di comportamento saranno incapsulate e, si spera, indipendenti l'una dall'altra.


1
Dove sono finite le ifs? Cioè, come si decide di eseguire un comportamento per un comando?
Mrjink,

2
@mrjink: scusami per il male. La decisione se eseguire o meno è delegata al comportamento (ho erroneamente omesso la ifparte nel comportamento).
npinti,

Penso di preferire il controllo sul fatto che un dato IChatBehaviourpossa gestire un determinato comando, poiché consente al chiamante di fare di più con esso, come l'elaborazione di errori se nessun comando corrisponde, anche se in realtà è solo una preferenza personale. Se ciò non è necessario, non ha senso complicare inutilmente il codice.
Neil,

avresti ancora bisogno di un modo per generare l'istanza della classe corretta per eseguirla, che avrebbe comunque la stessa lunga catena di if o un'enorme istruzione switch ...
jwenting

1

"Intelligente" può essere (almeno) tre cose:

Prestazioni più elevate

Il suggerimento di Dispatch Table (e suoi equivalenti) è valido. Un tavolo del genere era chiamato "CADET" negli anni passati per "Non posso aggiungere; non ci prova nemmeno". Tuttavia, considera un commento per aiutare un manutentore inesperto su come gestire detta tabella.

manutenibilità

"Make it beautiful" non è un ammonimento ozioso.

e, spesso trascurato ...

elasticità

L'uso di toLowerCase ha delle insidie ​​nel fatto che alcuni testi in alcune lingue devono subire una dolorosa ristrutturazione quando si passa da magiscule a miniscule. Sfortunatamente, esistono le stesse insidie ​​per toUpperCase. Basta essere consapevoli.


0

È possibile che tutti i comandi implementino la stessa interfaccia. Quindi un parser di messaggi potrebbe restituirti il ​​comando appropriato che eseguirai solo.

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

Sembra solo più codice. Sì, devi ancora analizzare il messaggio per sapere quale comando eseguire, ma ora si trova in un punto ben definito. Può essere riutilizzato altrove. (Potresti voler iniettare il MessageParser, ma questa è un'altra questione. Inoltre, il modello Flyweight potrebbe essere una buona idea per i comandi, a seconda di quanti ti aspetti di essere creato.)


Penso che questo sia utile per il disaccoppiamento e l'organizzazione del programma, anche se non penso che questo risolva direttamente il problema con troppe istruzioni if ​​... elseif.
Neil,

0

Quello che vorrei fare è questo:

  1. Raggruppa i comandi che hai in gruppi. (hai almeno 20 in questo momento)
  2. Al primo livello, categorizza per gruppo, in modo da avere il comando relativo al nome utente, i comandi dei brani, i comandi di conteggio, ecc.
  3. Quindi segui il metodo di ciascun gruppo, questa volta otterrai il comando originale.

Questo renderà questo più gestibile. Maggiori vantaggi quando il numero di "else if" aumenta troppo.

Certo, a volte avere questi "se altro" non sarebbe un grosso problema. Non credo che 20 sia così male.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.