Mappa delle funzioni vs istruzione switch


21

Sto lavorando a un progetto che elabora le richieste e ci sono due componenti per la richiesta: il comando e i parametri. Il gestore per ciascun comando è molto semplice (<10 righe, spesso <5). Ci sono almeno 20 comandi e probabilmente ne avranno più di 50.

Ho escogitato un paio di soluzioni:

  • un grande interruttore / if-else sui comandi
  • mappa dei comandi alle funzioni
  • mappa dei comandi per classi / singoli statici

Ogni comando esegue un piccolo controllo degli errori e l'unico bit che può essere estratto è il controllo del numero di parametri, che è definito per ciascun comando.

Quale sarebbe la migliore soluzione a questo problema e perché? Sono anche aperto a tutti i modelli di design che potrei aver perso.

Ho escogitato il seguente elenco pro / con per ciascuno:

interruttore

  • professionisti
    • mantiene tutti i comandi in un'unica funzione; poiché sono semplici, questo lo rende una tabella di ricerca visiva
    • non è necessario ingombrare la fonte con tonnellate di piccole funzioni / classi che verranno utilizzate solo in un posto
  • cons
    • molto lungo
    • difficile aggiungere comandi a livello di codice (necessità di concatenare utilizzando il caso predefinito)

comandi mappa -> funzione

  • professionisti
    • piccoli pezzi di dimensioni ridotte
    • può aggiungere / rimuovere comandi a livello di codice
  • cons
    • se fatto in linea, lo stesso visualmente dell'interruttore
    • se non fatto in linea, molte funzioni utilizzate solo in un posto

comandi mappa -> classe statica / singleton

  • professionisti
    • può usare il polimorfismo per gestire un semplice controllo degli errori (solo come 3 righe, ma comunque)
    • vantaggi simili a map -> soluzione funzionale
  • cons
    • molte classi molto piccole ingombreranno il progetto
    • l'implementazione non è tutto nello stesso posto, quindi non è facile analizzarla

Note extra:

Sto scrivendo questo in Go, ma non credo che la soluzione sia specifica della lingua. Sto cercando una soluzione più generale perché potrei aver bisogno di fare qualcosa di molto simile in altre lingue.

Un comando è una stringa, ma posso facilmente mapparlo su un numero, se conveniente. La firma della funzione è simile a:

Reply Command(List<String> params)

Go ha funzioni di livello superiore e altre piattaforme che sto considerando hanno anche funzioni di livello superiore, da cui la differenza tra la seconda e la terza opzione.


8
associare i comandi alle funzioni e caricarli in fase di esecuzione dalla configurazione. usa il modello di comando.
Steven Evers,

3
Non aver paura di creare molte piccole funzioni. Di solito, una raccolta di diverse piccole funzioni è più gestibile di una grande funzione.
Bart van Ingen Schenau,

8
Cosa c'è in questo mondo turbolento, in cui le persone rispondono alle domande nei commenti e chiedono maggiori informazioni nelle risposte?
pdr,

4
@SteveEvers: se non ha bisogno di elaborazione, è una risposta, non importa quanto breve. Se lo fa, e non hai tempo o altro, lascia che sia qualcun altro a rispondere (hai sempre voglia di imbrogliare per scrivere una risposta che confermi un commento che ha già una mezza dozzina di voti). Personalmente, ritengo che questo abbia bisogno di elaborazione, l'OP vuole davvero sapere perché la soluzione migliore è la soluzione migliore.
pdr,

1
@pdr - Giusto. La mia inclinazione era una mappa di comandi alle funzioni, ma sono un programmatore relativamente junior in un corso di progettazione CS. Al mio professore piacciono molte lezioni, quindi ci sono almeno 2 soluzioni legittime. Volevo sapere il preferito della community.
beatgammit

Risposte:


14

È perfetto per una mappa (seconda o terza soluzione proposta). L'ho usato decine di volte ed è semplice ed efficace. Non distinguo davvero tra queste soluzioni; il punto importante è che esiste una mappa con i nomi delle funzioni come tasti.

Il principale vantaggio dell'approccio cartografico, a mio avviso, è che la tabella è costituita da dati. Ciò significa che può essere passato in giro, aumentato o altrimenti modificato in fase di esecuzione; è anche facile codificare funzioni aggiuntive che interpretano la mappa in modi nuovi ed entusiasmanti. Ciò non sarebbe possibile con la soluzione case / switch.

Non ho davvero sperimentato i contro di cui parli, ma vorrei menzionare un ulteriore svantaggio: l'invio è facile se conta solo il nome della stringa, ma se devi prendere in considerazione ulteriori informazioni per decidere quale funzione eseguire , è molto meno pulito.

Forse non ho mai incontrato un problema abbastanza difficile, ma vedo poco valore sia nel modello di comando che nella codifica dell'invio come gerarchia di classi, come altri hanno già detto. Il nocciolo del problema è la mappatura delle richieste alle funzioni; una mappa è semplice, ovvia e facile da testare. Una gerarchia di classi richiede più codice e design, aumenta l'accoppiamento tra quel codice e può costringerti a prendere più decisioni in anticipo che probabilmente dovrai cambiare. Non credo che il modello di comando si applichi affatto.


4

Il tuo problema si presta molto bene al modello di progettazione Command . Quindi in pratica avrai Commandun'interfaccia di base e poi ci sarebbero più CommandImplclassi che implementerebbero quell'interfaccia. L'interfaccia deve essenzialmente avere un solo metodo doCommand(Args args). Puoi far passare gli argomenti tramite un'istanza di Argsclasse. In questo modo si fa leva sul potere del polimorfismo invece che su grosse dichiarazioni if ​​/ else. Anche questo design è facilmente estensibile.


