Nella creazione di un "registro", che è peggio: usando la riflessione o violando il principio aperto / chiuso?

5

Nel mio attuale corso di ingegneria del software, il mio team sta lavorando su un sistema di gestione della libreria che è essenzialmente un ambiente di riga di comando / REPL con una mezza dozzina di comandi, ad es. checkout , search , ecc. Abbiamo scelto di usare lo schema di comando per questo, quindi ogni comando sarà rappresentato come una classe. Ora, c'è la domanda su come creiamo nuovi oggetti comando quando l'utente inserisce una stringa.

  1. Usa condizioni:

    switch(input) {
        case "checkout":
            return new CheckoutCommand(args);
            break;
        // etc.
    
  2. Crea una stringa di mappatura HashMap ai costruttori:

    interface CommandCreator {
        abstract public Command createCommand(String[] args);
    }
    
    Map<String, CommandCreator> commandMap = new HashMap<>();
    commandMap.put("checkout", CheckoutCommand::new);
    commandMap.put("search", SearchCommand::new);
    // etc.
    
    return commandMap.get(input).createCommand(args);
    
  3. Usa riflessione:

    Class.forName("library.commands." + input).getConstructor(args.getClass()).newInstance(args);
    

    Le classi di comando avranno quindi gli stessi nomi delle stringhe che le invocano, cioè

    class checkout implements Command {
        // class body
    }
    
  4. Qualcos'altro.

Abbiamo convenuto che il # 1 non è la soluzione migliore. Alcuni di noi pensano che il # 2 non dovrebbe essere usato, perché viola il Principio Aperto / Chiuso; quando aggiungiamo più comandi in futuro, dovremmo modificare la classe che popola il comando HashMap. Alcuni di uso pensano che il # 3 non dovrebbe essere usato, perché la riflessione dovrebbe essere usata solo come ultima risorsa, e sembra troppo "hacky". Tuttavia, nessuno di noi ha idee migliori.

Dovremmo usare # 2 o # 3, o c'è una soluzione migliore?

    
posta The Guy with The Hat 06.07.2017 - 02:23
fonte

6 risposte

18

Il problema con il Principio Aperto / Chiuso è che il suo nome implica che si tratta di una guida generale che deve essere sempre seguita o che accadono cose brutte. Questo non è vero.

L'OCP è una linea guida : utile in alcune situazioni, e da qualche parte tra una distrazione e addirittura controproducente negli altri. È molto utile nel codice della libreria che è probabile che possa essere riutilizzato in più punti. È utile ai bordi dei moduli, consentendo di implementare le funzionalità nel posto giusto senza la necessità di coordinare gli autori dei moduli. Non ha posto nel codice di livello superiore di un'applicazione.

Semplicemente non è realistico aspettarsi di scrivere un'intera applicazione e quindi cambiarne il comportamento senza cambiare un singolo modulo esistente. Ad un certo livello, è necessario coordinare il modo in cui le nuove funzioni si integrano con l'esistente. In genere, questo viene fatto utilizzando un grafico oggetto che è configurato al più alto livello dell'applicazione.

Nel tuo caso, molto probabilmente significherebbe l'opzione 2 (sebbene l'opzione 1 non sia fuori questione, a mio avviso, e abbia il vantaggio di una semplicità diretta che potrebbe renderla attraente in alcune circostanze).

    
risposta data 06.07.2017 - 12:37
fonte
12
Command cmd = commandFactory.create(input, args);

Ora quale implementazione ho usato?

Non posso dirlo? Esatto, perché non ha importanza . Almeno, non importa finché hai un'astrazione responsabile della creazione del comando. Nascondere questa logica dietro questa interfaccia e semplicemente non importa quale implementazione si sceglie. Scegline uno che funzioni e se trovi qualcosa di cui essere doloroso, cambia le viscere di quella fabbrica. La tua squadra ha speso molto di più sul ciclismo su questo argomento che ne vale la pena.

    
risposta data 07.07.2017 - 13:06
fonte
3

Utilizza un metodo factory statico.

Dovrai comunque modificare il codice sorgente se introduci nuovi comandi, quindi non vedo il problema con il principio Open / Closed; è simile al # 1, ma farlo con un metodo statico nella classe base è più estensibile del # 1.

Si finirebbe con qualcosa di simile

public abstract class Command
{
    public static Command getCommand( String cmd, String[] args )
    {
        switch( cmd )
        {
            case "checkout":
                return new CheckoutCommand( args );
        }
    }
}

class CheckoutCommand extends Command
{
}

Command.getCommand( "checkout", [] );
    
risposta data 06.07.2017 - 07:14
fonte
2

Penso che # 4 qualcos'altro sia appropriato qui.

