Basic Model-View-Controller structure question

I started trying to make a program that uses menus and displays stuff that the user can interact with (like a image they can move or zoom in/out of, for example). I quickly realized that it became a mess ("a blob" as I've seen other people talk about) because I have a bunch of nested if statements and states all entangled in my main's while loop after my main event loop.

Anyway... so I'm trying to figure out how to use the "Model-View-Controller (MVC)" architecture to still keep interacts between the user and program's data efficient while trying to separate out logic in my code. I want to do this with the minimal amount of redundant data as possible.

So, and I know this is a short example that probably doesn't show potential challenges or problems, but does this seem like a good start to using MVC? Am I "doing it right" so far?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <string>
#include <iostream>
class Model {
  public:
    void receiveInput(const std::string& input)
    {
        data = input;
    }
    const std::string& getData() const
    {
        return data;
    }
  private:
    std::string data;
};

class View {
  public:
    void displayData(const Model& model)
    {
        std::cout << "Displaying data: " << model.getData() << "!\n" << std::endl;
    }
};

class Controller {
  public:
    void getAndSendUserInput(Model& model)
    {
        std::cout << "Enter input: ";
        std::string input;
        std::cin >> input;
        model.receiveInput(input);
    }
};

#define USE_MODEL_VIEW_CONTROLLER
int main()
{
    Model      model;
    View       view;
    Controller controller;

    #ifdef USE_MODEL_VIEW_CONTROLLER
    while (true)
    {
        controller.getAndSendUserInput(model); // send User Input *to* the Model
        view.displayData(model); // Display data *from* the model.
    }
    
    #else
    // This is the equivalent program without using MVC:
    while (true)
    {
        std::cout << "Enter input: ";
        std::string data;
        std::cin >> data;
        std::cout << "Displaying data: " << data << "!\n" << std::endl;      
    }
    #endif // USE MODEL_VIEW_CONTROLLER
}


Also, it seems like this will eventually lead to me just making a huge class (Model) that contains all my state except for view information, and this huge state is passed to other functions, is that the end result?
and this huge state is passed to other functions, is that the end result?
Yes, this approach is way too naive to be practical.

What makes sense is to separate data from control. Such a control may be the user screen or a file.
> it seems like this will eventually lead to me just making a huge class (Model) that contains all my state
> except for view information, and this huge state is passed to other functions,

Model-View-Controller is an architectural pattern, and there are a variety of ways to implement it. In the classical (Smalltalk) MVC architecture, the model holds references to the views.

In any implementation of the MVC architecture, the controller and views would hold references to the model; so the model is never passed to explicitly to most member functions (C++: member functions other than constructors).

A a huge monolithic model class would be a bad idea; if the design slides into this, consider implementing the model as an aggregate.

An (unequivocally C++) implementation, based on the classical (Smalltalk) MVC architecture. (In this expository example, the model and the controller are not object-oriented, the model owns the views and the controller holds the model by value.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#include <iostream>
#include <string>
#include <vector>
#include <memory>

struct view_base // polymorphic class (object oriented)
{
    using pointer = std::unique_ptr<view_base> ;

    virtual ~view_base() = default ;
    virtual void render() const = 0 ;
    virtual void say_hello( std::string source ) const = 0 ; // only for exposition
};

struct model
{
    // data management, access and validation

    // number
    static constexpr int min_n = 0 ;
    static constexpr int max_n = 6 ;

    int number() const { return number_ ; }
    void number( int n ) { if( valid_number(n) ) number_ = n ; }
    void increment() { if( number_ < max_n ) { ++number_ ; update_views() ; } }
    void decrement() { if( number_ > min_n ) { --number_ ; update_views() ; } }
    static bool valid_number( int number ) { return number >= min_n && number <= max_n ; }

    // ...

    // TODO: persistence support
    std::istream& read( std::istream& stm ) ;
    std::ostream& write( std::istream& stm ) ;

    // associated views
    void add_view( view_base::pointer&& view )
    { views.push_back( std::move(view) ) ; views.back()->render() ; }

    const std::vector<view_base::pointer>& associated_views() const { return views ; }

    private:

        std::vector<view_base::pointer> views ; // in this variant, the model owns the associated views
        void update_views() { for( auto& p : views ) p->render() ; }

        int number_ = ( min_n + max_n ) / 2 ;

        // ...
};

struct controller
{
    void handle_input( char input )
    {
        if( input == '+' ) model_.increment() ;
        else if( input == '-' ) model_.decrement() ;
    }

    // the controller is typically responsible for creating the view(s) and associating them with the model
    template < typename VIEW, typename... CONSTRUCTOR_ARGS >
    void create_view( CONSTRUCTOR_ARGS&&... args )
    { model_.add_view( std::make_unique<VIEW>( model_, *this, std::forward<CONSTRUCTOR_ARGS>(args)... ) ) ; }

    // note: model_.associated_views() gives (non-mutable) access to the associated views:
    // for instance:
    void say_hello_to_all() const
    { for( const auto& view : model_.associated_views() ) view->say_hello( "controller" ) ; }

    private: model model_ ; // in this variant, holds the DefaultConstructible model by value
};

struct view : view_base
{
    view( model& m, controller& c ) : model_(m), controller_(c)  {}

    // retrieve the information from the model and display it
    virtual void render() const override { std::cout << "view: " << model_.number() << '\n' ; }

    virtual void say_hello( std::string source ) const override
    { std::cout << "view: '" << source << "' says hello\n" ; }


    protected:
        model& model_ ; // and to the model

        controller& controller_ ; // optional: the view holds a reference to its controller
        // this is required if the view directly accepts user input requesting a change
        // in the data and it needs to forward the request to the associated controller
};

int main()
{
    struct text_view : view
    {
        using view::view ;
        text_view( model& m, controller& c, std::string id ) : view(m,c), id(id) {}

        virtual void render() const override
        {
            static const char* const text[] = { "zero", "one", "two", "three", "four", "five", "six" } ;
            static constexpr int MAXN = sizeof(text) / sizeof( text[0] ) - 1 ;
            static_assert( model::min_n == 0 && model::max_n == MAXN, "stale code: model has changed" ) ;

            std::cout << "text_view (" << id << "): '" << text[ model_.number() ] << "'\n\n" ;
        }

        virtual void say_hello( std::string source ) const override
        { std::cout << "text_view (" << id << "): '" << source << "' says hello\n" ; }

        const std::string id = "noname" ;
    };

    controller controller ;
    controller.create_view<view>() ;
    controller.create_view<text_view>("daffy") ;

    for( char c : { '+', '+', '+', '?', '+', '-', '$', '-' } ) controller.handle_input(c) ;

    controller.say_hello_to_all() ;
}

http://coliru.stacked-crooked.com/a/825016dd69691b66
http://rextester.com/DEZG28439
Wow, I read this and I'm looking at your code bit by bit to try to digest it (so many references/pointers that all seem to be dependent on each other!). I might ask some more questions later, but for now:
JLBorges wrote:
The controller and views would hold references to the model; so the model is never passed to explicitly to most member functions
JLBorges wrote:
A a huge monolithic model class would be a bad idea; if the design slides into this, consider implementing the model as an aggregate.

Sorry but these two thoughts seem to conflict with each other, the only way I can see that the controller and views would hold a single reference to the model is if the model *is* one monolithic structure. I suppose that isn't necessary a problem as long as the the Model class itself is further broken down into smaller logical parts that minimally rely on each other. Is that basically what you mean by having aggregates?

Thanks for the example I'm going to try to understand it and apply it and research more about the different flavors of MVC, wasn't aware the patterns could differ so much. (Want to understand this before I make my program any more complex).
Last edited on
> I suppose that isn't necessary a problem as long as the the Model class itself is further broken down
>into smaller logical parts that minimally rely on each other. Is that basically what you mean by having aggregates?

Yes.
Topic archived. No new replies allowed.