MVC

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

Проблема

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

Решение

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

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

MVC придумал Трюгве Реенскауг (Trygve Reenskaug) в 1978-1979 годах в исследовательском центре Xerox PARC, работая над средой Smalltalk-79. Его первоначальное название было длиннее и точнее сути - «Thing-Model-View-Editor»: «Thing» обозначало объект предметной области, а «Editor» позже сократили до «Controller». Цель была не архитектурной абстракцией ради красоты, а конкретной инженерной задачей - позволить конечным пользователям Smalltalk самим собирать интерфейсы из готовых блоков, не переписывая бизнес-логику.

В оригинальной задумке View - это не один класс, а потенциально несколько объектов, одновременно наблюдающих за одной Model через классический паттерн Observer: изменение Model рассылает уведомление всем подписанным View сразу. Controller изначально был жёстко связан один-к-одному со своим View, образуя маленький «триад» Model-View-Controller, и такие триады можно было вкладывать друг в друга - составной View собирался из под-View, у каждого из которых был свой Controller.

Спустя почти три десятилетия Мартин Фаулер (Martin Fowler) в статье «GUI Architectures» (2006) формализовал два варианта того, как именно View узнаёт об изменениях Model: Passive View, где Controller (или Presenter) явно дёргает методы View после каждого изменения, и Supervising Controller, где View сама подписана на простые изменения через data binding, а Controller вмешивается только в сложную логику. Первый вариант проще тестировать - можно проверить, что Controller вызвал нужный метод View через mock-объект, вообще не поднимая реальный UI.

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

Веб-фреймворки вроде Ruby on Rails и Django называют себя MVC, но реализуют его в урезанном виде: на каждый HTTP-запрос создаётся новый экземпляр Controller-а, который один раз читает Model и один раз рендерит View в HTML-ответ - никакого долгоживущего объекта, который «уведомляет» View о будущих изменениях, там попросту нет. То, что в оригинальном Smalltalk-варианте было подпиской через Observer, в вебе превращается в простой прямой вызов метода рендеринга внутри тела обработчика запроса.

Настольные и десктопные GUI-среды, напротив, сохраняют настоящий цикл уведомления. Классический Apple Cocoa AppKit использует NSNotificationCenter и key-value observing именно для того, чтобы Model могла оповестить произвольное число View о своём изменении без прямой ссылки на них - это ближе всего к тому, что задумывал Реенскауг в 1979 году, чем request-response модель веба.

На практике именно Controller чаще всего перестаёт соответствовать первоначальному замыслу. В маленьком приложении с одним экраном граница между «разбором пользовательского события» и «отображением результата» легко стирается, и код, который по идее принадлежит View, оседает в обработчике клика внутри Controller-а - паттерн не содержит механизма, который бы автоматически удерживал эту границу на месте.

Спустя более 45 лет с публикации оригинального меморандума Реенскауга ядро идеи не изменилось: View и Controller знают о Model, а Model ничего не знает ни о том, ни о другом. Именно эта односторонняя зависимость - а не конкретный механизм уведомления, который различается от Smalltalk до Rails, - и есть то единственное, что должно пережить перенос MVC на новую платформу.

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

Настольные приложения с долгоживущей Model - редакторы, IDE, графические программы, где один и тот же объект Model меняется сотни раз за сессию и реально выгодно держать постоянную подписку View на его изменения, как в оригинальном замысле Реенскауга.

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

Серверные веб-фреймворки с циклом запрос-ответ - Rails, Django и подобные им фреймворки, где Controller и так создаётся заново на каждый запрос, так что урезанная версия MVC без постоянной подписки View ничего не теряет по сравнению с оригинальной схемой.

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

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

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

Trygve Reenskaug, внутренний меморандум «Models-Views-Controllers» (Xerox PARC, 1979) - оригинальный документ, впервые описавший паттерн и его мотивацию для системы Smalltalk-79.

Adele Goldberg и David Robson, «Smalltalk-80: The Language and Its Implementation» (1983, «Голубая книга») - формализовала MVC как часть стандартной библиотеки Smalltalk-80 для широкой аудитории разработчиков.

Martin Fowler, «GUI Architectures» (martinfowler.com, 2006) - статья, которая ввела разграничение Passive View / Supervising Controller и до сих пор остаётся стандартной точкой отсчёта при сравнении презентационных паттернов.

ASP.NET MVC (Microsoft, 2009) - явно назван в честь паттерна и выпущен как альтернатива ASP.NET WebForms именно ради того разделения ответственностей, которого не хватало в более старом фреймворке.

NSNotificationCenter в Apple Cocoa AppKit (с 1988 года, NeXTSTEP) - механизм рассылки уведомлений, который до сих пор используется в macOS-приложениях как реальная реализация оповещения View со стороны Model, максимально близкая к оригинальному замыслу Реенскауга.

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

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

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

  • Ruby on Rails и Django организуют серверный код вокруг MVC: маршрут ведёт к методу Controller-а, тот обращается к Model (ActiveRecord/ORM) и рендерит View-шаблон.
  • Spring MVC в Java-экосистеме и классический Apple Cocoa/UIKit до появления SwiftUI - оба явно называют свои базовые классы Model, View и Controller.

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