Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVC and Subject-Observer pattern in C++ & QT

Disclaimer:

As the first answers have duly noted, using MVC in the current example case is overkill. The goal of the question is to understand the underlying concepts, with a simple example, to be able to use them in a bigger program where more complex data (arrays, objects) is modified.


I am trying to implement the MVC pattern in C++ & QT, similar to the question here:

Other MVC Questions

The program has 2 line edits:

  • mHexLineEdit
  • mDecLineEdit

3 buttons

  • mConvertToHexButton
  • mConvertoDecButton
  • mClearButton

and just modifies strings.

enter image description here

The difference with the other question is that I am trying to implement the Subject/Observer pattern to update the View once the Model is changed.

Model.h

#ifndef MODEL_H
#define MODEL_H

#include <QString>
#include <Subject>

class Model : virtual public Subject
{
public:
    Model();
    ~Model();
    void convertDecToHex(QString iDec);
    void convertHexToDec(QString iHex);
    void clear();

    QString getDecValue() {return mDecValue;}
    QString getHexValue() {return mHexValue;}
private:
    QString mDecValue;
    QString mHexValue;
};
#endif // MODEL_H

Model.cpp

#include "Model.h"

Model::Model():mDecValue(""),mHexValue(""){}
Model::~Model(){}

void Model::convertDecToHex(QString iDec)
{
    mHexValue = iDec + "Hex";

    notify("HexValue");
}

void Model::convertHexToDec(QString iHex)
{
    mDecValue = iHex + "Dec";

    notify("DecValue");
}

void Model::clear()
{
  mHexValue = "";
  mDecValue = "";

  notify("AllValues");
}

View.h

#ifndef VIEW_H
#define VIEW_H

#include <QtGui/QMainWindow>
#include "ui_View.h"
#include <Observer>

class Controller;
class Model;
class View : public QMainWindow, public Observer
{
    Q_OBJECT

public:
    View(QWidget *parent = 0, Qt::WFlags flags = 0);
    ~View();
    void setController(VController* iController);
    void setModel(VModel* iModel);
    QString getDecValue();
    QString getHexValue();
public slots:
    void ConvertToDecButtonClicked();
    void ConvertToHexButtonClicked();
    void ClearButtonClicked();
private:

    virtual void update(Subject* iChangedSubject, std::string iNotification);

    Ui::ViewClass ui;

    Controller*  mController;
    Model*        mModel;
};

#endif // VIEW_H

View.cpp

#include "View.h"
#include "Model.h"
#include "Controller.h"
#include <QSignalMapper>

VWorld::VWorld(QWidget *parent, Qt::WFlags flags)
: QMainWindow(parent, flags)
{
    ui.setupUi(this);

    connect(ui.mConvertToHexButton,SIGNAL(clicked(bool)),this,SLOT(ConvertToHexButtonClicked()));
    connect(ui.mConvertToDecButton,SIGNAL(clicked(bool)),this,SLOT(ConvertToDecButtonClicked()));
    connect(ui.mClearButton,SIGNAL(clicked(bool)),this,SLOT(ClearButtonClicked()));
}

View::~View(){}

void View::setController(Controller* iController)
{
    mController = iController;

    //connect(ui.mConvertToHexButton,SIGNAL(clicked(bool)),this,SLOT(mController->OnConvertToHexButtonClicked(this)));
    //connect(ui.mConvertToDecButton,SIGNAL(clicked(bool)),this,SLOT(mController->OnConvertToDecButtonClicked(this)));
    //connect(ui.mClearButton,SIGNAL(clicked(bool)),this,SLOT(mController->OnClearButtonClicked(this)));
}

void View::setModel(Model* iModel)
{
    mModel = iModel;

    mModel->attach(this);
}

QString View::getDecValue()
{
    return ui.mDecLineEdit->text();
}

QString View::getHexValue()
{
    return ui.mHexLineEdit->text();
}

void View::ConvertToHexButtonClicked()
{
    mController->OnConvertToHexButtonClicked(this);
}

void View::ConvertToDecButtonClicked()
{
    mController->OnConvertToDecButtonClicked(this);
}

void VWorld::ClearButtonClicked() 
{
    mController->OnClearButtonClicked(this);
}

