14 марта 2014 в 21:34

Паттерны для новичков: MVC vs MVP vs MVVM

C#*, .NET*
Добрый день, уважаемые коллеги. В этой статье я бы хотел рассказать о своем аналитическом понимании различий паттернов MVC, MVP и MVVM. Написать эту статью меня побудило желание разобраться в современных подходах при разработке крупного программного обеспечения и соответствующих архитектурных особенностях. На текущем этапе своей карьерной лестницы я не являюсь непосредственным разработчиком, поэтому статья может содержать ошибки, неточности и недопонимание. Заинтригованы, как аналитики видят, что делают программисты и архитекторы? Тогда добро пожаловать под кат.

Ссылки

Первое, с чего я бы хотел начать — это ссылки на внешние материалы, которыми я руководствовался в процессе написания этой статьи:


Введение

Во времена, когда солнце светило ярче, а трава была зеленее, на тот момент команда студентов, как автор этой статьи, разрабатывали программное обеспечение, писав сотни строк кода непосредственно в интерфейсе продукта. Иногда использовались сервисы и менеджеры для работы с данными и тогда решение получалось с использованием паттерна Document-View. Поддержка такого кода требовала колоссальных затрат, т. к. нового разработчика надо обучить (рассказать), какой код за что в продукте отвечает, и ни о каком модульном тестировании и речи не было. Команда разработки — это 4 человека, которые сидят в одной комнате.
Прошло время, менялась работа. Разрабатываемые приложения становились больше и сложнее, из одной сплоченной команды разработчиков стало много разных команд разработчиков, архитекторов, юзабилистов, дизайнеров и PMов. Теперь каждый ответственен за свою область: GUI, бизнес-логика, компоненты. Появился отдел анализа, тестирования, архитектуры. Стоимость разработки ПО возросла в сотни и даже тысячи раз. Такой подход к разработке требует наличие стойкой архитектуры, которая бы синхронизировала разные функциональные области продукта между собой.

Паттерны

Учитывая цель уменьшения трудозатрат на разработку сложного программного обеспечения, предположим, что необходимо использовать готовые унифицированные решения. Ведь шаблонность действий облегчает коммуникацию между разработчиками, позволяет ссылаться на известные конструкции, снижает количество ошибок.
По словам Википедии, паттерн (англ. design pattern) — повторимая архитектурная конструкция, представляющая собой решение проблемы проектирования в рамках некоторого часто возникающего контекста.

Начнем с первого главного – Model-View-Controller. MVC — это фундаментальный паттерн, который нашел применение во многих технологиях, дал развитие новым технологиям и каждый день облегчает жизнь разработчикам.

Впервые паттерн MVC появился в языке SmallTalk. Разработчики должны были придумать архитектурное решение, которое позволяло бы отделить графический интерфейс от бизнес логики, а бизнес логику от данных. Таким образом, в классическом варианте, MVC состоит из трех частей, которые и дали ему название. Рассмотрим их:

Модель

Под Моделью, обычно понимается часть содержащая в себе функциональную бизнес-логику приложения. Модель должна быть полностью независима от остальных частей продукта. Модельный слой ничего не должен знать об элементах дизайна, и каким образом он будет отображаться. Достигается результат, позволяющий менять представление данных, то как они отображаются, не трогая саму Модель.

Модель обладает следующими признаками:
  • Модель — это бизнес-логика приложения;
  • Модель обладает знаниями о себе самой и не знает о контроллерах и представлениях;
  • Для некоторых проектов модель — это просто слой данных (DAO, база данных, XML-файл);
  • Для других проектов модель — это менеджер базы данных, набор объектов или просто логика приложения;


Представление (View)

В обязанности Представления входит отображение данных полученных от Модели. Однако, представление не может напрямую влиять на модель. Можно говорить, что представление обладает доступом «только на чтение» к данным.

Представление обладает следующими признаками:
  • В представлении реализуется отображение данных, которые получаются от модели любым способом;
  • В некоторых случаях, представление может иметь код, который реализует некоторую бизнес-логику.

