Реализация Model-View-Presenter в Qt

    Проектируя архитектуру одного проекта, остановился на паттерне MVP — подкупила возможность легко менять ui, а также простота покрытия тестами. Все примеры реализации MVP, что я нашёл в сети, были на C#. При реализации на Qt возникла пара неочевидных моментов, решение которых было успешно найдено. Собранная информация ниже.

    История MVP


    Как следует из [1] и [2], шаблон проектирования MVP является модификацией MVC, примененной впервые в компании IBM в 90-х годах прошлого века при работе над объектно-ориентированной операционной системой Taligent. Позднее MVP подробно описал Майк Потел.

    Чтобы увидеть, в чём же отличие MVP от MVC, можно посмотреть соответствующие параграфы [3]: MVC, MVP

    Реализация MVP в Qt


    В общем и целом, MVP уже и так применяется в Qt в неявном виде, как это показано в [4]:

    image

    Но такая реализация MVP имеет ряд недостатков:
    • невозможно создать несколько представлений для одного представителя (Presenter)
    • невозможно использовать паттерн Inversion of Control, описанный в [3] (тут), т.к. в данной схеме View генерируется автоматически и не может быть унаследовано от интерфейса

    Полная реализация MVP будет выглядеть так:

    image

    Отсюда диаграмма классов:

    image

    Рассмотрим каждый класс:
    • IView — интерфейсный класс, определяющий методы и сигналы, которые должен реализовывать конкретный View (см. Inversion of Control в [3])
    • Ui::View — класс, сгенерированный по .ui файлу Qt-дизайнера
    • View — конкретное представление, наследуемое от QWidget (или его потомка) и от IView. Содержит логику GUI, т.е. поведение объектов (например, их анимацию)
    • Model — модель данных предметной области (Domain Model); содержит переменные, флаги, модели таблиц и т.д.
    • Presenter — представитель; реализует взаимодействие между моделью и представлением. Также через него происходит взаимодействие с внешним миром, т.е. с основным приложением, с сервером (например, через слой служб приложения) и т.д.

    На этапе «рисования квадратиков» всё понятно. Проблемы у меня возникли при попытке реализации.
    1. При написании IView необходимо объявлять сигналы. Но для этого IView должен быть унаследован от QObject. Далее при попытке наследовать View одновременно от QWidget и IView возникала ошибка. Оказалось, что класс не может быть унаследован одновременно от двух QObject-объектов (не помогло даже виртуальное наследование). Как обойти эту проблему показано в [5]: ссылка. Таким образом, IView не наследуется от QObject и просто объявляет сигналы как полностью виртуальные (абстрактные) методы в секции public. Это вполне логично, т.к. сигнал — это тоже функция, по вызову которой происходит оповещение наблюдателей через их слоты (см. паттерн Observer).
    2. Еще одна проблема возникла при привязке сигналов IView в классе Presenter. Дело в том, что Presenter содержит ссылку на IView (конкретный View добавляется в Presenter на этапе выполнения, но хранится как IView — используется полиморфизм). Но для соединения сигналов и слотов используется статический метод QObject::connect(), принимающий в качестве объектов только наследников от QObject, а IView не является таковым. Но мы то знаем, что любой наш View будет им являться. Так что проблема решается динамическим приведением типов, как это показано в [6]:

      QObject *view_obj = dynamic_cast<QObject*>(m_view); // где m_view - IView
      QObject::connect(view_obj, SIGNAL(okActionTriggered()),
                       this, SLOT(processOkAction()));


    Также стоит заметить, что при реализации IView нужен только заголовочный (.h) файл. Если для IView создан .cpp файл — его необходимо удалить, иначе могут возникнуть проблемы при компиляции.

    В принципе, этой информации достаточно, чтобы самостоятельно реализовать MVP в Qt, но я приведу простой пример (только реализация MVP, без рассмотрения взаимодействия в контексте реального приложения).

    Простой пример использования MVP в Qt


    В архиве 3 примера, это одно и то же приложение, но с дополнениями в каждом примере.
    1. Реализация MVP
    2. Добавлена возможность создавать несколько представлений
    3. Полная синхронизация между представлениями при изменении данных

    Весь код комментирован в Doxygen-стиле, т.е. вы легко можете сгенерировать документацию с помощью Doxywizard.

    Скачать примеры можно тут

    Out of scope


    Вот список интересных вопросов по теме, которые не вошли в статью:
    • тестирование модулей полученной системы
    • реализация View в виде библиотек (с помощью QtPlugin) в контексте MVP
    • реализация View с помощью QtDeclarative (QML) в контексте MVP
    • передача представлений в Presenter посредством фабрики классов (таким образом можно легко менять стили)

    В комментариях приветствуется любая информация по этим вопросам.

    Источники


    1. http://www.rsdn.ru/article/patterns/generic-mvc2.xml
    2. http://en.wikipedia.org/wiki/Model-view-presenter
    3. http://www.rsdn.ru/article/patterns/ModelViewPresenter.xml
    4. http://thesmithfam.org/blog/2009/09/27/model-view-presenter-and-qt/
    5. http://doc.trolltech.com/qq/qq15-academic.html
    6. http://developer.qt.nokia.com/forums/viewthread/284
    Поделиться публикацией

    Похожие публикации

    Комментарии 17
      +2
      Статья неплохая, в примере с полной синхронизации надо будет поковыряться.

      Вопрос: а почему нельзя было сделать IView наследником сразу QWidget? Реализация некоторых QMainWindow- и QDialog-специфичных вещей вручную — не проблема.
      В этом случае сразу решается проблема с QtPlugin, например.

      Про QML я бы с интересом почитал, как раз думаю о возможной реализации MVC с небольшой логикой, зашитой сразу в QML-файл.

      > передача представлений в Presenter посредством фабрики классов (таким образом можно легко менять стили)
      Стили обычно меняются при помощи QApplication::setStyle(). Или это про другие стили?
        +1
        >Вопрос: а почему нельзя было сделать IView наследником сразу QWidget?
        Почему же нельзя, можно. Но таким образом вы «скажете», что все представления должны быть наследниками от QWidget. Если это подходит для конкретного проекта — то это нормально. Но если представление должно быть веб-страницей или консолью — то не покатит.

        >Стили обычно меняются при помощи QApplication::setStyle(). Или это про другие стили?
        Я имел ввиду общий вид приложения, т.е. фабрика может выдавать совершенно разные формы, главное чтобы они наследовались от интерфейсного класса
          0
          Я думал, вы говорите об различных itemview, а идея совсем в другом :)
          Что ж, браво!
          +1
          QML ещё слишком сырой, я пытался его использовать на новом проекте — зря потратил время. Там не реализован ни один стандартный контрол, писать же всё с нуля интересно с точки зрения изучения QML, но абсолютно непрактично. К тому же там отсутствуют некоторые важные моменты. В общем, не зря он всё ещё в лаборатории Qt значится. Пока что на нём хорошо получаются только интерфейсы а-ля «кавайный твиттер или фликер».
            0
            Так туда же можно встраивать обычные QWidget. Не в дизайнере, но ручками.
            И из Labs его вытащили в QtDeclarative. А дизайнер нормальный уже на подходе, его обещали в qt-creator-2.1.
              0
              Судя по cahnge-log 4.7.1, работа над QML в разгаре (http://qt.nokia.com/developer/changes/changes-4.7.1/). Используем QML в проекте, полёт нормальный. Облом был один раз, когда они выпилили эффекты, но через qmlRegisterType() эффекты легко вернулись. Таким же образом можно и стандартные виджеты передавать из C++ в QML. Всё же для новых проектов действительно не рекомендовал бы его применять — думаю еще пол-годика надо подождать, пока допилят.
                0
                Я собираюсь дождаться QtCreator 2.1, а там ещё раз пощупать. Но да, вы правы, ещё минимум полгода до нормального состояния.
            –1
            Вот здорово. Спасибо большое. На C# уже давненько применяем MVP а по QT пока ничего не встречал. Если есть возможность пожалуйста на примере каком-нибудь по-подробнее ещё несколько статеек.
              0
              Подскажите старику — а почему для связи view и presenter используются не сигналы и слоты, а передача указателя на интерфейс? Выбор чем-то обусловлен, или просто «так больше нравится»?
                0
                Пришлось бы создавать в Presenter-е дополнительные публичные слоты и сигналы, и коннектить View и Presenter в контексте вашего приложения. Также объект приложения должен будет ЗНАТЬ, как именно должны связываться View и Presenter. Мне кажется, это было бы не так удобно.
                  0
                  Пришлось бы создавать в Presenter-е дополнительные публичные слоты и сигналы, и коннектить View и Presenter в контексте вашего приложения.


                  Это да. Но ведь объем кода для описания интерфейса и для организации сигналов — одинаков? Зато сигналы по определению мультикаст — к одному презентеру из коробки можно подключить много view. А в вашем случае придется делать список указателей на view и вызывать их вручную.

                  Также объект приложения должен будет ЗНАТЬ, как именно должны связываться View и Presenter. Мне кажется, это было бы не так удобно.


                  А в текущем приложении разве не то же самое происходит когда указатель на view передается в презентер?
                    0
                    > Но ведь объем кода для описания интерфейса и для организации сигналов — одинаков?

                    Ну можно обойтись вообще без IView, да. Но тогда увеличится связанность, т.е. презентеру придется знать о конкретном View, а не об интерфейсе. IView для этого и применен, для уменьшения связанности. Подробней можно прочесть тут: www.rsdn.ru/article/patterns/ModelViewPresenter.xml#EGGAC, называется Inversion of Control.

                    > А в текущем приложении разве не то же самое происходит когда указатель на view передается в презентер?

                    В описанном мной случае объект, в котором конкретный View передается в Presenter, не знает, как именно происходит связь между View и Presenter-ом. В предлагаемом вами варианте при изменении интерфейса придется менять код в вашем объекте, а при использовании интерфейса организация связи View и Presenter сокрыта в Presenter.

                    Вообще, основная идея в том, чтобы однажды написав IView, можно было бы создавать конкретные View, при этом код основного приложения абсолютно не менялся. Например, есть фабрики классов, которые выдают конкретные View (окошки вашего приложения). Допустим фабрики ClassicViewFactory, GlamourViewFactory и WebViewFactory, которые наследуются от AbstractViewFactory. Далее, создавая на этапе запуска приложения конкретную фабрику (скажем прочитав соответствующий флаг из файла настроек приложения), имеем абсолютно разный внешний вид приложения. При этом код приложения вообще не меняется. При добавлении нового стиля приложения — добавляется новая фабрика, а в написанный код добавится лишь код:
                    if (settings.value(«style»).toString() == «web»)
                    m_viewFactory = new WebViewFactory(this); // где m_viewFactory объявлена как QAbstractViewFactory *m_viewFactory;
                      0
                      Ну можно обойтись вообще без IView, да. Но тогда увеличится связанность, т.е. презентеру придется знать о конкретном View, а не об интерфейсе. IView для этого и применен, для уменьшения связанности. Подробней можно прочесть тут: www.rsdn.ru/article/patterns/ModelViewPresenter.xml#EGGAC, называется Inversion of Control.


                      С inversion of control я немного знаком :). Что я бы хотел обсудить, если у вас есть немного времени: если не использовать интерфейс, то презентеру вообщем-то также не нужно знать о конкретном view. Давайте посмотрим на простой пример. Предположим, у нас есть view в лице небольшого окошка, которое может отображать текст и имеет кнопку «закрой меня». При использовании интерфейса это будет реализовано так (псевдокод, не компилируется):

                      class INotify
                      {
                        public: virtual void setText( QString ) = 0;
                        public signals: virtual void closeClicked() = 0;
                      };
                       
                      class CNotify : public INotify
                      {
                        public: void setText( QString )
                        {
                          //  Устанавливаем текст.
                        }
                       
                        //  Вызывается Qt при клике на кнопку.
                        private slots: void on_btn_close_clicked()
                        {
                          //  Рапортуем о клике на кнопку.
                          emit closeClicked()
                        }
                      }
                       
                      class CPresenter
                      {
                        public: CPresenter( INotify* view )
                        {
                          m_view = view;
                          connect( view, SIGNAL(closeClicked()), this, SLOT(onClose()) );
                        }
                       
                        private slots: void onClose()
                        {
                          //  Модель опустим, тут у нас как-бы логика.
                          view->setText( tr( L"меня закрыли :(" ) );
                        }
                      }
                       
                      void main()
                      {
                        CNotify notify;
                        CPresenter presenter( notify );
                      }
                       


                      Теперь то же самое при использовании сигналов и слотов:

                      class CNotify
                      {
                        public: CNotify()
                        {
                          connect( ui->btn_close, SIGNAL(clicked), this, SIGNAL(closeClicked()) );
                        }
                       
                        signals: void closeClicked();
                       
                        public slots: void setText( QString )
                        {
                          //  Устанавливаем текст.
                        }
                      }
                       
                      class CPresenter
                      {
                        signals: void setText( QString );
                       
                        public slots: void onClicked()
                        {
                          emit setText( tr( L"меня закрыли :(" ) );
                        }
                      }
                       
                      void main()
                      {
                        CNotify notify
                        CPresenter presenter
                        connect( notify, SIGNAL(closeClicked()), presenter, SLOT(onClicked()) );
                        connect( presenter, SIGNAL(setText(QString)), notify, SLOT(setText(QString)) );
                      }
                       


                      Как видно из исходника, в случае использования сигналов и слотов презентер также ничего не знает о view.

                      В описанном мной случае объект, в котором конкретный View передается в Presenter, не знает, как именно происходит связь между View и Presenter-ом. В предлагаемом вами варианте при изменении интерфейса придется менять код в вашем объекте, а при использовании интерфейса организация связи View и Presenter сокрыта в Presenter.


                      Ну почему же не знает — он ведь передает интерфейс. Соответственно мы набор connect() меняем на описание интерфейса. И в том и другом случае объект, который связывает View и Presenter знает как происходит связь — либо через кучку connect, либо через спецификацию интерфейса.

                      Вообще, основная идея в том, чтобы однажды написав IView, можно было бы создавать конкретные View, при этом код основного приложения абсолютно не менялся. Например, есть фабрики классов, которые выдают конкретные View (окошки вашего приложения). Допустим фабрики ClassicViewFactory, GlamourViewFactory и WebViewFactory, которые наследуются от AbstractViewFactory. Далее, создавая на этапе запуска приложения конкретную фабрику (скажем прочитав соответствующий флаг из файла настроек приложения), имеем абсолютно разный внешний вид приложения. При этом код приложения вообще не меняется. При добавлении нового стиля приложения — добавляется новая фабрика, а в написанный код добавится лишь код:


                      Паттерн «абстрактная фабрика», если не ошибаюсь? А оно в данном случае имеет какую-либо практическую ценность? Насколько я помню, внешний вид интерфейса в Qt меняется штатными средствами на уровне вызова одной функции «установить стиль для всего приложения». Что нам может дать абстрактная фабрика в реальных приложениях?
              0
              В приведенном вами примере, функция main() знает о том, как именно происходит связь между CNotify и CPresenter. Теперь при изменении интерфейсной части CNotify придется переписывать также функцию main(). Кроме того, метод QObject::connect() выполняется в динамике, в отличие передачи объекта по интерфейсу. А значит вы лишаетесь статической проверки того, насколько вы всё правильно сделали; например, если вы сигнал closeClicked() измените на closed(), программа скомпилируется, но не будет работать корректно (уже во время выполнения программы в консоль выведется информация о том, что не получилось соединиться с несуществующим сигналом closeClicked()). А теперь представьте, что View делает совершенно другой человек, более того — менее квалифицированный в программирование. Думаю статическая типизация сэкономит немало нервов.

              > Как видно из исходника, в случае использования сигналов и слотов презентер также ничего не знает о view.
              Знает, просто это не так заметно. На самом деле механизм сигналов и слотов — это реализация паттерна Observer на уровне MOC-компилятора, а значит объект подписки (Subject) знает о наблюдателях (Observer), а также знает указатели на функции уведомления у Observer.

              > Ну почему же не знает — он ведь передает интерфейс
              Я писал не о том, что объект не знает об объекте View, а о том, что он не знает, как именно происходит связь между View и Presenter

              > А оно в данном случае имеет какую-либо практическую ценность? Насколько я помню, внешний вид интерфейса в Qt меняется штатными средствами на уровне вызова одной функции «установить стиль для всего приложения».

              Насчет этого я уже писал в комментарии выше. Похоже, вы не до конца поняли, что дает MVP. setStyle() и setStyleSheet() позволяют с помощью QSS изменить внешний вид элементов. MVP позволяет вам изменить сам View, т.е. на окне могут быть совершенно другие элементы, расположенные совершенно по-другому. При этом setStyle() и setStyleSheet() никто не отменяет, MVP и страницы стилей это совершенно ортогональные вещи. Более того, с помощью MVP вы можете сделать представление не окошком, а консолью, или вообще веб-формой. Так что абстрактная фабрика действительно может пригодится в случае, если ваше приложение должно быть очень гибкое в плане UI, т.е. легко менять все View в приложении в зависимости от выбранного пользователем стиля. При чем останется возможность масштабирования системы, т.е. добавления новых видом View и фабрики для них. Наверное можно вообще сделать это дело плагинами через QtPlugin, предоставив таким образом разработчикам SDK для создания собственных View.
                0
                В приведенном вами примере, функция main() знает о том, как именно происходит связь между CNotify и CPresenter. Теперь при изменении интерфейсной части CNotify придется переписывать также функцию main().


                В случае использования интерфейса, при изменении интерфейсной части CNotify придется переписывать также интерфейс. ИМХО, тут у подходов паритет?

                роме того, метод QObject::connect() выполняется в динамике, в отличие передачи объекта по интерфейсу. А значит вы лишаетесь статической проверки того, насколько вы всё правильно сделали


                В целом да. Хотя, по моему скромному мнению, для больших проектов это очень маленькая проблема :).

                Знает, просто это не так заметно. На самом деле механизм сигналов и слотов — это реализация паттерна Observer на уровне MOC-компилятора, а значит объект подписки (Subject) знает о наблюдателях (Observer), а также знает указатели на функции уведомления у Observer.


                Реализация, да. Но при этом процесс подписывания — внешний по отношению и к observer, и к observable. Тоесть с точки зрения observable у него есть только торчащий наружу делегат (signal), он не знает к кому этот делегат подключен.

                Я писал не о том, что объект не знает об объекте View, а о том, что он не знает, как именно происходит связь между View и Presenter


                Тут я с вами соглашусь. С другой стороны, иметь все связи в одном месте — удобнее для поддержки проекта, нежели иметь из размазанными по сотням интерфейсов?

                Более того, с помощью MVP вы можете сделать представление не окошком, а консолью, или вообще веб-формой. Так что абстрактная фабрика действительно может пригодится в случае, если ваше приложение должно быть очень гибкое в плане UI, т.е. легко менять все View в приложении в зависимости от выбранного пользователем стиля.


                Я таких «очень гибких» пока на практике не встречал. Зато встречал, например, необходмость из разных частей программы обращаться к фрагменту функциональности presenter. Например, для IM клиента нам в главном окне нужна полная функциональность работы со списком пользователей, для окна настроек — только получение списка, для модуля нотификаций — только получения имени по ID и так далее. Чем мне не очень нравятся интерфейсы — для каждого взаимодействия придется реализовывать ВЕСЬ интерфейс, так как в C++ ни informal protocol, ни optional interface не реализовано :(. Соответственно, опасаюсь что в проектах от миллиона строк кода это приведет к тому же, что в свое время было у COM: любое взаимодействие требует обязательной имплементации интерфейса о пятидесяти — ста методах О_О.
                0
                Примеры перезалиты на гит-репозиторий: https://gitlab.com/joeskb7/qt-mvp

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое