web-dev-qa-db-ja.com

OOP CLIメニュー構造の編成?

この問題は、アプリケーションの構造全体に影響を与えるため、現在取り組んでいるプロジェクトを続行できませんでした。この質問は here について簡単に触れられていますが、多くの人には見られなかったため、あまりよく答えられなかったように思います。

バイナリ検索ツリーのナビゲートと操作を担当するアプリケーションに取り組んでいます。このアプリケーションは、選択メニューを通じて制御され、いくつかのオプションはサブメニューにつながります。この問題に対する最もハックっぽいアプローチは次のようなものです:

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

良いコードを入力したい人は誰でも、メイン関数がこれほど長くて面倒なものになりたくありません。それを私のメインファイルにある他の関数に分割しても、あまり役に立ちません。それでも、厄介で、オブジェクト指向ではありません。可能な解決策は、このように各メニューを独自のクラスに配置することです。

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

間違いなく最もクリーンなメインメソッドですが、MainMenuメソッド内からツリーオブジェクトにアクセスできなくなったため、実行可能なオプションではありません。ひどいスタイルになるので、クラスメソッド内で無限に渡したくはありません。それで、ついに私が思いついた中立点はこのようなものでした。

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、私のサンプルコードはすべて邪魔になりません。私はそれらのどれも好きではないし、インターネットで本当により良い解決策を見つけていません。私は学部生なので、実務経験はありません。私が知らない業界で広く受け入れられているベストプラクティスはありますか?個人的にこれにどのように取り組みますか?これはC++で実装されているので、できるだけオブジェクト指向になるようにしています。しかし、あなたがそれを共有したいというあなたが本当にうまく機能していることを知っている機能的な方法があるなら、私はそれを受け入れるつもりです。

3
Jared

まず、C++について、OOPの使用を強制する(または推奨する)ことはまったくありません。これはよくある誤解です。他の言語はC++よりもはるかに優れたOOPに適しています。C++はOOPをサポートしていますが、他のパラダイムをはるかによくサポートしています。

とはいえ、あなたの問題は古典的であり、 コマンドパターン に適しています。これはサブクラス階層を使用せずに実装することもできますが、ここでは「クラシック」OOP実装に固執します。

コマンドは、一般的なcommand基本クラスのサブクラスです。

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

using command_ptr = std::unique_ptr<command>;

contextオブジェクトには、コマンドを実行するために必要な情報が含まれています。あなたの場合、これはBinarySearchTreeになる可能性があります。ですから、そうですdoこれを渡す必要があります。これを回避する方法はありません。実際、これは悪いことではありません。あなたが主張するように、それは確かに「ひどいスタイル」ではありません– まったく逆です!

これを実装する簡単なコマンドを次に示します。

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

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

これで、メニュー構造にはコマンドのリストが含まれ、ループで表示されます。簡略化:

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

今興味深いのはこれです:submenucommandのサブクラスであり、menuをラップできますクラス。このようにして、コマンドメニューを任意に深くエレガントにネストできます。

最後に、このメニューを使用するには、次のように初期化します。

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

これにより、いくつかのコマンドが作成されますが、そのうちの1つ(edit_submenu)はサブメニューで、menushowsを初期化します。

一言で言えば、それだけです。上記のコードにはC++ 14が必要です(実際には、C++ 11 + std::make_unique )。 C++ 03で実行するように簡単に書き換えることができますが、C++ 11の方が簡単です。

2つの接線の注意:

  1. iostreamsはインタラクティブな入力および出力用に設計されていません。それらを使用してそのようなインタラクティブなメニューを実装することは、ユーザー入力の特殊性にうまく対応できないため、うまく機能しません。残念ながら、私が知っているC++用の優れたCLIライブラリはありません。 Unixの世界におけるCLIの事実上の標準は GNU readlineライブラリ ですが、(a)C APIしか持っておらず、(b)ライセンスは何でも使用することを禁止していますしかしGNUライセンスされたソフトウェア。

    これには本当に良い解決策はありません。ただし、ほとんどのコマンドラインアプリケーションは、コマンドラインオプションとコマンドを優先して対話型CLIを避け、プログラムを呼び出すたびに、コマンドライン引数によって制御される1つのコマンド(またはいくつかの組み合わせ)を実行します。

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

    これはコマンドラインアプリケーションの従来のワークフローであり、簡単にスクリプト化でき、他のコマンドラインツールと組み合わせることができるため、ほとんどの点で優れています。

  2. 静的メニューの場合(つまり、サブメニュー項目の数が実行時に変化しない場合)は、 Boost.Variantライブラリ が上記のクラス階層の優れた代替手段になることは注目に値します。各メニューはboost::variant関連コマンドの。ただし、一般的な考え方は変わりません。

2
Konrad Rudolph

別のクラスのどこかに作成されたデータが必要な場合は、それを渡す必要があります。それは悪いスタイルではありません、そこに「厄介な」ものは何もありません、それはデータがプログラムでどのように使用されることになっている方法です。理想的には、コンストラクターで1回だけ渡します。

 MainMenu main_menu(tree);

そのツリーがクラスMainMenu内の多くの場所で必要な場合は、そのツリーへの参照のみをMainMenuのメンバー変数に格納します。たとえば、

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

これにより、あなたが書いたように、「クラスメソッド内で無限に渡す」必要がなくなります。

メニューコードでバイナリツリーへのアクセスを「織り込みすぎ」ないようにすることを検討してください。しかし、それが可能かどうか、またどのようにして可能かを伝えるには、実際にツリーを処理するプログラムの部分を調べる必要があります。したがって、私がこれまでに提供できる唯一のガイドラインは、ツリー上の実際のoperationsをメニュークラスから分離された関数に入れることです(多分BinarySearchTreeのメンバー関数として、おそらくメンバー) TreeManipulatorクラスのような関数は、それらの操作がどのように見えるかに依存します)。

1
Doc Brown