Примеры представления: HTML-страница, WPF форма, Windows Form.

Различия MVP & MVVM & MVP

Наиболее распространенные виды MVC-паттерна, это:
  • Model-View-Controller
  • Model-View-Presenter
  • Model-View-View Model


Рассмотрим и сравним каждый из них.

Model-View-Presenter


Данный подход позволяет создавать абстракцию представления. Для этого необходимо выделить интерфейс представления с определенным набором свойств и методов. Презентер, в свою очередь, получает ссылку на реализацию интерфейса, подписывается на события представления и по запросу изменяет модель.

Признаки презентера:
  • Двухсторонняя коммуникация с представлением;
  • Представление взаимодействует напрямую с презентером, путем вызова соответствующих функций или событий экземпляра презентера;
  • Презентер взаимодействует с View путем использования специального интерфейса, реализованного представлением;
  • Один экземпляр презентера связан с одним отображением.


Реализация:
Каждое представление должно реализовывать соответствующий интерфейс. Интерфейс представления определяет набор функций и событий, необходимых для взаимодействия с пользователем (например, IView.ShowErrorMessage(string msg)). Презентер должен иметь ссылку на реализацию соответствующего интерфейса, которую обычно передают в конструкторе.
Логика представления должна иметь ссылку на экземпляр презентера. Все события представления передаются для обработки в презентер и практически никогда не обрабатываются логикой представления (в т.ч. создания других представлений).

Пример использования: Windows Forms.

Model-View-View Model


Данный подход позволяет связывать элементы представления со свойствами и событиями View-модели. Можно утверждать, что каждый слой этого паттерна не знает о существовании другого слоя.

Признаки View-модели:
  • Двухсторонняя коммуникация с представлением;
  • View-модель — это абстракция представления. Обычно означает, что свойства представления совпадают со свойствами View-модели / модели
  • View-модель не имеет ссылки на интерфейс представления (IView). Изменение состояния View-модели автоматически изменяет представление и наоборот, поскольку используется механизм связывания данных (Bindings)
  • Один экземпляр View-модели связан с одним отображением.


Реализация:
При использовании этого паттерна, представление не реализует соответствующий интерфейс (IView).
Представление должно иметь ссылку на источник данных (DataContex), которым в данном случае является View-модель. Элементы представления связаны (Bind) с соответствующими свойствами и событиями View-модели.
В свою очередь, View-модель реализует специальный интерфейс, который используется для автоматического обновления элементов представления. Примером такого интерфейса в WPF может быть INotifyPropertyChanged.

Пример использования: WPF

Model-View-Controller


Основная идея этого паттерна в том, что и контроллер и представление зависят от модели, но модель никак не зависит от этих двух компонент.

Признаки контроллера
  • Контроллер определяет, какие представление должно быть отображено в данный момент;
  • События представления могут повлиять только на контроллер.контроллер может повлиять на модель и определить другое представление.
  • Возможно несколько представлений только для одного контроллера;


Реализация:
Контроллер перехватывает событие извне и в соответствии с заложенной в него логикой, реагирует на это событие изменяя Mодель, посредством вызова соответствующего метода. После изменения Модель использует событие о том что она изменилась, и все подписанные на это события Представления, получив его, обращаются к Модели за обновленными данными, после чего их и отображают.

Пример использования: MVC ASP.NET

Резюме

Реализация MVVM и MVP-паттернов, на первый взгляд, выглядит достаточно простой схожей. Однако, для MVVM связывание представления с View-моделью осуществляется автоматически, а для MVP — необходимо программировать
MVC, по-видимому, имеет больше возможностей по управлению представлением.

Общие правила выбора паттерна

MVVM

  • Используется в ситуации, когда возможно связывание данных без необходимости ввода специальных интерфейсов представления (т.е. отсутствует необходимость реализовывать IView);
  • Частым примером является технология WPF.


MVP

  • Используется в ситуации, когда невозможно связывание данных (нельзя использовать Binding);
  • Частым примером может быть использование Windows Forms.