Progetterei questo sistema per utilizzare un file di configurazione che associa il nome del comando al nome della classe.

La configurazione potrebbe essere un file JSON, ad esempio:

{
  "commands": [
    {
      "name": "Checkout",
      "longCommand": "checkout",
      "shortCommand": "chk",
      "className": "library.commands.CheckoutCommandFactory",
      "documentation": "Checks-out a book from the library"
    },
    ...
  ]
}

Ecco la mia motivazione:

Hai ragione su # 1 e # 2 in caso di progettazione scadente.

Reflection non è una buona scelta qui perché stai cercando di mappare l'input dell'utente al nome della classe. Non dovresti nominare la tua classe come il comando dell'utente, perché i nomi delle classi non dovrebbero essere accoppiati all'interfaccia utente e non dovresti sovvertire le convenzioni di denominazione delle classi. In questo modo si lascia la manipolazione delle stringhe per provare a formare il nome della classe dall'input dell'utente (capitalizza il primo carattere e aggiungi "Comando"), ma ciò sarebbe complicato da comandi di più parole. Inoltre, se decidi di cambiare un comando, dovrai rifattorizzare i nomi delle classi. Questa tecnica non consente alias di comando e rende più difficile enumerare tutti i comandi. Cosa succede se qualcuno vuole aggiungere un comando al programma aggiungendo il proprio jar sul classpath - dovrebbero conoscere e seguire la complessa convenzione di denominazione. L'uso di un sistema di riflessione rigido è complesso e non è gestibile.

L'utilizzo di un file di configurazione ti consentirà di mappare facilmente i comandi alle classi, in modo sostenibile, e supporta molti casi d'uso e funzionalità che probabilmente vorrai.

Nota: questa soluzione potrebbe aggiungere una certa complessità al progetto, ma un buon compromesso se non si desidera aggiungere l'analisi dei file al progetto è definire una classe per rappresentare lo schema del file:

public class CommandDefinition {
    private final String name;
    private final String longCommand;
    private final String shortCommand;
    private final String className;
    private final String documentation;
}

È quindi possibile creare una raccolta in memoria di questi oggetti per evitare o rimandare il lavoro di analisi dei file (essenzialmente ricadendo sugli stessi vantaggi o svantaggi di # 2)

    
risposta data 06.07.2017 - 03:11
fonte
2

Non userei # 1, condizionali (switch): ogni volta che si apportano modifiche alla necessità di modificare la routine condizionale e ricompilare il processo che analizza i comandi e li esegue.

Vorrei usare una combinazione di Strategia e Builder per gestire la costruzione di ogni comando: ogni comando può avere il proprio processo di contruction incapsulato in una classe indipendente: il processo che esegue i comandi ha la stessa interfaccia per gestire ogni comando (questo è ciò che porta la strategia), ma ogni comando avrà il suo processo di costruzione (questo è ciò che Builder porta).

L'uso di HashMap o del dizionario va bene.

Avrei ogni Strategy / Builder in un componente indipendente, in modo che un metodo factory legga da un file di configurazione quale componente si riferisce a quale hash / comando e crea HashMap o Dictionary all'avvio, prima dell'esecuzione dei comandi iniziare.

Cordiali saluti, GEN

    
risposta data 07.07.2017 - 14:21
fonte
1

Le tue preoccupazioni riguardo al principio "aperto chiuso" nei tuoi suggerimenti non sono corrette. L'OCP non significa che non si può modificare alcuna implementazione. Significa che devi solo modificare le classi in cui viene trattata la semantica dei nuovi requisiti.

Quindi # 2 dei tuoi suggerimenti è la soluzione migliore per un registro.

Ma c'è un'altra possibilità: Inversion of control!

Quello che vuoi veramente è introdurre un nuovo "Comando" senza cambiare qualcosa da TE STESSO. Pertanto, è possibile utilizzare un framework che riconosca le nuove implementazioni e fornisca loro un meccanismo di iniezione delle dipendenze.

Esempio di primavera:

public interface CommandCreator { ... }


@Component
public class CommandCreator1 implements CommandCreator {
}

@Component
public class CommandCreator2 implements CommandCreator {
}

@Component
public class CommandCreator3 implements CommandCreator {
}


@Component
public class CommandCreatorRegistry {

    private Map<String, CommandCreator> map;

    public CommandCreatorRegistry(@AutoWired List<CommandCreator> commandCreators) {

       // register commands in map

    }

}

La primavera è solo un esempio per raggiungere questo obiettivo. Ma se la primavera potrebbe essere al di fuori del campo di applicazione del tuo progetto o troppo visto come troppo pesante, allora potresti andare con la tua soluzione.

    
risposta data 07.07.2017 - 12:50
fonte

Leggi altre domande sui tag