Designing is fundamentally about taking things apart... in such a way that they can be put back together. So separating things into things that can be composed that's what design is.
— Rich Hickey Design. Composition and Performance
Когда мы пишем сложные приложения, нам нужно выполнять различные операции, иногда совершенно друг на друга не похожие:
- обновить данные на сервере;
- показать всплывающее окно после клика пользователя;
- валидировать данные из формы;
- загрузить дополнительные ресурсы, картинки, скрипты;
- вызвать стороннее API и обработать ответ.
Считается хорошим тоном делить отличающийся код на модули, которые отвечают за свои конкретные задачи. Как именно разделить код на модули, по каким критериям и принципам — на эти вопросы старается ответить паттерн MVC.
Кратко
СкопированоMVC (сокращение от Model—View—Controller) — это архитектурный паттерн, который делит модули на три группы:
- модель (model),
- представление (view),
- контроллер (controller).
Модель содержит данные приложения, за которыми приходит пользователь. Например, список своих заказов в интернет-магазине.
Представление показывает эти данные в понятном для пользователя виде. Например, на свёрстанной странице сайта или в приложении на телефоне.
Контроллеры принимают пользовательские команды и преобразуют данные по этим командам. Например, если пользователь нажимает кнопку «Удалить заказ», то контроллер отмечает этот заказ в модели удалённым.
Компоненты архитектуры
СкопированоВ архитектуре MVC пользователь взаимодействует только с представлением — чаще всего это UI.
Пользователь подаёт команды программе. Контроллер получает эти команды и преобразует данные в модели. Модель обновляется и уведомляет представление о том, что нужно перерисовать интерфейс, чтобы отобразить изменения в данных.
Представим, что мы хотим написать приложение-фонарик. У него будет два состояния: включён и выключен. Состояния будут переключаться кнопкой «On/Off». Также у него будут кнопки включения дневного и ночного света, которые будут менять цвет лампочки на синий и жёлтый соответственно.
Попробуем написать его, используя паттерн MVC.
Модель
СкопированоМодель содержит данные приложения. Это самый независимый компонент архитектуры, именно от модели зависит, что будет показывать представление, и как будет работать контроллер.
В модели мы будем держать состояние фонарика. По умолчанию мы его выключим:
const flashLightModel = { isOn: false, color: "blue",}
const flashLightModel = { isOn: false, color: "blue", }
Когда пользователь включит фонарик, поле is
должно будет принять значение true
, за это будет отвечать контроллер. Поле color
содержит, каким цветом фонарик будет гореть.
Контроллер
СкопированоКонтроллер принимает команды от пользователя и преобразует данные в модели согласно этим командам. В нашем приложении контроллер будет содержать функцию для переключения состояния фонарика:
const flashLightController = { toggle() { flashLightModel.isOn = !flashLightModel.isOn },}
const flashLightController = { toggle() { flashLightModel.isOn = !flashLightModel.isOn }, }
Контроллер может принимать и обрабатывать данные от представления. Например, мы можем переключать цвет специальными кнопками, тогда контроллер проверит, какую кнопку нажали, чтобы включить нужный цвет:
const flashLightController = { // Остальной код selectColor(e) { const buttonName = e.target.name const buttonColors = { daylight: "blue", nightlight: "yellow", } const preferredColor = buttonColors[buttonName] flashLightModel.color = preferredColor },}
const flashLightController = { // Остальной код selectColor(e) { const buttonName = e.target.name const buttonColors = { daylight: "blue", nightlight: "yellow", } const preferredColor = buttonColors[buttonName] flashLightModel.color = preferredColor }, }
В примере выше контроллер проверяет, кнопку какого цвета нажали: дневного или ночного. В зависимости от нажатой кнопки он выбирает нужный цвет.
Представление
СкопированоПредставление показывает пользователю данные из модели в удобном и понятном виде. В нашем случае представлением будет собственно фонарик, который может гореть двумя цветами, а также кнопки для его включения и переключения цветов.
<div class="flashlight"></div><button type="button" name="power">Включить</button><button type="button" name="daylight">Дневной свет</button><button type="button" name="nightlight">Ночной свет</button>
<div class="flashlight"></div> <button type="button" name="power">Включить</button> <button type="button" name="daylight">Дневной свет</button> <button type="button" name="nightlight">Ночной свет</button>
Кроме разметки в представление также можно отнести код, который управляет отображением фонарика:
const flashLightView = { redraw() { const { isOn, color } = flashLightModel const flash = document.querySelector(".flashlight") flash.classList.add(`has-color-${color}`) if (isOn) { flash.classList.add("is-on") } },}flashLightView.redraw()
const flashLightView = { redraw() { const { isOn, color } = flashLightModel const flash = document.querySelector(".flashlight") flash.classList.add(`has-color-${color}`) if (isOn) { flash.classList.add("is-on") } }, } flashLightView.redraw()
А также — код для обработки событий, которые представление будет отдавать контроллеру:
const flashLightView = { // Остальной код initEvents() { const powerButton = document.querySelector(`[name="power"]`) powerButton.addEventListener("click", () => flashLightController.toggle()) // Код для событий других кнопок },}
const flashLightView = { // Остальной код initEvents() { const powerButton = document.querySelector(`[name="power"]`) powerButton.addEventListener("click", () => flashLightController.toggle()) // Код для событий других кнопок }, }
Взаимодействие компонентов
СкопированоПри использовании архитектуры MVC мы определяем, как компоненты будут общаться друг с другом — то есть определяем потоки данных.
Поток данных
СкопированоВ классическом MVC стандартом считается, когда данные:
- от пользователя передаются представлению;
- от представления — контроллеру;
- через контроллер обновляется модель;
- модель уведомляет представление о том, что что-то изменилось.
Иногда допускается, что компоненты могут общаться напрямую с другими компонентами не по этой схеме, но мы всё же рекомендуем не отходить от канона.
Представление или контроллер?
СкопированоВ MVC часто возникает вопрос, к чему отнести какой-то код: к представлению или контроллеру. В нашем примере выше даже есть такие места.
const flashLightView = { // ... initEvents() { const powerButton = document.querySelector(`[name="power"]`) powerButton.addEventListener("click", () => flashLightController.toggle()) },}
const flashLightView = { // ... initEvents() { const powerButton = document.querySelector(`[name="power"]`) powerButton.addEventListener("click", () => flashLightController.toggle()) }, }
Метод init
в представлении может относиться и к контроллеру, если мы решим, что централизованная обработка событий конкретных элементов — это задача контроллера.
Так же с методом select
в контроллере:
const flashLightController = { // Остальной код selectColor(e) { const buttonName = e.target.name const buttonColors = { daylight: "blue", nightlight: "yellow", } const preferredColor = buttonColors[buttonName] flashLightModel.color = preferredColor },}
const flashLightController = { // Остальной код selectColor(e) { const buttonName = e.target.name const buttonColors = { daylight: "blue", nightlight: "yellow", } const preferredColor = buttonColors[buttonName] flashLightModel.color = preferredColor }, }
Если мы решаем, что обработка событий — это задача представления, то мы можем отнести функцию выбора цвета в представление, а в контроллере оставить лишь метод для изменения цвета:
const flashLightController = { updateColor(color) { flashLightModel.color = color },}
const flashLightController = { updateColor(color) { flashLightModel.color = color }, }
Так как MVC позволяет пользователю обращаться напрямую к контроллеру, то конкретных правил здесь нет, только рекомендации:
- Стоит использовать последовательные правила для контроллера, модели и представления во всём проекте.
- Если правила нарушаются специально, это стоит зафиксировать в документации вместе с причиной.
- Правила стоит выводить из зон ответственности каждого компонента, которые стоит определить заранее.
Как много должен знать контроллер?
СкопированоДругой подобный вопрос, который возникает при работе с MVC — какая зона ответственности у контроллера, где она заканчивается.
При тонком контроллере знание о том, как преобразовывать данные, находится в модели. Это не всегда полезно, потому что может замусорить код модели, но иногда оправданно. Например, если мы не хотим «размазывать» это знание по нескольким компонентам.
Такая проблема называется проблемой тонкого и толстого контроллера. Разные команды решают её по-своему, исходя из договорённостей и выгод и издержек каждого варианта для своего проекта.
Похожие паттерны
СкопированоКроме MVC вы могли слышать о других вариациях этой аббревиатуры. Каждая из вариаций — это паттерн, слегка отличающийся от MVC спецификой или ответственностью компонентов.
Model—View—Viewmodel
СкопированоВ MVVM (сокращение от Model—View—Viewmodel) вместо контроллера используется Viewmodel. Это «надстройка» над представлением, которая связывает данные и представление так, что разработчикам больше не нужно писать самим логику обновления UI и обработки команд пользователя.
Для работы связывания нужен Binder (биндер) — фреймворк, библиотека или целый язык, который автоматически отображает изменения из модели в UI.
Model-View-Presenter
СкопированоВ MVP (сокращение от Model-View-Presenter) место контроллера занимает презентер.
Главное отличие от MVC в том, как расположены компоненты и, соответственно, как передаются данные. Если в MVC данные передавались по кругу, то в MVP компоненты располагаются по линии. На концах находятся модель и представление, а между ними — презентер.
Презентер забирает на себя всю логику обработки данных, обновления представления и обработки пользовательских команд.
Представление в этом случае пассивно: оно не делает ничего, кроме отображения данных так, как ему скажет презентер. Если в MVC представление могло брать форматирование вывода на себя, то в MVP за это тоже будет отвечать презентер.
Плюс такого подхода в том, что не возникает вопросов, какой код к чему относится. Минус — в том, что презентер быстро становится большим и сложным. Приходится разбивать его на модули поменьше, вероятно, добавлять дополнительные «слои».