Слой представления

Как разделить данные, экран и ввод пользователя: MVC, MVP, MVVM

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

MVC, MVP и MVVM - три классических паттерна этого семейства, и все три ведут родословную к одной и той же идее Трюгве Реенскауга 1979 года: данные (Model) не должны знать об экране, а экран должен лишь отображать данные. Разница между тремя паттернами - не в этом общем правиле, а в двух конкретных вопросах.

  • Кто общается с View напрямую: в MVC это Controller, обычно без всякой абстракции между ним и конкретным виджетом. В MVP - Presenter, но уже через абстрактный интерфейс View, который легко подменить тестовой заглушкой.
  • Как View узнаёт об изменениях: явный вызов метода отрисовки (Passive View), подписка через паттерн Observer, или автоматическое двустороннее связывание (data binding) в MVVM, где ViewModel вообще не знает о существовании View.

Универсально лучшего варианта нет - выбор паттерна определяется тем, какие средства связывания данных и экрана предоставляет платформа.

  • MVC - естественный выбор для серверных веб-фреймворков (Rails, Django, Spring MVC): Controller создаётся заново на каждый HTTP-запрос, долгоживущее состояние экрана там и не нужно.
  • MVP - когда Presenter обязан тестироваться юнит-тестами без реального UI: абстрактный интерфейс View позволяет подменить экран моком и проверить логику в изоляции.
  • MVVM - когда платформа сама умеет data binding (WPF/XAML, Angular, Vue, SwiftUI): ViewModel просто выставляет наблюдаемые свойства, а View подписывается на них декларативно, без ручного кода синхронизации.

Практическое правило: если платформа даёт готовый механизм связывания - берите MVVM. Если нужна тестируемость логики без готового биндинга - MVP. Если экран живёт ровно один запрос-ответ - классический MVC.

Связывание данных (data binding) - механизм, которым View узнаёт об изменении Model или ViewModel. Он бывает разной степени автоматизации, и от выбора зависит, сколько кода придётся писать вручную.

  • Одностороннее связывание (one-way binding): данные текут только от Model/ViewModel к View - типичная схема React или Vue при обычном рендере: состояние поменялось, компонент перерисовался, обратного пути нет.
  • Двустороннее связывание (two-way binding): View может напрямую менять Model/ViewModel в ответ на ввод пользователя - классика Angular (ngModel) и WPF, удобно для форм, но усложняет отслеживание источника изменения.
  • Явное уведомление (push через Observer): Model сама рассылает событие подписчикам при изменении - оригинальный механизм MVC в Smalltalk и NSNotificationCenter в Cocoa AppKit.
  • Ручной вызов (Passive View): ни подписки, ни автоматического биндинга нет вообще - Controller или Presenter явно вызывает метод View после каждого изменения, и это самый простой вариант для мокирования в тестах.

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

  • По месту, где живёт решающая логика: Controller в MVC, Presenter в MVP, ViewModel в MVVM - в каждом паттерне это один и тот же по смыслу компонент под разным именем.
  • По механизму уведомления View: ручной вызов (Passive View), подписка Observer, автоматический data binding - от самого явного к самому неявному.
  • По тестируемости без реального UI: MVC сложнее всего - Controller обычно трогает View напрямую. MVP проще - интерфейс View подменяется моком. MVVM проще всего - ViewModel вообще не хранит ссылку на View.
  • По накладным расходам: MVC добавляет меньше всего кода на маленьком экране, MVVM требует инфраструктуры для observable-свойств, но окупается на экранах с большим количеством полей и сложной валидацией.

MVC

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

MVP

MVP развивает MVC и доводит View до полной пассивности: он не выполняет никакой логики и лишь передаёт действия пользователя в Presenter, а для обновления экрана предоставляет несколько простых методов. Presenter закреплён за своим View один к одному, работает с ним через интерфейс, меняет Model и готовит её данные к выводу. За счёт этого всю логику представления можно покрыть юнит-тестами, подставив вместо настоящего View заглушку.