MVC

  • Используется в ситуации, когда связь между представление и другими частями приложения невозможна (и Вы не можете использовать MVVM или MVP);
  • Частым примером использования может служить ASP.NET MVC.


Заключение

В заключении, автор этой статьи хотел бы отметить, что строго придерживаться только одному паттерну — не всегда лучший выбор. Например, представьте, что Вы хотели бы использовать MVVM для разработки приложений с использованием Windows Forms через свойство контролов Bindings. Ваша цель — это отделить представление от бизнес логики и логики, которая их связывает. Приложение должно быть легко тестируемым и поддерживаемым, а для аналитиков — понятным (ведь на вопрос «в чем измеряется работа жесткого диска» существует единственный правильный ответ — в Джоулях (абстрактный пример Модели -> Представления)).

Большое спасибо за уделенное время, приятного чтения!
Сергей @Cydoor
карма
14,2
рейтинг 0,0
Самое читаемое Разработка

Комментарии (36)

  • +1
    Открыл для себя давно уже паттерн MVPVM для XAML платформ. Это просто смесь MVP + MVVM.
    Там где удобно использовать bindings — просто используешь всю мощь MVVM. А там где нужно вызвать что-то вроде ListBox.ScrollIntoView — просто используешь мощь MVP — делаешь метод для интерфейса представления и его просто вызываешь.
    • 0
      Спасибо, добрый человек.
      Во-первых, спасибо Вам за статью, написанную ранее по этой же теме (я про Ваш блог).
      Во-вторых, MSDN-журнал уже писал об этом.
      IMHO, MVPVM отлично подходит для Windows Forms. Для каждого интерфейса представления должна быть реализована функция, типа «InitializeBindings», где для каждого контрола связывание должно происходить.
      • 0
        IMHO, MVPVM отлично подходит для Windows Forms. Для каждого интерфейса представления должна быть реализована функция, типа «InitializeBindings», где для каждого контрола связывание должно происходить.

        Это уже тонкости реализации. Я просто говорю брать лучшее от двух. В случае с MVP приходится писать много кода, который связывает View и Presenter, в случае с MVVP этого делать не нужно в 80% случаях, но в оставшихся 20% (ототбражение Popup, вызова ListBox.ScrollIntoView ) придется писать безумных Iteraction, хотя иногда это намного проще решается при помощи MVP связки.
        Когда у меня была идея написать больше одного приложения для Windows Store я сделал что-то вроде Framework для себя github.com/outcoldman/Framework под эту платформу. И удачно реализовал gMusicW на нем (http://apps.microsoft.com/windows/en-us/app/gmusicw/939f0859-1413-4a52-9ab6-6e50405c8c2e). Но в итоге из-за нехватки времени просто стал контрибютить дальше только для gMusicW. Но в целом, Framework может показать пару интересных идей.
    • 0
      Не могли бы вы взглянуть на bitbucket.org/igor_kostromin/wpf-mvp/ (ссылка на пример)? Интересно ваше мнение о такой реализации — похоже, что у нас и получился именно MVPVM, о котором вы говорите.
      • +1
        Да очень похоже MVPVM. Ничего хорошего или плохого сказать не могу о реализации, пример слабоват. Но главное, чтобы для вас работало как нужно ;)
  • +1
    Мне кажется в описании View-Model вкралась ошибочка в последнем пункте. Вполне возможно и даже принято связывать одну View-модель с несколькими View. Очень распространено в master-details представлениях.
    • 0
      Не совсем понятно, а не приведет ли это к чрезмерному разрастанию View-модели, которая станет поистине универсальной и огромной, «на все случае (виды) жизни»?

      В классическом описании модель может представлять себе один экземпляр.
      • 0
        Все зависит от программиста, но такого ограничения ни физически, ни логически нет. Для примера возьмем упомянутый паттерн master-details & WPF. Создаем в мастере список view-моделей, привязываем его к лист-боксу и задаем отображение для элемента. Это первое view для нашей view-модели. Далее создаем ContentControl и привязываем наш список с заключительным слешем (кто не знает, это привязывает выбранный элемент списка). Создаем отображение details для той же view-модели. Это будет второе view. Есть примеры отображения модели как графика и как списка или разных графиков и много другого разного. Главное, что идея MVVM не ограничивает привязку одного отображения.
        • 0
          Трудно не согласится, что все, в конечном итоге, сводится к людям и их профессионализму.
          В любом случае, если такой подход решает поставленные задачи, то она всегда будет хорошей :)
      • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          Таких частных случаев как пионеров нерезанных. Я привел самый распространенный, без которого не обходится ни одна аппликация. Кстати Майкрософт по этому поводу говорит следующее: «There is typically a one-to-one relationship between a view and its view model». Другими словами это типично, но не обязательно. Пруфлинк msdn.microsoft.com/en-us/library/gg405484(v=pandp.40).aspx.
  • +2
    MVC, это не паттерн — это парадигма.
    При MVC пользователь взаимодействует с Controller а не со View
    • 0
      Не ясно, что такое View в этом случае… Позвольте утверждать, что аббревиатурой MVC обычно понимают целое семейство паттернов, среди которых можно выделить первоначальный вариант.
      • 0
        www.itu.dk/courses/VOP/E2005/VOP2005E/8_mvc_krasner_and_pope.pdf
        К тому же нужно разобраться в терминах, что есть шаблон. а что есть парадигма.
        Но так как вы выделили, что:
        что аббревиатурой MVC обычно понимают целое семейство паттернов

        уже говорит о том что вы подразумеваете парадигму.
        • 0
          Я могу ошибаться, но вроде как парадигма это совокупность действий, определяющих стиль. Т.е. парадигма в программровании — это семейство нотаций, различающихся по методикам. Примеры:
          1. Функциональное программирование
          2.Логическое программирование
          3. ООП.

          С другой стороны, Вы правы, в классическом MVC действия пользователя передаются в контроллер. Наверное, это имелось ввиду.
          • 0
            Я могу ошибаться, но вроде как парадигма это совокупность действий, определяющих стиль. Т.е. парадигма в программровании — это семейство нотаций, различающихся по методикам.

            Вы правы, так оно и есть.
            MVC — это парадигма, в которую вписываются семейство паттернов. Паттерн — это реализация и они различны, как вы сказали ранее (М.Фаулер о MVC)
  • 0
    Спасибо за статью.
    Вопрос: как практически реализовать MVPVM в Windows Forms?
    Сейчас у меня есть классы, в которых реализована логика и хранятся данные, есть формы в которых реализован интерфейс, в классе формы есть ссылка на экземпляр класса логики/данных и дальше происходит следующее:
    когда мне нужно выполнить действие, то я ловлю событие нажатия на кнопку, метод обработки события сначала собирает данные из контролов формы, обновляет состояние класса логики и вызывает метод класса логики, затем обновляет интерфейсные контролы.
    А как можно улучшить структуру?
    • 0
      Добрый день,
      > когда мне нужно выполнить действие, то я ловлю событие нажатия на кнопку, метод обработки события сначала собирает данные из контролов формы, обновляет состояние класса логики и вызывает метод класса логики, затем обновляет интерфейсные контролы.
      Вы описали логику Presentera. В Вашем случае, для практики, я бы рекомендовал обратиться к использованию паттерна MVP,
  • 0
    Несколько некорректно выглядит вот это утверждение об MVC: "Модель использует событие о том что она изменилась, и все подписанные на это события Представления, получив его, обращаются к Модели за обновленными данными, после чего их и отображают" в связке с указанием о том, что этот подход применяется в ASP.NET MVC. Конкретно там View не подписывается на изменения модели и никак не следит за ними. Эта роль отводится контроллеру, он реагирует на действие пользователя, меняет модель и отдает View измененные данные для отображения.
  • +1
    Мне одному кажется, что статьи про архитектуру ПО сильно проигрывают, если не дополнены примерами кода?
    • +1
      Беда в том, что объем кода, способный нормально проиллюстрировать архитектурный прием такого уровня будет превышать объем данной статьи.
      • 0
        Не вижу в этом вообще никакой проблемы. Среднестистический девелопер в команде постоянно работает с такими колоссальными объемами кода, на фоне которых примеры реализации архитектурных паттернов — капля в море. Тем более их всегда можно залить на гитхаб или спрятать под спойлер.

        Вот пример статьи уважаемого мной товарища про модули в Java Script habrahabr.ru/post/181536/. Статья большая? Большая. Стало от этого хуже? Нет, не стало, статья замечательная и отвечает на 90% вопросов, связанных с паттернами модулей в Java Script.
        • –1
          Пожалуйста, больше никогда не пишите JavaScript раздельно.
  • 0
    А как предотвратить лишние обновления Представления, если приходят повторяющиеся события об изменении данных в процессе пересчета Модели?
    • 0
      Видимо тут все сильно зависит от платформы. Если это XAML технология построенная на Bindings (XAML, Angular) то там представление обновится только тогда, когда данные реально поменяются.
      В противном случае можно представление разбивать на более мелкие и обновлять только их.
      • 0
        Спасибо за ответ. Только не очень понятно про разбитие представления. Можно, я попробую описать пару проблемных примеров.

        Пример 1. На форме в виде текстового поля нужно отобразить протокол выполняемых в Модели действий. Каждое действие в модели добавляет к тексту новую строку — и Модель каждый раз будет уведомлять представление, а представление каждый раз перерисовывать контрол с текстом.

        Пример 2. Модель рассчитывает итоговую сумму, в которой суммируются другие расчетные значения, общее количество которых в начале расчета неизвестно. К примеру, в страховая премия складывается из выбранных пунктов договора страхования. Сначала мы обнуляем итоговую сумму, а затем рассчитываем нужные пункты и добавляем к итоговой сумме. И каждый раз, когда меняется итоговая сумма, от модели в представление будет идти событие об изменении и итоговая сумма будет перерисовываться в представлении.

        Как избежать этих перерисовываний в обоих примерах? По идее модель должна уведомить представление только один раз — после всех расчетов, а представление отобразит окончательное значение один раз. Но как в модели определить, что именно это изменение будет последним? Ведь любая модификация данных может либо стать последней в алгоритме, либо за ней последуют другие модификации. В какой момент модель должна отправить сообщение представлению?
        Как при связывании данных решается эта проблема?
        • 0
          В MVVM обычно можно создать привязку, которая будет отправлять данные в вид или модель вида с задержкой, игнорируя быстро происходящие изменения. В популярных фреймворках это делается одной строчкой: WPF, Knockout.
          • 0
            А без задержки никак нельзя?
            • 0
              Если хочется сэкономить пару перерисовок, можно не заниматься преждевременной оптимизацией, и оставить всё, как есть.
              В некоторых фреймворках, например Angular, изменение модели на вид никак не влияет, фреймворк сам решает, когда перечитать модель, и такие оптимизации не понадобятся.

              В примере 1, если одно действие пользователя вызовет добавление десятков строк в лог, а сама модель за лог не отвечает, задержка будет неплохим вариантом. Если лог генерируется в UI-потоке, можно поставить задержку в 1 мс, что по факту приведёт к 1 перерисовке сразу после завершения вычислений и освобождения UI-потока. Задержки по полсекунды нужны для других случаев, например, когда ввод текста пользователем вызывает отправку запросов на сервер (поисковые подсказки и т.д.).

              В примере 2, результатом расчётов, видимо, является одно значение, вот пусть модель его и считает, а результат передаёт в модель вида, только когда он готов. Разделение на M и VM нужно, в том числе, и для этого.
              • 0
                Спасибо за разъяснения. Посмотрел про Angular — там красивые уроки и примеры. Получается, что в этом фреймворке модель пассивная и сама не отправляет сообщения. Как же все-таки Angular определяет, что именно в $scope изменилось в модели по сравнению с предыдущей версией?
                • 0
                  На то или иное изменение во view вызывается метод $scope.$apply, например (цитата из angular.js, код стандартного обработчика для checkbox'а):

                  ...
                  element.on('click', function() {
                      scope.$apply(function() {
                        ctrl.$setViewValue(element[0].checked);
                      });
                    });
                  ...
                  


                  $scope.$apply() в своем коде вызывает $scope.$digest (см. исходный код).

                  При этом у $scope'а есть свойство $$watchers — это функции, которые по вызову вычисляют какое-то определенное для каждой из них выражение (первый аргумент), и если оно поменялось с момента предыдущего вызова, вызывают callback (второй аргумент) [ссылка на синтаксис создания $watcher'а. В большинстве случаев эти штуки добавляются встроенными директивами].

                  $scope.$digest() вызывает все $scope.$$watchers по очереди, сравнивая старое и новое значение выражений для каждого из них (см. тело функции $digest) и, если старое и новое значения для данного $watcher'а различаются, запускается callback соответствующего $watcher'а, принимающего oldValue и newValue в качестве аргументов.

                  Соответственно уже конкретный callback может делать что угодно — менять какое-то другое выражение, добавлять/менять/убирать разметку определенного элемента и т.п.

                  Как-то так)
  • 0
    В последнее время на Хабре, да и в принципе в головах разработчиков так часто мусолится тема MVC/MVP/MVVM/MV*, начало всплывать такое множество различных вариаций этих паттернов, что в какой-то момент задумываешься: а где же, блин, отличия между ними :-) Приходится (зачем?) глубоко вчитываться во все описания и приходить к выводу, что всё это многообразие обусловлено лишь ограничениями\спецификой случаев, в которых их применяют. И «толщиной» каждого из слоёв — где-то больше кода во вью, где-то в контроллере. Как результат — когда кто-то рассказывает тебе о MV* или задаёт тебе вопрос на эту тему, уже невольно теряешься: а что конкретно этот человек понимает под этими буквами. Все эти различия и нюансы настолько незначительны и не принципиальны…
    • +1
      Добрый день,
      Позвольте не согласиться с Вашим последним утверждением. Различия и нюансы сильно влияют на дальнейшее развитие продукта в целом.
      На мой взгляд, применение определенной архитектуры — это стратегия развития продукта, его возможностей в будущем. Например, через 2-3 релиза, учитывая, что у нас RUP.
      • 0
        Я пытался сказать, что в теории, статьях, картинках, разговорах — различия между паттернами высосаны из пальца и не принципиальны. Мало смысла пытаться забивать себе голову тем, как MVC отличается от, допустим, MVPVM. На практике разница будет, но не в принципиальном подходе, а небольших деталях реализации. И вытекать она будет из требований и условий проекта, а не решений сверху «давайте здесь будет использовать MVP, а вот здесь — MVVM». И всё-равно в каждом проекте будет своя «версия» MV*, отличающаяся от теории. Даже в пресловутом ASP.NET MVC не то, что описано в данной статье.
        • 0
          И вытекать она будет из требований и условий проекта, а не решений сверху «давайте здесь будет использовать MVP, а вот здесь — MVVM». И всё-равно в каждом проекте будет своя «версия» MV*, отличающаяся от теории.

          Верные слова! Выбирается только исходя из целей и трудозатра.
  • 0
    Спасибо за статью, Сергей!

    В секции Model-View-Controller в качестве признака Контроллера указан пункт «Контроллер определяет, какие представление должно быть отображено в данный момент;», однако на прикрепленной вами диаграмме не указана определяющая связь от Контроллера к Представлению. Скорее, указана определяющая связь от Представления к Контроллеру. Я правильно понимаю, что это противоречие?

    Возможно, вы имели в виду что-то вроде www.sitepoint.com/getting-started-with-mvc с диаграммой

    image

    В этой диаграмме действительно показана определяющая роль Контроллера при выборе Представления.




    Также должен сказать, что в ряде обучающих статей на тему (например, в Википедии ru.wikipedia.org/wiki/Model-View-Controller) действия пользователя передаются не в Представление, а в Контроллер. Но тут стоит сказать, что общее несоответствие в этой детали по поводу MVC мы можем наблюдать во всех статьях по теме.

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