It's time now to take some of the features covered in this chapter and apply them to MovieMan
. The code listings here will, unfortunately, be incomplete in the interests of keeping the page count down. However, the complete implementation can be found in the Chapter03/MovieMan
directory of the downloadable source code.
The MovieMan
menus are implemented as a small set of classes: Menu
, MainMenu
, and DisplayMenu
. We could debate composition versus inheritance, or the use of classes to implement such simple menus, but my priority is to provide a demonstration of D features.
To get started, save a file called $MOVIEMAN/source/movieman/menu/menu.d
. At the very top, add the following lines:
module movieman.menu.menu; import std.stdio; import movieman.io;
Next, we'll make an empty class declaration that we'll fill in a little at a time:
class Menu { }
The Menu
class will have three private members:
private: string _header; string[] _items; bool _shouldExit;
_header
is the title of the menu and _items
is the text specifying the available actions, each of which will be displayed in a numbered list for the user to input a selection. _shouldExit
is a flag that will be set when a subclass is ready to exit the menu loop. Now we've got a few protected
functions, starting with the constructor:
protected: this(string header, string[] items) { _header = header; _items = items; }
A protected
constructor is normally used to allow subclasses access while preventing instances of the class from being instantiated outside the class scope. That's the exact behavior we want for Menu
, but it isn't necessary to make the constructor protected
in this case as the very next function is abstract
; I simply prefer to make all constructors in an abstract class protected
:
abstract void handleSelection(uint selection);
Subclasses will override handleSelection
in order to respond to the user's selection of a menu item. The next function, exitMenu
, is called when a subclass is ready to give up control and terminate the loop that prints the menu and reads the user's selection:
void exitMenu() { _shouldExit = true; }
The next two functions ensure that a title or number entered by the user is valid and print an "abort" message if not. The former only checks whether title is null
for now and the latter if the number is 0
. The label parameter is used in the error message:
bool validateTitle(string title) {…} bool validateNumber(uint number, string label) {…}
The last function in Menu
is the one that drives things:
public: final void run() { do { printList(_header, _items); auto selection = readUint(); if(!selection || selection > _items.length) writefln("Sorry, that's an invalid selection. Please try again."); else handleSelection(selection); } while(!_shouldExit); } }
The public:
keyword before the function declaration "turns off" the protected:
added previously. The final
in the declaration prevents subclasses from overriding run
. The implementation is a loop that begins with a call to movieman.io.printList
to display the header and the numbered menu items. It then asks the user to enter a number, which it sends to the subclass to handle if it is in the range of valid options (which is 1
to items.length
, inclusive), then the loop goes back up to the top and the menu is displayed again, unless exitMenu
is called.
Save a new file as $MOVIEMAN/source/movieman/menu/main.d
. Add the following lines to the top:
module movieman.menu.main; import movieman.menu.menu; final class MainMenu : Menu { }
Note that the class is declared final
. Given that there's no need to ever subclass MainMenu
, we can use this not just as a preventative measure, but also as an optimization hint. For example, the compiler can be sure that none of the member functions will ever be overridden and can safely inline calls to them. Now, inside MainMenu
:
private: enum Options : uint { addMovie = 1u, displayMovie, exit, } Menu _displayMenu;
The Options
enumeration will be used to determine the action to take in handleSelection
. The user can choose to add a new movie, display one or more movies, or exit the program. The _displayMenu
member is an instance of DisplayMenu
, declared as an instance of the Menu
base class. It is created in one of two private functions, onDisplayMovie
, when the user chooses to display a movie:
void onDisplayMovie() { import movieman.menu.display : DisplayMenu; if(_displayMenu is null) _displayMenu = new DisplayMenu; _displayMenu.run(); }
A local selective import is used since DisplayMenu
is not used anywhere else in the module. _displayMenu
is allocated only if it's null
, then its run
function is called. The second private function adds a movie to the database:
void onAddMovie() { import movieman.io; import std.stdio : writeln; auto title = readTitle(); if(!validateTitle(title)) return; auto caseNumber = readNumber("case"); if(!validateNumber(caseNumber, "case")) return; auto pageNumber = readNumber("page"); if(!validateNumber(pageNumber, "page")) return; if(readChoice("add this movie to the database")) writeln("Adding movie!"); else writeln(" Discarding new movie."); }
This function asks the user to enter a title, case number, and page number. If any one of these is invalid, the function returns without making any changes. Finally, it gives the user a chance to verify the information is correct via readChoice
. If the user approves, a message is printed saying the movie has been added. Later in the book, this will be changed to add a movie to the database. Next:
protected: override void handleSelection(uint selection) { final switch(cast(Options)selection) with(Options) { case addMovie: onAddMovie(); break; case displayMovie: onDisplayMovie(); break; case exit: exitMenu(); break; } }
This function uses a final switch
to cover every member of the Options
enumeration. with(Options)
is used as a convenience to avoid adding the namespace in every case
. Notice that there's no private function to handle the exit
option. Instead, the exitMenu
function implemented in the base class is called. Finally, the public
constructor:
public: this() { auto options = [ "Add Movie", "Display Movie(s)", "Exit" ]; super("Select one of the following actions.", options); } }
When a class has no default constructor, either implicit or explicit, then any subclasses must call one of the constructors that have been implemented. The MainMenu
class must call Menu
's sole constructor. Before doing so, an array of menu items is allocated as options
with an array literal. Using the literal directly in the constructor call would have been somewhat less readable. Finally, the Menu
constructor is called via super
.
This DisplayMenu
class will eventually print movie data to the screen and provide the option to edit movies in the database. The implementation is similar to the MainMenu
class, though the only function currently implemented is handleSelection
, which does the work of reading input and printing responses (there's no movie data yet to display or edit). Before looking at the implementation in the downloadable source, consider how you might implement DisplayMenu
, using MainMenu
as a guide.
3.147.80.100