От переводчика:
Уже опубликовано много материалов по MVC и его производным паттернам, но каждый понимает их по-своему. На этой почве возникают разногласия и холивары. Даже опытные разработчики спорят о том, в чем отличие между MVP, MVVM и Presentation Model и что должен делать тот или иной компонент в каждом паттерне. Ситуация усугубляется еще и тем, что многие не знают истинную роль контроллера в классическом варианте MVC. Предлагаю вашему вниманию перевод хорошей обзорной статьи, которая многое проясняет и расставляет всё по своим местам.
Прежде чем мы начнем погружаться в детали паттерна Model-View-ViewModel (MVVM), я думаю, будет полезно описать сходства и различия между MVVM и другими шаблонами проектирования для разделения модели и представления (MV*-паттерны).
Существует довольно много MV*-паттернов: Model-View-Controller, Model-View-Presenter, Presentation Model, Passive View, Supervising Controller, Model-View-ViewModel и многие другие:
Глядя на схемы, вы, конечно же, видите, что стрелки показывают отношения между компонентами. Но только ли в этом различие? Является ли Controller тем же, что и Presenter или PresentationModel? Как бы вы сравнили между собой Model-View-Presenter и Model-View-ViewModel? В данной статье я собираюсь описать сходства и различия между наиболее распространенными MV*-паттернами.
Построение UI без использования MV*-паттернов
Как бы вы построили пользовательский интерфейс (UI), не используя вышеперечисленные паттерны? Взяли бы форму, добавили на нее виджеты, а логику написали бы в коде. Такой код, описывающий логику View, жёстко связан с пользовательским интерфейсом, так как он напрямую взаимодействует с элементами на экране. Это хороший, но прямолинейный подход. Он применим только для очень простых интерфейсов. Когда логика становится более сложной, поддержка такого UI может превратиться в кошмар!
Корень проблемы заключается в том, что построение UI таким способом нарушает принцип единственной ответственности (single responsibility principle), который гласит: «У класса должна быть только одна причина для изменения». Если UI-компонент содержит код для отображения, логики и данных, то у него есть несколько причин для изменения. Например, если вы хотите поменять тип пользовательского элемента, который используется для отображения данных, то изменения не должны повлиять на логику. Однако поскольку логика так тесно связана с элементами управления, ее тоже придется менять. Это так называемый «код с душком» (code smell), который сигнализирует, что принцип единственной ответственности нарушен.
Таким образом, если форма содержит код для отображения элементов управления, логику интерфейса (что происходит при нажатии кнопки) и данные для отображения на экране, вы столкнетесь со следующими проблемами:
Усложнение поддержки
Изменения в UI, логике или данных, скорее всего, повлекут за собой изменения в остальных частях. Поэтому вносить правки гораздо сложнее, что затрудняет поддержку.
Ухудшение тестируемости
Логика и данные приложения могут быть написаны таким образом, чтобы каждый компонент мог быть протестирован отдельно. Однако код, связанный с пользовательским интерфейсом, плохо поддается модульному тестированию, потому что для этого часто требуется участие пользователя для запуска логики в UI. Кроме того, любая визуализация часто требует оценки со стороны человека, что всё «выглядит правильно». Отметим, что существуют решения для автоматизации тестирования пользовательского интерфейса. Однако они только имитируют взаимодействие с пользователем. Как правило, они сложнее в настройке и обслуживании, чем unit-тесты, и чаще всего используются при интеграционном тестировании, так как для этого требуется запуск всего приложения.
- Уменьшение возможности переиспользования
Если ваш UI-код смешан с кодом логики и данных, то его становится гораздо сложнее переиспользовать.
Цели MV*-паттернов
Хотя каждый из паттернов имеет довольно много отличий, их цели похожи: отделить UI-код (View) от кода логики (Presenter, Controller, ViewModel и т. д.) и кода обработки данных (Model). Это позволяет каждому из них развиваться самостоятельно. Например, вы сможете изменить внешний вид и стиль приложения, не затрагивая логику и данные.
Кроме того, так как логика и данные отделены от отображения, то они могут быть протестированы отдельно. Для простых приложений это может быть не так важно. Например, если ваше приложение является простым редактором данных. Однако, если у вас более сложная логика интерфейса, то возможность автоматически проверить, что она работает правильно, будет очень ценной.
Model-View-Controller
Одним из самых первых паттернов для отделения представления от логики и модели стал Model-View-Controller (MVC). Эта концепция была описана Трюгве Реенскаугом.
В 1979 году! (Я тогда ещё даже не родился).
Этот паттерн был разработан для написания приложений на Smalltalk. Но в те дни, программирование было не таким, как сегодня. Не было Windows. Не было графического интерфейса пользователя. Не было библиотек виджетов. Если вы хотите пользовательский интерфейс, его нужно отрисовать самостоятельно. Или если вы хотите взаимодействовать с устройствами ввода, такими как клавиатура.
Но то, что сделал Трюгве было весьма революционным. Там, где все смешивали код отображения, логики и данных, он применил паттерн, чтобы разделить эти обязанности между отдельными классами.
Проблема паттерна MVC состоит в том, что это, вероятно, один из самых неправильно понятых паттернов в мире. И я думаю, это из-за названия. Трюгве сначала назвал паттерн Model-View-Editor, но позже остановился на Model-View-Controller. Понятно, что такое Model (данные) и что такое View (то, что я вижу на экране). Но что такое Сontroller? Является ли Application Controller таким же, как в паттерне MVC? (Нет, но вы можете увидеть, откуда взялась путаница).
Что же представляют собой эти Model, View и Controller:
Model
Модель – это данные вашего приложения, логика их получения и сохранения. Зачастую это модель предметной области (domain model), основанная на базе данных или на результатах от веб-сервисов. В некоторых случаях domain model хорошо проецируется на то, что вы видите на экране. Но иногда перед использованием ее необходимо адаптировать, изменить или расширить.
View
View отвечала за отображение UI на экране. Без библиотек виджетов, это означало самостоятельную отрисовку блоков, кнопок, полей ввода и т. п. View также может наблюдать за моделью и отображать данные из неё.
- Controller
Controller обрабатывает действия пользователя и затем обновляет Model или View. Если пользователь взаимодействует с приложением (нажимает кнопки на клавиатуре, передвигает курсор мыши), контроллер получает уведомление об этих действиях и решает, что с ними делать.
Примечание от переводчика:
Следует отметить, что Controller получает события ввода напрямую, а не через View. Контроллер интерпретирует пользовательский ввод от клавиатуры или мыши, и посылает команды модели и/или представлению внести соответствующие изменения.
Я написал пример на скорую руку для иллюстрации того, как будет выглядеть контроллер в «чистой» реализации MVC. Я его реализовал на обычном asp.net (не asp.net MVC), но без применения каких-либо пользовательских элементов управления. Так что это более традиционный asp стиль. (Да, это не очень удачный пример, но я надеюсь, что он станет отправной точкой к пониманию истинной роли контроллера).
public class Controller
{
private readonly IView _view;
public Controller(IView view)
{
_view = view;
HttpRequest request = HttpContext.Current.Request;
if (request.Form["ShowPerson"] == "1")
{
if (string.IsNullOrEmpty(request.Form["Id"]))
{
ShowError("The ID was missing");
return;
}
ShowPerson(Convert.ToInt32(request.Form["Id"]));
}
}
private void ShowError(string s)
{
_view.ShowError(s);
}
private void ShowPerson(int Id)
{
var model = new Repository().GetModel(Id);
_view.ShowPerson(model);
}
}
После многих лет парадигма программирования несколько изменилась – появились пользовательские элементы управления (виджеты). Виджеты как отрисовывают самих себя, так и интерпретируют пользовательский ввод. Кнопка знает, что делать, если вы кликните по ней. Поле ввода знает, что делать, если вы вводите текст в нём. Это уменьшает потребность в контроллере, и паттерн MVC стал менее актуальным. Однако так как по-прежнему существует необходимость отделения логики приложения от представления и от данных, набрал популярность другой паттерн под названием Model-View-Presenter (MVP).
Большинство примеров паттерна MVC фокусируются на очень небольших компонентах, таких как реализация текстового окна или реализация кнопки. При использовании более современных технологий пользовательского интерфейса (Visual Basic 3 является современным по сравнению со Smalltalk 1979), как правило, нет необходимости в этом паттерне. Но он может помочь, если вы разрабатываете свой виджет, используя очень низкий уровень API (например, Direct X).
Последние пару лет паттерн MVC стал снова актуальным, но уже по другой причине, в связи с появлением ASP.NET MVC. Фреймворк ASP.NET MVC не использует концепцию виджетов в отличии от ASP.NET. В ASP.NET MVC View представляет собой элемент управления ASPX, который отрисовывает HTML. И контроллер снова обрабатывает действия пользователя, так как он принимает HTTP запросы. На основании http-запроса он определяет, что делать (обновить Model или отобразить конкретную View).
Model-View-Presenter
С развитием среды визуального программирования и внедрения виджетов, которые инкапсулирует отрисовку и обработку пользовательского ввода, отпала необходимость в создании отдельного класса контроллера. Но разработчики всё ещё нуждаются в отделении логики от представления, только теперь на более высоком уровне абстракции. Потому что оказалось, что если вы создаете форму из нескольких пользовательских элементов, она также содержит и логику интерфейса и данных. Паттерн MVP описывает, как отделить UI от логики интерфейса (что происходит при взаимодействии с виджетами) и от данных (какие данные отображать на экране).
Model
Это данные вашего приложения, логика их получения и сохранения. Зачастую она основана на базе данных или на результатах от веб-сервисов. В некоторых случаях потребуется ее адаптировать, изменить или расширить перед использованием во View.
View
Обычно представляет собой форму с виджетами. Пользователь может взаимодействовать с ее элементами, но когда какое-нибудь событие виджета будет затрагивать логику интерфейса, View будет направлять его презентеру.
- Presenter
Презентер содержит всю логику пользовательского интерфейса и отвечает за синхронизацию модели и представления. Когда представление уведомляет презентер, что пользователь что-то сделал (например, нажал кнопку), презентер принимает решение об обновлении модели и синхронизирует все изменения между моделью и представлением.
Стоит отметить одну важную вещь, что презентер не общается с представлением напрямую. Вместо этого, он общается через интерфейс. Благодаря этому презентер и модель могут быть протестированы по отдельности.
Существует два варианта этого паттерна: Passive View и Supervising Controller.
Passive View
В этом варианте MVP представление ничего не знает о модели, но вместо этого предоставляет простые свойства для всей информации, которую необходимо отобразить на экране. Презентер будет считывать информацию из модели и обновлять свойства во View.
Это было бы примером PassiveView:
public PersonalDataView : UserControl, IPersonalDataView
{
TextBox _firstNameTextBox;
public string FirstName
{
get
{
return _firstNameTextBox.Value;
}
set
{
_firstNameTextBox.Value = value;
}
}
}
Как вы можете видеть, требуется писать довольно много кода как во View, так и в презентере. Тем не менее, это сделает взаимодействие между ними более тестируемым.
Supervising Controller
В этом варианте MVP представление знает о модели и отвечает за связывание данных с отображением. Это делает общение между презентером и View более лаконичным, но в ущерб тестируемости взаимодействия View-Presenter. Лично я ненавижу тот факт, что этот паттерн содержит в названии «Controller». Потому что контроллер снова не тот, что в MVC и не такой, как Application Controller.
Это было бы примером представления в паттерне Supervising Controller:
public class PersonalDataView : UserControl, IPersonalDataView
{
protected TextBox _firstNameTextBox;
public void SetPersonalData(PersonalData data)
{
_firstNameTextBox.Value = data.FirstName;
}
public void UpdatePersonalData(PersonalData data)
{
data.FirstName = _firstNameTextBox.Value;
}
}
Как вы можете видеть, этот интерфейс является менее детальным и возлагает больше ответственности на View.
Presentation Model
Мартин Фаулер описывает на своем сайте другой подход для достижения разделения ответственности, который называется Presentation Model. PresentationModel представляет собой логическое представление пользовательского интерфейса, не опираясь на какие-либо визуальные элементы.
PresentationModel имеет несколько обязанностей:
Содержит логику пользовательского интерфейса:
Так же, как и презентер, PresentationModel содержит логику пользовательского интерфейса. Когда вы нажимаете на кнопку, это событие направляется в PresentationModel, которая затем решает, что с ним делать.
Предоставляет данные из модели для отображения на экране
PresentationModel может преобразовывать данные из модели так, чтобы они были легко отображены на экране. Часто информация, содержащаяся в модели, не может непосредственно использоваться на экране. Вам, возможно, сначала потребуется преобразовать данные, их дополнить или собрать из нескольких источников. Это наиболее вероятно, когда у вас нет полного контроля над моделью. Например, если вы получаете данные от сторонних веб-сервисов или же из базы данных существующего приложения.
- Хранит состояние пользовательского интерфейса
Зачастую пользовательский интерфейс должен хранить дополнительную информацию, которая не имеет ничего общего с моделью. Например, какой элемент выбран в данный момент на экране? Какие ошибки валидации произошли? PresentationModel может хранить эту информацию в свойствах.
View может легко извлекать данные из PresentationModel и получать всю необходимую информацию для отображения на экране. Одно из преимуществ такого подхода заключается в том, что вы можете создать логическое и полностью тестируемое представление вашего UI, не полагаясь на тестирование визуальных элементов.
Паттерн Presentation Model никак не описывает, каким образом View использует данные из модели (PresentationModel).
Model-View-ViewModel
Наконец, паттерн Model-View-ViewModel также известный, как MVVM или просто шаблон ViewModel. Он очень похож на паттерн Presentation Model:
В действительности едва ли не единственным отличием является явное использование возможностей связывания данных (databinding) в WPF и Silverlight. Не удивительно, потому что Джон Госсман был одним из первых, кто упомянул об этом паттерне в своем блоге.
ViewModel не может общаться со View напрямую. Вместо этого она представляет легко связываемые свойства и методы в виде команд. View может привязываться к этим свойствам, чтобы получать информацию из ViewModel и вызывать на ней команды (методы). Это не требует того, чтобы View знала о ViewModel. XAML Databinding использует рефлексию, чтобы связать View и ViewModel. Таким образом, вы можете использовать любую ViewModel для View, которая предоставляет нужные свойства.
Некоторые из вещей, которые мне действительно нравится в этом паттерне, когда он применяется к Silverlight или WPF:
Вы получаете полностью тестируемую логическую модель вашего приложения.
Поскольку ViewModel предоставляет View всю необходимую информацию в удобном виде, то само представление может быть довольно простым. А дизайнер может экспериментировать с внешним видом и стилем в редакторе Expression Blend и изменять его, не влияя на пользовательский интерфейс.
- И, наконец, вы можете избежать написания кода для View (code behind). Теперь это повод для споров среди поклонников паттерна MVVM. Я лично считаю, что, как правило, вам не нужно писать дополнительный код для View, и найдется решение лучше. Да, иногда нужно проделать некоторые трюки (такие как создание attached behaviors), но они обеспечивают хорошие и переиспользуемые решения. Тем не менее я также признаю, что не все любят XAML разметку и связывание данных в XAML. Паттерн ViewModel не заставляет вас использовать или избегать code behind. Делайте то, что кажется вам правильным.
Заключение
Я надеюсь, что это описание наиболее распространенных MV*-паттернов поможет вам понять их различия.
Комментарий переводчика:
Как и автор, я надеюсь, что это описание поможет вам понять сходства и различия MV*-паттернов. Разобравшись в них, вам будет легче принять решение, какой из паттернов применить в своем приложении.
Основные выводы, которые можно сделать из статьи:
- Модель во всех паттернах выглядит одинаково и имеет одну и ту же цель – получение, обработка, а также сохранение данных.
- В классическом MVC пользовательский ввод обрабатывает Controller, а не View.
- Современные ОС и библиотеки виджетов берут на себя обработку пользовательского ввода, поэтому у вас больше нет нужды в контроллере из паттерна MVC.
- Цель MV*-паттернов: отделить друг от друга отображение UI, логику интерфейса и данные (их получение и обработку).
- Используя MV*-паттерн в своем приложении, вы упрощаете его поддержку и тестирование, отделяете данные от способа их визуализации.
- MVP достаточно универсальный паттерн и подойдет во многих случаях (это мое личное мнение). Какой вариант использовать: Passive View или Supervising Controller – решать вам. Руководствуйтесь тем, что вам нужно: больше контроля и тестируемости либо лаконичности и краткости кода. Лавируйте между задачами и применяйте тот или другой подход.
- Если в системе присутствует хорошая реализация автоматического связывания данных (databinding), то MVVM – это ваш выбор.
- Presentation Model – хорошая альтернатива MVVM, и будет полезна там, где нет автоматического связывания. Но вам придется писать код связывания самостоятельно (это несложный, но рутинный код). Есть идеи, как это элегантно реализовать, но об этом мы поговорим в следующей статье.
P.S. Отдельно хочу поблагодарить своего коллегу Jeevuz за помощь при подготовке перевода.