void View::update(Subject* iChangedSubject, std::string     iNotification)
{
    if(iNotification.compare("DecValue") == 0)
    {
        ui.mDecLineEdit->setText(mModel->getDecValue());
    }
    else if(iNotification.compare("HexValue") == 0)
    {
        ui.mHexLineEdit->setText(mModel->getHexValue());
    }
    else if(iNotification.compare("AllValues") == 0)
    {
        ui.mDecLineEdit->setText(mModel->getDecValue());
        ui.mHexLineEdit->setText(mModel->getHexValue());
    }
    else
    {
        //Unknown notification;
    }
}

Controller.h

#ifndef CONTROLLER_H
#define CONTROLLER_H

//Forward Declaration
class Model;
class View;

class Controller 
{
public:
    Controller(Model* iModel);
    virtual ~Controller();
    void OnConvertToDecButtonClicked(View* iView);
    void OnConvertToHexButtonClicked(View* iView);
    void OnClearButtonClicked(View* iView);
private:
    Model* mModel;
};
#endif // CONTROLLER_H

Controller.cpp

#include "Controller.h"
#include "Model.h"
#include "View.h"

Controller::Controller(Model* iModel):mModel(iModel){}

Controller::~Controller(){}

void Controller::OnConvertToDecButtonClicked(View* iView) 
{
  QString wHexValue = iView->getHexValue();

  mModel->convertHexToDec(wHexValue);
}

void Controller::OnConvertToHexButtonClicked(View* iView) 
{
  QString wDecValue = iView->getDecValue();

  mModel->convertDecToHex(wDecValue);
}

void Controller::OnClearButtonClicked(View* iView) 
{
  mModel->clear();
}

main.cpp

#include "View.h"
#include "Model.h"
#include "Controller.h"
#include <QtGui/QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Model wModel;
    View wView;
    Controller wCtrl(&wModel);
    wView.setController(&wCtrl);
    wView.setModel(&wModel);
    wView.show();
    return a.exec();
}

I can post the Subject/Observer files later if they become relevant.

Asides from general comments, can someone please answer these questions:

1) Would it be better to connect the buttons signals to the Controller slots directly (like in the portion commented out in View::setController)? The Controller needs to know which View called so it can use the proper information from the View doesn't it? This would mean either:

a) Reimplement a QSignalMapper or

b) Upgrade to Qt5 and VS2012 in order to connect directly with lambdas (C++11);

2) What is optimal way to know what has changed when update is called by the Model ? Is it a switch/looping through all possibilities, a predefined map... ?

3) Also, should I pass the necessary info through the update function, or let View check the required values the of Model once it gets notified ?

In the second case the View needs to access the Model data...


EDIT:

In particular in the case where there is a lot of data modified. In example, there is a load button and a whole object/array is modified. Passing a copy to the View through the signal/slot mechanism would be time consuming.

From ddriver's answer

Now, it would be a different matter if you have a traditional "list of items" model and your view is a list/tree/table, but your case is one of a single form.


4) Should the View need to have a reference to the Model ? since it only acts with on controller? (View::setModel())

If not, how does it register itself as an observer to the Model ?

like image 570
Smash Avatar asked Apr 05 '16 17:04

Smash


People also ask

Is MVC an Observer pattern?

In the case of MVC, the model is a subject and viewports are observers. See Section 4.2, "The Subject-Observer Pattern," for an overview and examples of this design pattern.

What is pattern in MVC?

The Model-View-Controller (MVC) is an architectural pattern which separates an application into three main groups of components: Models, Views, and Controllers. MVC is abbreviated as Model View Controller is a design pattern created for developing applications specifically web applications.

What is subject in Observer pattern?

In software design and engineering, the observer pattern is a software design pattern in which an object, named the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods.

What is the difference between mediator and Observer pattern?

Mediator (273) and Observer (293) are competing patterns. The difference between them is that Observer distributes communication by introducing Observer and Subject objects, whereas a Mediator object encapsulates the communication between other objects.


2 Answers

You are overthinking something practically trivial. You are also over-engineering.

Yes, it is always a good idea to abstract logic from UI, but in the case of your particular example, the extra abstraction layer of data is not necessary, mainly because you do not have different data sets, you only have two values which are really part of the logic and do not merit a data abstraction layer.

Now, it would be a different matter if you have a traditional "list of items" model and your view is a list/tree/table, but your case is one of a single form.

In your case, the proper design would be a Converter class which includes your current model data, controller and conversion logic, and a ConverterUI class, which is essentially your view form. You save on boilerplate code and component interconnect.

