MVVM

MVVM (Model-View-ViewModel) выносит состояние и поведение экрана в ViewModel - обычный объект с наблюдаемыми свойствами и командами, который ничего не знает о View. View описывает, какой виджет к какому свойству привязан, а синхронизацию в обе стороны берёт на себя слой связывания данных. Логику экрана можно тестировать без UI, и при этом не нужно писать ни одного ручного вызова обновления экрана.

Проблема

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

Решение

ViewModel хранит всё, что нужно экрану, в виде наблюдаемых свойств (текст в поле ввода, флаг доступности кнопки, готовые строки списка) и выставляет действия пользователя как команды (add, save). Ссылки на View у неё нет. View - это разметка, где у каждого виджета указано, к какому свойству он привязан. Биндер подписывается на свойства и обновляет виджеты при каждом изменении, а ввод пользователя записывает обратно в свойства. ViewModel обращается к Model за данными и бизнес-правилами, подписывается на её изменения и пересчитывает свои свойства - дальше биндер сам доносит их до экрана.

Как это работает

MVVM придумали в Microsoft под WPF (тогда ещё Avalon): архитекторы Кен Купер (Ken Cooper) и Тед Питерс (Ted Peters) искали способ строить интерфейсы на новой платформе с мощной привязкой данных. Публично паттерн описал Джон Госсман (John Gossman) в блоге в 2005 году. Главная мотивация была не только в тестах: в XAML разметку мог править дизайнер в отдельном инструменте, и нужна была чёткая граница: дизайнер правит разметку, не касаясь кода.

Идейно MVVM - это Presentation Model Мартина Фаулера (2004), приспособленная к платформе с привязкой данных. Presentation Model уже предлагала держать состояние и поведение экрана в отдельном объекте, не зависящем от виджетов, но синхронизацию с View оставляла на ручной код. MVVM делает тот же шаг, а синхронизацию отдаёт биндеру - отсюда и всё отличие.

Ключевое отличие от MVP - направление зависимости. Presenter знает интерфейс View и вызывает его методы; ViewModel не знает о View ничего, это View (через привязки) знает о ViewModel. Поэтому у ViewModel нет методов вида showError, есть только свойство errorText, а будет ли оно показано красной строкой, всплывающим окном или не показано вовсе, решает разметка.

Связывание бывает односторонним (свойство -> виджет: подпись, список, доступность кнопки) и двусторонним (поле ввода, где значение течёт в обе стороны). Действия пользователя оформляются командами: в WPF это ICommand с методами Execute и CanExecute, и кнопка сама становится недоступной, когда CanExecute возвращает false - ровно то, что в примере делает свойство canAdd.

В браузер MVVM принёс Knockout.js (Стив Сандерсон, 2010): ko.observable для свойств и атрибуты data-bind в HTML - пример на вкладке «Реализация» почти повторяет эту модель. Затем идею подхватили AngularJS с двусторонним ng-model и Vue, чья документация прямо называет MVVM одним из источников вдохновения. React пошёл другим путём - однонаправленный поток данных без двусторонней привязки, - но разделение на «состояние и логику» и «декларативное описание экрана» у него то же.

В Android MVVM стал официальным курсом после Architecture Components (Google I/O 2017): класс ViewModel переживает поворот экрана, состояние лежит в LiveData, позже в StateFlow, а UI подписывается на него. С приходом Jetpack Compose и SwiftUI на iOS (ObservableObject, @Published) View стало функцией от состояния, и привязка превратилась в простое «перерисуй, когда свойство изменилось».

Слабые места известны давно, и о части из них писал сам Госсман вскоре после публикации: на простом экране MVVM избыточен, а на большом трудно заранее спроектировать ViewModel. Привязки декларативны, поэтому опечатка в имени свойства часто не даёт ни ошибки компиляции, ни исключения - виджет просто остаётся пустым. На практике частая болезнь - «толстая ViewModel»: в неё начинают складывать бизнес-логику, которой место в Model, и она разрастается так же, как Controller в плохом MVC.

Итог для всего семейства: MVC, MVP и MVVM отвечают на один вопрос - кто доставляет изменения на экран. В MVC это делает сам View, читая Model; в MVP - Presenter явными вызовами через интерфейс; в MVVM - инфраструктура привязки, а ViewModel об экране вообще не знает. Чем дальше по этому ряду, тем меньше ручного кода и тем больше работы перекладывается на платформу.

Нюансы выбора

Платформа с готовой привязкой - WPF, .NET MAUI, Android с Jetpack, SwiftUI, Vue, Angular. Здесь MVVM - путь наименьшего сопротивления: инфраструктура уже написана, и паттерн просто объясняет, как ей пользоваться.

Вместо MVP - когда интерфейс View разросся до десятков методов showX, а Presenter в основном переносит значения туда и обратно. Если биндер доступен, этот код можно удалить, а логика останется в ViewModel и под тестами.

Против MVVM - если биндера нет, а писать свой нет причины. Самодельная привязка быстро обрастает краевыми случаями (циклы обновлений, списки, фокус), и явные вызовы MVP в такой ситуации честнее и проще в отладке.

Несколько видов одних данных - когда одно и то же состояние показывается таблицей и графиком, в компактном и полном режиме. Одну ViewModel привязывают к нескольким View, и правило «один Presenter на один View» из MVP не мешает.

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

Примеры и история

John Gossman, «Introduction to Model/View/ViewModel pattern for building WPF apps» (блог, 2005) - первая публичная запись о паттерне от одного из архитекторов WPF.

Martin Fowler, «Presentation Model» (martinfowler.com, 2004) - описание паттерна, из которого вырос MVVM: состояние и поведение экрана в отдельном объекте, независимом от виджетов.

Josh Smith, «WPF Apps With The Model-View-ViewModel Design Pattern» (MSDN Magazine, 2009) - статья, после которой MVVM стал стандартом в .NET-сообществе; в ней разобран RelayCommand - простая реализация ICommand.

Knockout.js (Steve Sanderson, 2010) - первая популярная MVVM-библиотека для браузера с ko.observable и атрибутами data-bind; на её модель похож пример на вкладке «Реализация».

Android Architecture Components (Google I/O 2017) - ViewModel и LiveData, с которыми Google сделал MVVM рекомендуемой архитектурой Android-приложений вместо MVP.

Когда применять

  • Когда платформа уже предоставляет привязку данных - WPF и .NET MAUI, Android с ViewModel и Compose, SwiftUI, Vue или Angular - и экран содержит много полей, состояний и производных значений.
  • Когда логику экрана нужно покрыть тестами, а разметку будут часто менять или делить между несколькими видами одних и тех же данных.

Примеры из практики

  • WPF, UWP и .NET MAUI - MVVM здесь стандартный подход: ViewModel реализует INotifyPropertyChanged, действия выставляются через ICommand, а XAML связывает виджеты выражениями вроде {Binding Name, Mode=TwoWay}.
  • Android Jetpack - класс ViewModel хранит состояние экрана в LiveData или StateFlow, а Activity, Fragment или функция Compose подписываются на него и перерисовываются сами. Этот подход заменил в Android-сообществе прежний MVP.

Похожие паттерны