This problem has kept me from pursuing a project I'm working on because it influences the entire structure of the application. This question has been briefly touched on here, but I feel that it wasn't seen by many people and therefore wasn't very well answered.
I'm working on an application in charge of navigating and manipulating a binary search tree. This application is controlled through a selection menu, with some options leading to submenus. The most hack-ish approach to this problem is something like this:
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.
}
}
Anyone who wants to type good code doesn't want their main function to be this long and messy. Breaking it into other functions sitting in my main file doesn't really help much either, it's still messy and not very object-oriented. A possible solution would be to put each menu into their own class like this.
int main(int argc, char *argv[])
{
BinarySearchTree tree(argv[1]);
MainMenu main_menu;
main_menu.run();
}
Definitely the cleanest main method, but not a viable option since my tree object can no longer be accessed from within the MainMenu methods, and I definitely don't want to pass it around endlessly within my class methods since that'd be terrible style. So finally some middle ground I came up with was like this.
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, all my sample code is out of the way. I don't like any of them, and I haven't found any truly better solutions on the internet. I'm an undergrad, so I don't have any practical experience in this. Is there some widely accepted best-practice in the industry that I don't know about? How would you approach this personally? Remember, this is being implemented in C++ so I'm trying to be as object-oriented as possible. However, if there's a functional way you know of that works really well that you'd like to share, I'd be open to it.
Best Answer
First off, there is absolutely nothing about C++ which forces (or even encourages) you to use OOP. This is a common misunderstanding. Other languages lend themselves to OOP much better than C++, and while C++ supports OOP, it supports other paradigms much better.
That said, your problem is classic, and lends itself well to the command pattern. It’s worth noting that this can also be implemented without the use of a subclass hierarchy but let’s stick to the “classical” OOP implementation here.
A command is a subclass of the general
command
base class:The
context
object contains the necessary information to fulfil the command. In your case, this could be theBinarySearchTree
. So yes, you do need to pass this around. There’s no clean way around this. In fact, this is not a bad thing: it’s certainly not “terrible style” as you claim – quite the opposite!Here is a simple command implementing this:
Now your menu structure would contain a list of commands and show them in a loop. Simplified:
Now the interesting thing is this: A submenu would also be a subclass of
command
, and could wrap themenu
class. That way, you can elegantly nest command menus arbitrarily deeply.Finally, to use this menu, you’d initialise it thusly:
This creates some commands, of which one (
edit_submenu
) is a submenu, initialises amenu
andshow
s it.That’s it, in a nutshell. The above code requires C++14 (actually, just C++11 +
std::make_unique
). It can easily be rewritten to run on C++03 but C++11 is just less painful.Two tangential remarks:
iostream
s are not designed for interactive input and output. Using them to implement such an interactive menu works badly, since they cannot cope well with the peculiarities of user input. Unfortunately there is no good CLI library for C++ that I know of. The de-facto standard for CLI in the Unix world is the GNUreadline
library but (a) it only has a C API, and (b) its license forbids its use in anything but GNU licensed software.There is no really good solution for this. However, most command line applications would eschew an interactive CLI in favour of command line options and commands, so that each invocation of your program would execute one command (or a combination of several), controlled by command line arguments:
This is the conventional workflow of command line applications, and it’s in most regards superior, since it’s trivially scriptable and can be combined with other command line tools.
It is worth noting that, for the case of static menus (i.e. where the number of submenu items doesn’t change at runtime) the Boost.Variant library offers a superior alternative to the above class hierarchy. Each menu would be a
boost::variant
of the relevant commands. However, the general idea remains the same.