That being said, you are free to go through unnecessary lengths and for the overkill.

1 - you emit the modification data from the view to controller connection, so it will always come from the appropriate view, the controller is not concerned with which view it is, how many views there might be, or if there is a view at all. QSignalMapper is an option, but it is rather limited - it only supports a single parameter and only a few parameter types. I myself honestly prefer one line slots, they are more flexible and not all that hard to write, plus they are reusable code, which sometimes comes handy. Lambdas are a new a cool feature, and using those will sure make you look cooler, but in your particular case they won't make that much of a difference, and lambdas alone do not merit switching to Qt5. That being said, there are a whole lot more reasons to update to Qt5 besides lambdas.

2 - signals and slots, you know what you are editing, so you only update that

3 - passing the values through the signals is more elegant, and it doesn't require your controller to keep a reference to the view(s) and managing which view it is, as discussed in 1

4 - as it is obvious from the MVC diagram, the view has a reference to the model for reading only. So if you want a "by the book" MVC, that's what you need.

enter image description here

I have improved (somewhat, still untested) on the previous example, now there is Data which is just a regular struct, you definitely do not want it to be a QObject derived if you are going to have many of those, as QObject is huge memory overhead, the Model which maintains a data set, the Controller which iterates the underlying Model data set and reads and writes data, the View which is tied to a controller, and the App which brings together a model and two independent controllers for it and two independent views. There is limited functionality - you can go to the next available data set entry, modify or remove, there is no adding or reordering in this example, you could implement those as an exercise. Changes will propagate back down to the model, and thus be reflect in every controller and associated view. You can have multiple different views tied to a single controller. The model of a controller is currently fixed, but if you want to change it, you must go through a procedure similar to setting the controller for a view - that is, disconnect the old one before connecting to the new one, although if you are deleting the old one, it will automatically disconnect.

struct Data {
    QString d1, d2;
};

class Model : public QObject {
    Q_OBJECT
    QVector<Data> dataSet;
  public:
    Model() {
      dataSet << Data{"John", "Doe"} << Data{"Jane", "Doe"} << Data{"Clark", "Kent"} << Data{"Rick", "Sanchez"};
    }
    int size() const { return dataSet.size(); }
  public slots:
    QString getd1(int i) const { return i > -1 && i < dataSet.size() ? dataSet[i].d1 : ""; }
    QString getd2(int i) const { return i > -1 && i < dataSet.size() ? dataSet[i].d2 : ""; }
    void setd1(int i, const QString & d) {
      if (i > -1 && i < dataSet.size()) {
        if (dataSet[i].d1 != d) {
          dataSet[i].d1 = d;
          emit d1Changed(i);
        }
      }
    }
    void setd2(int i, const QString & d) {
      if (i > -1 && i < dataSet.size()) {
        if (dataSet[i].d2 != d) {
          dataSet[i].d2 = d;
          emit d2Changed(i);
        }
      }
    }
    void remove(int i) {
      if (i > -1 && i < dataSet.size()) {
        removing(i);
        dataSet.remove(i);
        removed();
      }
    }
  signals:
    void removing(int);
    void removed();
    void d1Changed(int);
    void d2Changed(int);
};

class Controller : public QObject {
    Q_OBJECT
    Model * data;
    int index;
    bool shifting;
  public:
    Controller(Model * _m) : data(_m), index(-1), shifting(false) {
      connect(data, SIGNAL(d1Changed(int)), this, SLOT(ond1Changed(int)));
      connect(data, SIGNAL(d2Changed(int)), this, SLOT(ond2Changed(int)));
      connect(data, SIGNAL(removing(int)), this, SLOT(onRemoving(int)));
      connect(data, SIGNAL(removed()), this, SLOT(onRemoved()));
      if (data->size()){
        index = 0;
        dataChanged();
      }
    }
  public slots:
    QString getd1() const { return data->getd1(index); }
    QString getd2() const { return data->getd2(index); }
    void setd1(const QString & d) { data->setd1(index, d); }
    void setd2(const QString & d) { data->setd2(index, d); }
    void remove() { data->remove(index); }
  private slots:
    void onRemoving(int i) { if (i <= index) shifting = true; }
    void onRemoved() {
      if (shifting) {
        shifting = false;
        if ((index > 0) || (index && !data->size())) --index;
        dataChanged();
      }
    }
    void ond1Changed(int i) { if (i == index) d1Changed(); }
    void ond2Changed(int i) { if (i == index) d2Changed(); }
    void fetchNext() {
      if (data->size()) {
        index = (index + 1) % data->size();
        dataChanged();
      }
    }
  signals:
    void dataChanged();
    void d1Changed();
    void d2Changed();
};