3

Ogni volta che mi trovo a chiedere se dovrei usare un'istruzione switch o un polimorfismo in stile OO, mi riferisco al problema dell'espressione . Fondamentalmente, se hai "casi" diversi per i tuoi dati e bacchetta per supportare diverse "azioni" (in cui ogni azione fa qualcosa di diverso per ogni caso), è davvero difficile creare un sistema che ti consenta naturalmente di aggiungere sia nuovi casi che nuove azioni nel futuro.

Se usi le istruzioni switch (o il modello Visitor), è facile aggiungere nuove azioni (perché racchiudi tutto in una singola funzione) ma è difficile aggiungere nuovi casi (perché devi tornare indietro e modificare le vecchie funzioni)

Al contrario, se si utilizza il polimorfismo in stile OO è facile aggiungere nuovi casi (basta creare una nuova classe) ma è difficile aggiungere metodi a un'interfaccia (perché quindi è necessario tornare indietro e modificare un gruppo di classi)

Nel tuo caso hai solo un metodo che devi supportare (elaborare una richiesta) ma molti casi possibili (ogni comando diverso). Poiché semplificare l'aggiunta di nuovi casi è più importante dell'aggiunta di nuovi metodi, è sufficiente creare una classe separata per ciascun comando diverso.


A proposito, dal punto di vista di quanto siano estendibili le cose, non fa molta differenza se usi le classi o le funzioni. Se stiamo confrontando con un'istruzione switch ciò che conta è come le cose vengono "spedite" e sia le classi che le funzioni vengono inviate allo stesso modo. Pertanto, basta usare ciò che è più conveniente nella tua lingua (e poiché Go ha scopi e chiusure lessicali, la distinzione tra classi e funzioni è in realtà molto piccola).

Ad esempio, di solito è possibile utilizzare la delega per eseguire la parte di controllo degli errori invece di fare affidamento sull'ereditarietà (il mio esempio è in Javascript perché O non conosco la sintassi di Go, spero non ti dispiaccia)

function make_command(real_command){
    return function(x){
        if(check_arguments(x)){
            return real_command(x);
        }else{
            //handle error here
        }
    }
 }

 command1 = make_command(function(x){ 
     //do something
 })

 command2 = make_command(function(x){
     //do something else
 })

 command1(17);
 commnad2(42);

Ovviamente, in questo esempio si presuppone che esista un modo sensato di avere la funzione wrapper o gli argomenti di controllo della classe genitore per ogni caso. A seconda di come vanno le cose, potrebbe essere più semplice inserire la chiamata a check_arguments all'interno dei comandi stessi (poiché ogni comando potrebbe dover chiamare la funzione di controllo con argomenti diversi, a causa di un diverso numero di argomenti, diversi tipi di comando, ecc.)

tl; dr: non esiste un modo migliore per risolvere tutti i problemi. Dal punto di vista del "far funzionare le cose", concentrati sulla creazione delle tue astrazioni in modo da imporre importanti invarianti e proteggerti dagli errori. Da un punto di vista "a prova di futuro", tenere presente quali parti del codice hanno maggiori probabilità di essere estese.


1

Non ho mai usato go, come programmatore ac # probabilmente andrei sulla riga seguente, spero che questa architettura si adatti a ciò che stai facendo.

Vorrei creare una piccola classe / oggetto per ognuno con la funzione principale da eseguire, ognuno dovrebbe conoscere la sua rappresentazione di stringa. Questo ti dà la plug-in che ti sembra che tu voglia mentre il numero di funzioni aumenterà. Nota che non userei la statica a meno che tu non ne abbia davvero bisogno, non danno molto vantaggio.

Avrei quindi una fabbrica che sa scoprire queste classi in fase di esecuzione modificandole per essere caricate da diversi assemblaggi ecc. Ciò significa che non è necessario che siano tutte nello stesso progetto.

Quindi rende anche più modulare per i test e dovrebbe rendere il codice piacevole e piccolo, che è più facile da mantenere in seguito.


0

Come selezionate i vostri comandi / funzioni?

Se esiste un modo "intelligente" di selezionare la funzione corretta, allora è la strada da percorrere - significa che puoi aggiungere un nuovo supporto (forse in una libreria esterna) senza modificare la logica di base.

Inoltre, testare le singole funzioni è più semplice in isolamento rispetto a un'enorme istruzione switch.

Infine, essendo utilizzato solo in un posto, potresti scoprire che una volta arrivati ​​a 50 è possibile riutilizzare diversi bit di funzioni diverse?


I comandi sono stringhe uniche. Posso mappare questi numeri interi se necessario.
beatgammit,

0

Non ho idea di come funzioni Go, ma un'architettura che ho usato in ActionScript è quella di avere un Doubly Linked List che funge da catena di responsabilità. Ogni link ha una funzione determinResponsibility (che ho implementato come callback, ma puoi scrivere ogni link separatamente se funziona meglio per quello che stai cercando di fare). Se un collegamento determinasse la sua responsabilità, chiamerebbe meetResponsibility (di nuovo, un callback), e questo terminerebbe la catena. Se non fosse responsabile, passerebbe la richiesta al collegamento successivo nella catena.

È possibile aggiungere e rimuovere facilmente diversi casi d'uso semplicemente aggiungendo un nuovo collegamento tra i collegamenti (o alla fine di) della catena esistente.

Questo è simile, ma leggermente diverso, alla tua idea di una mappa di funzioni. La cosa bella è che hai appena passato la richiesta e fa la sua cosa senza che tu debba fare altro. Il lato negativo è che non funziona bene se è necessario restituire un valore.

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.