Відмінності між MVVM та іншими MV*-патернами


Від перекладача:
Вже опубліковано багато матеріалів по 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 і багато інших:
mv_patterns_img
Дивлячись на схеми, ви, звичайно ж, бачите, що стрілки показують відносини між компонентами. Але чи тільки в цьому різниця? Є 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? (Ні, але ви можете побачити, звідки взялася плутанина).
mvc_img
Що ж являють собою ці 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 від логіки інтерфейсу (що відбувається при взаємодії з віджетами) і від даних (які дані відображати на екрані).
mvp_img
  • 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 являє собою логічне представлення користувальницького інтерфейсу, не спираючись на будь-які візуальні елементи.
presentation_model_img
PresentationModel має декілька обов'язків:
  1. Містить логіку інтерфейсу:
    Так само, як і презентер, PresentationModel містить логіку користувальницького інтерфейсу. Коли ви натискаєте на кнопку, це подія направляється в PresentationModel, яка потім вирішує, що з ним робити.
  2. Надає дані з моделі для відображення на екрані
    PresentationModel може перетворювати дані з моделі так, щоб вони були відображені на екрані. Часто інформація, що міститься в моделі, не може безпосередньо використовуватися на екрані. Вам, можливо, спочатку потрібно перетворити дані, доповнити або зібрати з декількох джерел. Це найбільш ймовірно, коли у вас немає повного контролю над моделлю. Наприклад, якщо ви отримуєте дані від сторонніх веб-сервісів або ж з бази даних існуючого програми.
  3. Зберігає стан користувальницького інтерфейсу
    Найчастіше користувальницький інтерфейс повинен зберігати додаткову інформацію, яка не має нічого спільного з моделлю. Наприклад, який елемент вибрано в даний момент на екрані? Які помилки валідації відбулися? PresentationModel може зберігати цю інформацію властивості.
View може легко витягувати дані з PresentationModel і отримувати всю необхідну інформацію для відображення на екрані. Одна з переваг такого підходу полягає в тому, що ви можете створити логічне і повністю досліджуване представлення вашого UI, не покладаючись на тестування візуальних елементів.
Патерн Presentation Model ніяк не описує, яким чином View використовує дані з моделі (PresentationModel).
Model-View-ViewModel
Нарешті, патерн Model-View-ViewModel також відомий, як MVVM або просто шаблон ViewModel. Він дуже схожий на патерн Presentation Model:
mvvm_img
насправді чи не єдиною відмінністю є явне використання можливостей зв'язування даних (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 за допомогу при підготовці перекладу.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.