class View : public QWidget {
    Q_OBJECT
    Controller * c;
    QLineEdit * l1, * l2;
    QPushButton * b1, * b2, * bnext, * bremove;
  public:
    View(Controller * _c) : c(nullptr) {
      QVBoxLayout * l = new QVBoxLayout;
      setLayout(l);
      l->addWidget(l1 = new QLineEdit(this));
      l->addWidget(b1 = new QPushButton("set", this));
      connect(b1, SIGNAL(clicked(bool)), this, SLOT(setd1()));
      l->addWidget(l2 = new QLineEdit(this));
      l->addWidget(b2 = new QPushButton("set", this));
      connect(b2, SIGNAL(clicked(bool)), this, SLOT(setd2()));
      l->addWidget(bnext = new QPushButton("next", this));
      l->addWidget(bremove = new QPushButton("remove", this));
      setController(_c);
    }
    void setController(Controller * _c) {
      if (_c != c) {
        if (c) {
          disconnect(c, SIGNAL(d1Changed()), this, SLOT(updateL1()));
          disconnect(c, SIGNAL(d2Changed()), this, SLOT(updateL2()));
          disconnect(c, SIGNAL(dataChanged()), this, SLOT(updateForm()));
          disconnect(bnext, SIGNAL(clicked(bool)), c, SLOT(fetchNext()));
          disconnect(bremove, SIGNAL(clicked(bool)), c, SLOT(remove()));
          c = nullptr;
        }
        c = _c;
        if (c) {
          connect(c, SIGNAL(d1Changed()), this, SLOT(updateL1()));
          connect(c, SIGNAL(d2Changed()), this, SLOT(updateL2()));
          connect(c, SIGNAL(dataChanged()), this, SLOT(updateForm()));
          connect(bnext, SIGNAL(clicked(bool)), c, SLOT(fetchNext()));
          connect(bremove, SIGNAL(clicked(bool)), c, SLOT(remove()));
        }
      }
      updateForm();
    }
  public slots:
    void updateL1() { l1->setText(c ? c->getd1() : ""); }
    void updateL2() { l2->setText(c ? c->getd2() : ""); }
    void updateForm() {
      updateL1();
      updateL2();
    }
    void setd1() { c->setd1(l1->text()); }
    void setd2() { c->setd2(l2->text()); }
};

class App : public QWidget {
    Q_OBJECT
    Model m;
    Controller c1, c2;
  public:
    App() : c1(&m), c2(&m) {
      QVBoxLayout * l = new QVBoxLayout;
      setLayout(l);          
      l->addWidget(new View(&c1));          
      l->addWidget(new View(&c2));
    }
};
like image 195
dtech Avatar answered Oct 07 '22 08:10

dtech


I am going to answer this in the context of Passive-View and Model-View-Presenter

Model View Presenter

which (see Wikipedia)

is a derivation of the model–view–controller (MVC) architectural pattern, and is used mostly for building user interfaces.

The Model:

Changes to the Model / Subject must be observable. Most of the Subject / Observer details are handled by the signal / slot mechanism, so for this simple use case it is sufficient to make the Model observable by giving it a signal that emits the value. Because the online compilers don't support Qt, I will use boost::signals2 and std::string.

class Model
{
public:

    Model(  )
    {
    }

    void setValue( int value )
    {
        value_ = value;
        sigValueChanged( value_ );
    }

    void clear()
    {
        value_ = boost::none;
        sigValueChanged( value_ );
    }

    boost::optional<int> value() const
    {
        return value_;
    }

    boost::signals2::signal< void( boost::optional<int> ) > sigValueChanged;

private:

    boost::optional<int> value_;
};

The Presenter:

Here the Presenter is Observer, not the View. The Presenters job is to translate the integral value of the model into a textual representation for display. Here we really have two controller, one for the decimal notation and one for the hexadecimal notation. Although possibly over-designed for this simple case, we create an abstact base class for the Presenter.

class AbstractPresenter
{
public:

    AbstractPresenter()
        : model_( nullptr )
        , view_( nullptr )
    {
    }

