MVC, MVP и MVVM
Где живут данные, где — кнопки, и кто их связывает: три классические схемы разделения интерфейса и логики.
MVC, MVP, MVVM — это семейство архитектурных паттернов уровня приложения, которые отвечают на один вопрос: как разделить «что приложение знает» (данные и правила), «что пользователь видит» (экран) и «что происходит при действиях пользователя» (логика связывания), чтобы эти части можно было менять и тестировать по отдельности.
Базовые паттерны GoF (Observer, Strategy, Command) — это кирпичики внутри одного объекта или небольшой группы. Архитектурные паттерны интерфейса — это уже план целого этажа: они описывают, как организовать весь слой представления приложения. Без такого разделения логика рисования, бизнес-правила и обработка кликов перемешиваются в одном классе на тысячу строк — его невозможно ни протестировать без запуска UI, ни переиспользовать модель в другом интерфейсе.
Три роли, которые есть всегда
Во всех трёх схемах присутствуют три ответственности, меняется лишь то, кто кого знает и как идёт поток данных:
- Model (Модель) — данные приложения и бизнес-правила. Не знает ничего про экран: ни про кнопки, ни про HTML. Это «мозг», который одинаково работает хоть в вебе, хоть в мобильном приложении.
- View (Представление) — то, что видит пользователь: поля, списки, кнопки. В идеале «глупое»: умеет показывать данные и сообщать о действиях, но само решений не принимает.
- Связующее звено — Controller, Presenter или ViewModel — координатор, который превращает действия пользователя в изменения модели и обновляет представление.
MVC: контроллер в центре
В классическом MVC поток такой: пользователь действует → Controller обрабатывает ввод и меняет Model → Model уведомляет View об изменении (через Observer) → View перечитывает данные модели и перерисовывается. Ключевая черта: View и Model знакомы напрямую — View подписан на модель.
class CounterModel:
def __init__(self):
self._value = 0
self._subscribers = []
def subscribe(self, callback):
self._subscribers.append(callback)
@property
def value(self):
return self._value
def increment(self):
self._value += 1
for callback in self._subscribers:
callback(self._value)
class ConsoleView:
def render(self, value):
print(f"[экран] счётчик = {value}")
class CounterController:
def __init__(self, model):
self.model = model
def on_button_click(self):
self.model.increment()
model = CounterModel()
view = ConsoleView()
controller = CounterController(model)
model.subscribe(view.render) # связывание: View слушает Model
controller.on_button_click()
controller.on_button_click()
controller.on_button_click()
Вывод:
[экран] счётчик = 1 [экран] счётчик = 2 [экран] счётчик = 3
Обратите внимание: CounterModel не импортирует и не упоминает ConsoleView. Заменим консоль на веб-страницу — модель не изменится ни на строку. Контроллер же тонкий: его дело — перевести «клик» в вызов increment(). MVC исторически родился в Smalltalk-80 и сегодня лежит в основе серверных фреймворков (Django, Ruby on Rails, ASP.NET MVC), где контроллер — это обработчик HTTP-запроса.
MVP: presenter как посредник
В MVP (Model-View-Presenter) View и Model больше не знают друг о друге — между ними стоит Presenter. View сообщает презентеру о действиях (через интерфейс), презентер дёргает модель и затем явно приказывает View обновиться вызовами вроде view.show_value(...). View становится совсем пассивным и прячется за интерфейсом — а значит, в тестах его легко заменить заглушкой.
| Аспект | MVC | MVP |
| View знает Model? | да, подписан напрямую | нет, только через Presenter |
| Кто обновляет View | View сам по уведомлению Model | Presenter явными вызовами |
| Тестируемость View | средняя | высокая (View за интерфейсом) |
MVP популярен там, где View «тяжёлый» и навязан платформой (десктоп WinForms, старый Android): презентер берёт всю логику на себя, а View остаётся тонкой оболочкой над виджетами.
MVVM: связывание данных
В MVVM (Model-View-ViewModel) появляется ViewModel — «модель для представления»: она хранит данные ровно в той форме, в какой их рисует экран (строки, флаги «кнопка активна»), и публикует их как наблюдаемые свойства. View связывается (data binding) с этими свойствами: меняется свойство ViewModel — экран обновляется сам, без ручных вызовов. Главное отличие от MVP: презентер толкает данные в View, а ViewModel их выставляет, а связывание (механизм фреймворка) переносит изменения автоматически.
// Псевдо-разметка двустороннего связывания (стиль WPF / Vue / Android)
<TextBlock text="{bind viewModel.displayValue}" />
<Button command="{bind viewModel.incrementCommand}" />
// ViewModel публикует наблюдаемые свойства:
class CounterViewModel {
observable displayValue = "0"
command incrementCommand = () => { value += 1; displayValue = str(value) }
}
MVVM лежит в основе WPF, Android Jetpack (ViewModel + LiveData), а его дух — в реактивных фреймворках вроде Vue и Angular, где шаблон автоматически отражает реактивное состояние. Платой становится «магия» связывания, которую сложнее отлаживать, чем явные вызовы MVP.
Как это работает под капотом
Все три схемы стоят на двух базовых идеях. Первая — принцип единственной ответственности: каждый класс отвечает за что-то одно, поэтому изменение правил расчёта не задевает разметку, и наоборот. Вторая — инверсия наблюдения: вместо того чтобы код рисования постоянно опрашивал модель «не изменилась ли ты?», модель сама сообщает об изменениях. В MVC это явный Observer (как subscribe в примере выше). В MVVM «подписку» прячет фреймворк: за наблюдаемым свойством стоит тот же механизм уведомлений (в .NET — событие PropertyChanged, во Vue — перехват через прокси/геттеры), а связывание — это сгенерированный подписчик, который при сигнале берёт новое значение свойства и пишет его в виджет. Поэтому «двустороннее связывание» — не волшебство, а два Observer-канала: из ViewModel в View и из ввода View обратно в ViewModel. Понимание этого снимает страх перед «магией»: вы всегда можете мысленно развернуть связывание обратно в подписки.
Частые ошибки
- Толстая View, тонкая всё остальное. Бизнес-правила («если баланс отрицательный — запретить») написаны прямо в обработчике клика. Их место — в Model; во View остаётся только отображение. Тест на правило не должен требовать запуска интерфейса.
- Контроллер-«бог». В MVC контроллер постепенно вбирает и валидацию, и работу с БД, и форматирование — и снова получается монолит. Контроллер только координирует; логика — в модели/сервисах.
- Model знает про View. Как только в модели появляется
import viewили строки разметки — переиспользование сломано, и в другом интерфейсе модель уже не запустить. - Путаница MVP и MVVM. Если вы вручную пишете
view.setText(...)— это MVP (явный push). Если меняете свойство и экран обновляется сам — это MVVM (связывание). Смешивать в одном экране оба подхода — источник трудноуловимых багов синхронизации. - Карго-культ. Для крошечного приложения на пару экранов MVVM с полным связыванием — оверинжиниринг; иногда хватает простого MVC. Паттерн выбирают под сложность и платформу, а не «потому что модно».
Итоги
- Во всех трёх схемах есть три роли: Model (данные и правила), View (экран), посредник (Controller / Presenter / ViewModel).
- MVC: контроллер обрабатывает ввод, View подписан на Model напрямую и сам обновляется. Основа серверных фреймворков.
- MVP: Presenter — единственный посредник, View пассивна и спрятана за интерфейсом, презентер обновляет её явными вызовами. Отлично тестируется.
- MVVM: ViewModel выставляет наблюдаемые свойства, View связывается с ними, обновление идёт автоматически через data binding. Основа WPF/Android/реактивных фреймворков.
- Главный критерий правильного разделения: Model не знает про View, а бизнес-правила можно протестировать без запуска интерфейса.