organizzazione della struttura del menu OOP CLI?

3

Questo problema mi ha impedito di perseguire un progetto su cui sto lavorando perché influenza l'intera struttura dell'applicazione. Questa domanda è stata brevemente sfiorata qui , ma Sento che non è stato visto da molte persone e quindi non ha avuto una risposta molto buona.

Sto lavorando su un'applicazione che si occupa della navigazione e della manipolazione di un albero di ricerca binario. Questa applicazione è controllata tramite un menu di selezione, con alcune opzioni che conducono ai sottomenu. L'approccio più incisivo a questo problema è qualcosa del genere:

int main(int argc, char *argv[])
{
    // Create the object using the data in the file at path argv[1].
    BinarySearchTree tree(argv[1]);

    bool exit = false;
    while (!exit) {
        // Print the menu out here with std::cout statements.
        // This always adds several lines that don't do much.

        // Store the users input in a variable here.
        // Coding a way that ensures something valid is input adds
        // many more lines of clutter.

        // If-elseif-else or switch statement here. More complexity with more
        // submenus that look exactly like this here. You get how complex
        // this will get.
    }
}

Chiunque voglia digitare un buon codice non vuole che la sua funzione principale sia così lunga e disordinata. Rompiarlo in altre funzioni che si trovano nel mio file principale non aiuta molto, è ancora disordinato e non molto orientato agli oggetti. Una possibile soluzione sarebbe quella di inserire ciascun menu nella propria classe in questo modo.

int main(int argc, char *argv[])
{
    BinarySearchTree tree(argv[1]);
    MainMenu main_menu;
    main_menu.run();
}

Sicuramente il metodo main più pulito, ma non un'opzione praticabile dal momento che il mio oggetto tree non può più essere utilizzato dai metodi MainMenu, e sicuramente non voglio passarlo all'infinito nei miei metodi di classe dal momento che sarebbe stile terribile. Quindi alla fine ho trovato una via di mezzo.

int main(int argc, char *argv[])
{
    BinarySearchTree tree(argv[1]);

    MainMenu main_menu();
    bool exit = false;
    while (!exit) {
        unsigned long prompt = main_menu.prompt();

        // The switch statement here. Still a lot of complexity I don't like
        // because this can go on for quite a while depending on how deep my
        // submenus go or how much I need to do for specific commands.
    }
}

Ok, tutto il mio codice di esempio è fuori strada. Non mi piace nessuno di loro, e non ho trovato soluzioni veramente migliori su internet. Sono uno studente, quindi non ho alcuna esperienza pratica in questo. Esistono alcune best practice largamente accettate nel settore che non conosco? Come ti avvicineresti a questo personalmente? Ricorda, questo è in fase di implementazione in C ++, quindi sto cercando di essere il più possibile orientato agli oggetti. Tuttavia, se c'è un modo funzionale per sapere che funziona davvero bene che ti piacerebbe condividere, sarei aperto a questo.

    
posta Jared 08.11.2014 - 05:39
fonte

2 risposte

2

Prima di tutto, non c'è assolutamente nulla riguardo al C ++ che costringe (o addirittura incoraggia) a usare OOP. Questo è un malinteso comune. Altri linguaggi si prestano a OOP molto meglio del C ++, e mentre C ++ supporta OOP, supporta molto meglio altri paradigmi.

Detto questo, il tuo problema è classico e si presta bene al schema di comando . Vale la pena notare che questo può anche essere implementato senza l'uso di una gerarchia di sottoclassi, ma atteniamoci alla "classica" implementazione OOP qui.

Un comando è una sottoclasse della classe base generale command :

struct command {
    virtual std::string description() const = 0;
    virtual void run(context&) = 0;
    virtual ~command() = default;
};

using command_ptr = std::unique_ptr<command>;

L'oggetto context contiene le informazioni necessarie per soddisfare il comando. Nel tuo caso, potrebbe essere il BinarySearchTree . Quindi sì, fai devi passare questo. Non c'è modo pulito intorno a questo. In realtà, questa non è una brutta cosa: non è certamente uno "stile terribile" come reclamate: al contrario!

Ecco un semplice comando che implementa questo:

struct open_command : command {
    std::string description() const override {
        return "Open a file";
    }

    void run(context& context) override {
        // TODO implement.
    }
};

Ora la struttura del menu conterrà un elenco di comandi e li mostrerà in un loop. Semplificata:

struct menu {
    menu(context& context, std::vector<command>& commands)
        : context{context}, commands{commands} {}

