Search code examples
c++user-inputencapsulation

Encapsulating user input of data for a class


For an assignment I've made a simple C++ program that uses a superclass (Student) and two subclasses (CourseStudent and ResearchStudent) to store a list of students and print out their details, with different details shown for the two different types of students (using overriding of the display() method from Student).

My question is about how the program collects input from the user of things like the student name, ID number, unit and fee information (for a course student) and research information (for research students):

My implementation has the prompting for user input and the collecting of that input handled within the classes themselves. The reasoning behind this was that each class knows what kind of input it needs, so it makes sense to me to have it know how to ask for it (given an ostream through which to ask and an istream to collect the input from).

My lecturer says that the prompting and input should all be handled in the main program, which seems to me somewhat messier, and would make it trickier to extend the program to handle different types of students.

I am considering, as a compromise, to make a helper class that handles the prompting and collection of user input for each type of Student, which could then be called on by the main program. The advantage of this would be that the student classes don't have as much in them (so they're cleaner), but also they can be bundled with the helper classes if the input functionality is required. This also means more classes of Student could be added without having to make major changes to the main program, as long as helper classes are provided for these new classes. Also the helper class could be swapped for an alternative language version without having to make any changes to the class itself.

What are the major advantages and disadvantages of the three different options for user input (fully encapsulated, helper class or in the main program)?


Solution

  • As mentioned by scv, it is normally better to decouple presentation (view) from internal structure (model).

    Here you have a typical case:

    • the Student class, root of a model hierarchy
    • the Displayer class, root of another independent hierarchy

    The issue with the display is that it varies according to two elements, which calls for a system of double dispatch (using virtual).

    This is traditionally solved using the Visitor Pattern.

    Let's check the base classes first:

    // student.h
    class Displayer;
    
    class Student
    {
    public:
      virtual ~Student();
      virtual void display(Displayer& d) const = 0; // display should not modify the model
    };
    
    // displayer.h
    class Student;
    class CourseStudent;
    class ResearchStudent;
    
    class Displayer
    {
    public:
      virtual ~Displayer();
    
      virtual void display(const Student& s) = 0; // default method for students
                                                  // not strictly necessary
      virtual void display(const CourseStudent& s) = 0;
      virtual void display(const ResearchStudent& s) = 0;
    };
    

    And now, let's implement some:

    // courseStudent.h
    #include "student.h"
    
    class CourseStudent: public Student
    {
    public:
      virtual void display(Displayer& d) const;
    
    };
    
    // courseStudent.cpp
    #include "courseStudent.h"
    #include "displayer.h"
    
    // *this has static type CourseStudent
    // so Displayer::display(const CourseStudent&) is invoked
    void CourseStudent::display(Displayer& d) const
    {
      d.display(*this);
    }
    
    
    // consoleDisplayer.h
    #include "displayer.h"
    
    class ConsoleDisplayer: public Displayer
    {
    public:
      virtual void display(const Student& s) = 0; // default method for students
                                                  // not strictly necessary
      virtual void display(const CourseStudent& s) = 0;
      virtual void display(const ResearchStudent& s) = 0;
    };
    
    // consoleDisplayer.cpp
    #include "consoleDisplayer.h"
    
    #include "student.h"
    #include "courseStudent.h"
    #include "researchStudent.h"
    
    void ConsoleDisplayer::display(const Student& s) { }
    
    void ConsoleDisplayer::display(const CourseStudent& s) { }
    
    void ConsoleDisplayer::display(const ResearchStudent& s) { }
    

    As you can see, the hard part is that if I wish to add a new derived class of Student, then I need to add a new virtual method in Displayer and override it in every derived class of Displayer... but otherwise it works great.

    The advantage is that the logic of display is now decoupled from the model, thus we can add new display logic without ever touching our model.