    void setModel( Model& model )
    {
        model_ = &model;
        model.sigValueChanged.connect( [this]( int value ){
            _modelChanged( value ); } );
    }

    void setView( TextView& view )
    {
        view_ = &view;
    }

    void editChanged( std::string const& hex )
    {
        _editChanged( hex );
    }

private:

    virtual void _editChanged( std::string const& ) = 0;
    virtual void _modelChanged( boost::optional<int> ) = 0;

protected:

    Model *model_;
    TextView  *view_;
};

and an implementation for the decimal Presenter

class DecPresenter
    : public AbstractPresenter
{
    void _editChanged( std::string const& dec ) override
    {
        int value;
        std::istringstream( dec ) >> value;

        model_->setValue( value );
    }

    void _modelChanged( boost::optional<int> value ) override
    {
        std::string text;

        if( value )
        {            
            text = std::to_string( *value );;
        }

        view_->setEdit( text );
    }
};

and an implementation for the hexadecimal case.

class HexPresenter
    : public AbstractPresenter
{
    void _editChanged( std::string const& hex ) override
    {
        int value;
        std::istringstream( hex ) >> std::hex >> value;

        model_->setValue( value );
    }

    void _modelChanged( boost::optional<int> value ) override
    {
        std::string text;

        if( value )
        {
            std::stringstream stream;
            stream << std::hex << *value;

            text = stream.str();
        }

        view_->setEdit( text );
    }
};

And lastly an aggregated Presenter

class Presenter
{
public:

    Presenter()
        : model_( nullptr )
    {
    }

    void setModel( Model& model )
    {
        model_ = &model;
        hexPresenter.setModel( model );
        decPresenter.setModel( model );    
    }

    void setView( View& view )
    {
        hexPresenter.setView( view.hexView );
        decPresenter.setView( view.decView );    
    }

    HexPresenter hexPresenter;
    DecPresenter decPresenter;    

    void clear()
    {
        model_->clear();
    }

private:

    Model * model_;
};

The View:

The views only job displays a text value, so we can use the same View for both cases.

class TextView
{
public:

    TextView( std::string which )
        : which_( which )
    {
    }

    void setPresenter( AbstractPresenter& presenter )
    {
        presenter_ = &presenter;
    }

    void setEdit( std::string const& string )
    {
        std::cout << which_ << " : " << string << "\n";
    }

private:

    AbstractPresenter* presenter_;
    std::string which_;
};

And the aggregated view.

class View
{
public:

    View()
        : hexView( "hex" )
        , decView( "dec" )
    {
    }

    TextView hexView;
    TextView decView;
};

In a Qt application, each view would have a pointer to the corresponding label and it would set the text of the label.

    void setEdit( std::string const& string )
    {
        label->setText( QSting::fromStdString( string ) );
    }

In this context we can also answer Question 1.

1) Would it be better to connect the buttons signals to the Controller slots directly (like in the portion commented out in View::setController)?

Since we want a "Passive-View" without logic it is perfectly ok to connect directly to the controller if the controls parameters fit. If you have to convert, say a std::string to a QString, you can create a local slot that does the conversion and passes the value on or use lambda for the job in Qt5.

The Controller needs to know which View called so it can use the proper information from the View doesn't it?

No it does not. If it needs to do different things there should either be separate presenters or a presenter with separate methods for each case.

2) What is optimal way to know what has changed when update is called by the Model ? Is it a switch/looping through all possibilities, a predefined map... ?

The optimal way is for the model to tell the observer what changed. This can be done with different signals or with an event that contains the information. In this case there is only one value, so there is no difference.

3) Also, should I pass the necessary info through the update function, or let View check the required values the of Model once it gets notified ?

Pass the information to avoid redundant change calculations in the presenter.

4) Should the View need to have a reference to the Model ?

No, at least not in MVP.

The pieces can be put together like this:

int main()
{
    Model model;
    Presenter presenter;
    View view;

    presenter.setModel( model );
    presenter.setView( view );

    view.decView.setPresenter( presenter.decPresenter );
    view.hexView.setPresenter( presenter.hexPresenter );

    // simulate some button presses

    presenter.hexPresenter.editChanged( "42" );
    presenter.clear();
    presenter.decPresenter.editChanged( "42" );
}

which creates the following output

hex : 42
dec : 66
hex :
dec :
hex : 2a
dec : 42

Live on Coliru

like image 36
Thomas Avatar answered Oct 07 '22 09:10

Thomas