    void show() {
        for (int i{}; i < commands.length(); ++i)
            show(i, commands[i].description());

        show(0, "Exit");

        int choice{};
        for (;;) {
            choice = input();
            if (choice == 0) return;
            if (choice > 0 and choice <= commands.length())
                break;
        }

        commands[choice - 1].run(context);
        // Don’t leave the menu:
        show();
    }

    // TODO:
    int choice() const;
    void show(int, std::string) const;

private:
    context& context;
    std::vector<command_ptr> commands;
};

Ora la cosa interessante è questa: un sottomenu sarebbe anche una sottoclasse di command , e potrebbe racchiudere la classe menu . In questo modo, puoi nidificare elegantemente in modo elegante i menu di comando.

Infine, per usare questo menu, avresti inizializzato così:

std::vector<command_ptr> cmds {
    make_unique<open_command>(),
    make_unique<save_command>(),
    make_unique<edit_submenu>(
        bst,
        std::vector<command_ptr>{
            make_unique<edit_delete_command>(),
            make_unique<edit_insert_command>(),
            …
        }
    ),
    …
};
menu menu{bst, cmds};
menu.show();

Questo crea alcuni comandi, di cui uno ( edit_submenu ) è un sottomenu, inizializza un menu e show s esso.

Questo è tutto, in poche parole. Il codice sopra richiede C ++ 14 (in realtà, solo C ++ 11 + std::make_unique ). Può essere facilmente riscritto per funzionare su C ++ 03, ma C ++ 11 è meno doloroso.

Due osservazioni tangenziali:

  1. iostream s non sono progettati per input e output interattivi. Usarli per implementare un tale menu interattivo funziona male, dal momento che non riescono a far fronte alle peculiarità dell'input dell'utente. Sfortunatamente non c'è una buona libreria CLI per C ++ che io conosca. Lo standard de facto per la CLI nel mondo Unix è la GNU readline library ma (a) ha solo un'API C e (b) la sua licenza ne proibisce l'uso in qualsiasi altro software di licenza GNU.

    Non c'è una soluzione veramente buona per questo. Tuttavia, la maggior parte delle applicazioni a riga di comando eviterebbe un'interfaccia CLI interattiva a favore di opzioni e comandi della riga di comando, in modo che ogni chiamata del programma eseguisse un comando (o una combinazione di più), controllato da argomenti della riga di comando:

    bst add entry my_database.file
    bst add another-entry my_database.file
    bst lookup entry my_database.file > output.file.
    

    Questo è il flusso di lavoro convenzionale delle applicazioni a riga di comando, ed è per lo più di livello superiore, poiché è banalmente programmabile e può essere combinato con altri strumenti da riga di comando.

  2. Vale la pena notare che, nel caso dei menu statici (cioè dove il numero di voci di sottomenu non cambia in fase di runtime) Boost.Variant offre un'alternativa superiore alla precedente gerarchia di classi. Ogni menu sarebbe un boost::variant dei comandi pertinenti. Tuttavia, l'idea generale rimane la stessa.

risposta data 12.11.2014 - 11:28
fonte
1

Se hai bisogno di dati creati da qualche parte in un'altra classe, devi passarlo. Non è un cattivo stile, non c'è nulla di "disordinato" in esso, è solo il modo in cui i dati devono essere utilizzati in un programma. Idealmente lo passi solo una volta nel costruttore:

 MainMenu main_menu(tree);

Se quell'albero è necessario in molti posti all'interno della classe MainMenu , puoi memorizzare solo un riferimento ad esso in una variabile membro di MainMenu , ad esempio

 class MainMenu
 {
     BinarySearchTree &m_tree;

     MainMenu(BinarySearchTree &tree)
     : m_tree(tree)
     { 
     }

     void run()
     {
         // do things with m_tree here, maybe pass it to a different sub menu
     }
  }

Questo evita la necessità di "passare senza fine all'interno dei tuoi metodi di classe", come hai scritto.

Potresti considerare di non "intrecciare" troppo l'accesso al tuo albero binario con il codice del menu. Ma per dirti se e come è possibile, devi guardare la parte del programma che effettivamente si occupa dell'albero. Quindi l'unica linea guida che posso darti finora è mettere le operazioni effettive sull'albero in funzioni che sono separate dalle tue classi di menu (forse come funzioni membro del tuo BinarySearchTree , forse funzioni membro di qualcosa come una classe TreeManipulator , dipende da come appaiono queste operazioni).

    
risposta data 08.11.2014 - 10:35
fonte

Leggi altre domande sui tag