MVP
MVP развивает MVC и доводит View до полной пассивности: он не выполняет никакой логики и лишь передаёт действия пользователя в Presenter, а для обновления экрана предоставляет несколько простых методов. Presenter закреплён за своим View один к одному, работает с ним через интерфейс, меняет Model и готовит её данные к выводу. За счёт этого всю логику представления можно покрыть юнит-тестами, подставив вместо настоящего View заглушку.
Проблема
В классическом MVC Controller может напрямую дёргать методы конкретного виджета, а View - сам читать Model. Из-за этих поблажек логику представления не проверить без запуска настоящих элементов интерфейса, а форматирование вывода расходится сразу по трём классам. Командам, которые собирали громоздкие экраны с множеством форм на десктопе и мобильных, нужен был вариант, где всё, что видит пользователь, задаётся в одном месте, пригодном для тестов.
Решение
View сводится к интерфейсу с методами вроде showItems(list), showError(text), setSubmitEnabled(flag) и к конкретному виджету, который этот интерфейс реализует. Виджет передаёт в Presenter сырые события (нажали кнопку, изменили текст) и никак их не трактует. Presenter держит ссылку на интерфейс View (не на сам виджет) и на Model: получив событие, он выполняет логику взаимодействия, вызывает методы Model, забирает результат и через интерфейс отдаёт View уже готовые к отрисовке значения. В классическом варианте (Dolphin Smalltalk) View вдобавок сам подписан на Model ради простых изменений значений; в варианте Passive View и это идёт через Presenter, а виджет о Model не знает вообще ничего.
Как это работает
Термин Model-View-Presenter появился в Taligent - совместном предприятии Apple, IBM и Hewlett-Packard (1992-1998), которое собиралось построить объектно-ориентированную операционную систему нового поколения. Майк Потел (Mike Potel), технический директор Taligent, в 1996 году опубликовал работу «MVP: Model-View-Presenter - The Taligent Programming Model for C++ and Java». MVP у самой Taligent был громоздким: взаимодействие дробилось на Interactor, Command, Selection и координирующий их Presenter. Операционная система так и не вышла, но терминология осталась.
Под MVP сегодня почти все понимают гораздо более простой вариант, который около 2000 года описали Энди Бауэр (Andy Bower) и Блэр Макглашан (Blair McGlashan) из команды Dolphin Smalltalk в статье, обычно известной как «Twisting the Triad». Тройное деление MVC они сохранили, но перераспределили связи: View подписан прямо на Model ради простого показа значений, а все жесты пользователя обрабатывает Presenter, и меняет Model только он. Controller из MVC - в Smalltalk он в основном разбирал сырые события мыши и клавиатуры - пропал: современные наборы виджетов делают эту работу сами.
В 2006 году Мартин Фаулер (Martin Fowler) разобрал всё это семейство и решил, что название «MVP» стало слишком размытым, чтобы оставаться одним. Он разделил его на Passive View - у View ноль логики и ноль доступа к Model, каждый пиксель через интерфейс рисует Presenter - и Supervising Controller - простые свойства Model View связывает с экраном сам, а Presenter подключается только к сложным правилам. Passive View выжимает максимум тестируемости ценой большого интерфейса View; Supervising Controller требует меньше связующего кода, но часть логики связывания снова оказывается в виджете и вне тестов.
Чаще всего разницу с MVC описывают через то, кто управляет отрисовкой. В MVC следующий View выбирает Controller, а View волен читать Model; в MVP Presenter намертво закреплён за своим интерфейсом View и - по крайней мере в Passive View - только он этот View и обновляет. Поэтому Presenter оказывается обычным объектом без зависимостей от виджетов, и в этом весь смысл: его создают с поддельным View, подают ему события и смотрят, какие showX он вызвал.
MVP и MVVM решают одну и ту же задачу - тестируемую логику представления - противоположными способами. В MVP вызовы обновления View пишут руками (view.showItems(...)); в MVVM их убирают: слой связывания данных сам синхронизирует виджеты View со свойствами ViewModel. Поэтому MVP не нужна поддержка со стороны фреймворка и он работает на любом языке, а MVVM нужен биндер, и окупается он прежде всего там, где биндер уже есть.
Больше всего MVP применяли в Android примерно 2015-2019 годов. Стандартным приёмом был интерфейс LoginContract с вложенными interface View { showProgress(); showError(String); } и interface Presenter { onLoginClicked(...); }: Activity или Fragment реализовывал View, а всё остальное передавал Presenter-у на чистой Java, который можно было гонять в JUnit без устройства. После того как Google в 2017-2019 годах взял курс на ViewModel, LiveData, а позже Compose, почти весь этот шаблонный код устарел - но в больших старых проектах его до сих пор много.
Постоянная претензия к MVP: интерфейс View разрастается - по методу на каждое визуальное состояние (showLoading, showEmpty, showError, showRetry, showContent), - а Presenter превращается в длинный набор веток, которые вызывают их в разных сочетаниях. Часть команд отвечает на это иначе: передаёт один неизменяемый объект ViewState в единственный метод render(state) - и это уже шаг от MVP в сторону подхода «нарисуй объект состояния», как в MVVM и современном React.
Если убрать платформенные детали, от MVP остаётся одна мысль: граница между логикой представления и набором виджетов должна быть явным интерфейсом, чтобы логику можно было тестировать в отрыве от UI. Сколько бы методов ни было у этого интерфейса - двадцать showX (Passive View) или один render(state) (современный вариант) - и заполняет ли его за вас биндер (MVVM), граница проходит там же, где её провёл MVP.
Нюансы выбора
Сложная логика одного экрана под тестами - формы-мастера, экраны с множеством условных полей и состояний ошибок, где каждую ветку нужно закрыть быстрым юнит-тестом, а поднимать ради каждого прогона настоящий UI недопустимо.
Вместо MVC - если ваш Controller в MVC уже дёргает методы конкретных виджетов и не поддаётся юнит-тестам, оформить View как интерфейс (то есть перейти на MVP) - это самая маленькая правка, которая решает проблему.
Вместо MVVM - берите MVP, когда в вашем языке или на платформе нет слоя связывания данных или когда вы намеренно хотите, чтобы вызовы обновления View были явными и находились обычным поиском по коду, а не прятались в выражениях привязки.
Старый Android и корпоративный десктоп - MVP разумная цель при рефакторинге Android-приложения образца 2016 года или приложения на Windows Forms: новый фреймворк не нужен, нужна дисциплина и по интерфейсу на экран.
Не для нескольких видов одной Model - на дашбордах, где одни и те же данные показаны сразу таблицей и графиком, правило «один Presenter на один View» только мешает; под такую форму лучше подходят MVC или MVVM.
Примеры и история
Mike Potel, «MVP: Model-View-Presenter - The Taligent Programming Model for C++ and Java» (Taligent Inc., 1996) - работа, давшая паттерну имя и описавшая громоздкий вариант с Interactor / Command / Selection, сделанный под операционную систему Taligent.
Andy Bower и Blair McGlashan, «Twisting the Triad» (Dolphin Smalltalk, около 2000) - текст, который свёл MVP к тому виду, каким им пользуются на практике: View подписан на Model, жестами занимается Presenter.
Martin Fowler, «GUI Architectures» (martinfowler.com, 2006) - делит MVP на Passive View и Supervising Controller и доказывает, что общий термин пора убрать; до сих пор основная точка отсчёта.
Google Web Toolkit, «Large scale application development and MVP» (Google, 2010) - официальное руководство GWT, где MVP назначен рекомендованной структурой для крупных GWT-приложений, а роль Presenter-ов играют Activities.
Android Architecture Blueprints - `todo-mvp` (Google, 2016-2019) - собственный образцовый репозиторий Google со стилем интерфейса Contract, который и задал канон MVP в Android-сообществе до Architecture Components.
Когда применять
- Когда логика представления на экране достаточно сложная (многошаговая проверка, условные поля, состояния ошибок), чтобы держать её под юнит-тестами без запущенного интерфейса.
- На платформах, где настоящий View поднимается медленно или его трудно автоматизировать - исторически это Android до ViewModel, GWT и Windows Forms.
Примеры из практики
- Приложения на Android примерно 2015-2019 годов активно строились на MVP: интерфейс
Contractс вложенными типамиViewиPresenterдля каждого экрана был самым распространённым приёмом в сообществе, пока Google не выпустилViewModelиLiveData. - Google Web Toolkit (GWT) предлагал MVP как официальную архитектуру для крупных приложений: Places и Activities соответствовали Presenter-ам, которые управляли пассивными виджетами View, скомпилированными в JavaScript.