? Предисловие
Формат постов не зашел для Хабра, поэтому буду собирать сразу несколько постов в одну статью
И для начала, очередной разбор MV* шаблонов, но с интересными деталями
Даже опытные разработчики смогут найти что-то новое для себя
(правда только со следующей статьи, это статья просто вводная)
Оригиналы этих постов можно почитать в тг канале НеКрутой Архитектор
Там набирается материал для будущих статей с сильным опережением
План:
? M-V-подставь_свое
Model‑View‑*** — это шаблоны проектирования представления данных клиенту
Важное уточнение: это шаблоны проектирования только для presentation слоя, а не для целого приложения
? Model
— это как раз та часть приложения о которой мы ближайшее время говорить не будем
Чаще всего ее называют бизнес логикой, но это не совсем верно
Пока, лучше всего воспринимать ее как модель данных которую наше приложение хочет передать пользователю
Причем не важно что видит пользователь: Ui или API, или что‑то другое
MV шаблоны не про frontend, а про то как передать данные клиенту, кем бы он ни был
?️ View
— это само представление данных пользователю
То, что он по итогу получит
В мире андройд это обычно Activity
, Fragment
, View
или Compose
?***
— это какая‑то прослойка между моделью данных и их представлением пользователю
Для чего все эти шаблоны вообще придумали?
? Дело в том, что View это какой‑то вариант отображения данных клиенту
⚽️ И часто возникает ситуация, что нужно создать другой вариант отображения
? Это может быть как бизнес потребность в перекраске кнопок c a/b тестами,
? или желание разработчиков заменить технологию отображения
Например, у тебя было консольное приложение,
а ты вдруг захотели сделать для него UI (GitBash | GitUi здравствуйте),
ну или решил заменить Android.View
на Compose
Во всех таких случаях хочется чем‑то разделить саму View и работу с Model,
чтобы иметь возможность заменить только View, не трогая ничего другого
В Android, к этому еще добавляется проблема пересоздания View при изменении конфигурации
Когда ты переворачиваешь экран (и не только), весь UI просто уничтожается и создается полностью заново
Хотя с точки зрения пользователя это все тот же экран с теми же данными ?
Это создает дополнительную потребность отвязать View от остального приложения,
чтобы можно было ее подменять прямо в рантайме
? MVC (Controller)
Начать стоит с MVC, потому что именно он является отправной точкой MV* шаблонов в целом и MVP в частности
Изначально первый Model‑View‑Controller был описан в далеком 1978 году,
а первая его окончательная версия была опубликована лишь в 1988 году

Суть здесь простая
View получает данные напрямую от Model без изменений,
а запросы пользователя направляет в Controller,
который по ним вызывает изменения в Model
????️
MVC не имеет строгой реализации, поэтому может быть реализован по‑разному
Однако, подход когда Model реализовывали как простой доступ к базе,
а логику описывали в Controller,
позднее назвали ? ТТУК (Толстые, тупые, уродливые контроллеры)
Controller стоит делать тонкой прослойкой, которая ответственна только за прием запроса пользователя и выбор что по этому запросу нужно сделать модели, а всю работу выполняет уже сама модель
Вообще это правило применимо ко всем прослойкам MV шаблонов
? Ходят легенды, что MVC когда‑то даже использовался в разработке под Android, но сегодня ты его точно не встретишь
Хотя на других платформах MVC все еще часто используется
И, к сожалению, очень часто описан как ТТУК, даже в официальных документациях ?
Ты так же можешь встретить более современные варианты MVC,
когда View получает данные от Controller и ничего не знает про Model
Выделяют Активный и Пассивный View
Пассивный: Controller управляет View
Активный: View подписывается на Model
Как раз на основе пассивной интерпретации и был реализован MVP

? MVP (Presenter)
В этом шаблоне более подробно описали взаимодействие View и Presenter
что стало особенно актуально для специфики Android, с ее пересозданием View в рантайме
Presenter переводится как Ведущий, что позволяет нам понять,
что это он управляет тем, что нужно делать View,
Presenter говорит View showMessage
или showLoading
, и тд
А View сообщает о событиях пользователя presenter.onButtonClick
Основное отличие от MVC, заключается в контракте для View
Контрактом является интерфейс, который реализует View
А Presenter, уже работает с этим контрактом
View получает доступ к Presenter и передает свой контракт
?? Почему контракт важен?
Направление зависимостей всегда, должно идти к центру приложения
В рамках MV шаблонов, центром можно считать Model
По схеме, View находится дальше от Model, чем Presenter
Поэтому зависимость View → Presenter
, является правильной
В случае, когда необходима обратная зависимость View ← Presenter
,
применяется инверсия зависимости, реализуемая через интерфейсViewImpl → (interface View ← Presenter)
Таким образом interface View является частью сущности Presenter,
что позволяет сохранить правильное направление зависимостей
Другими словами Presenter неразрывно связан с интерфейсом View
Такая схема позволяет подменять реализацию контракта,
в случае пересоздания View (и не только),
чтобы Presenter продолжал выполнять свою работу без перебоев
И все выглядит хорошо, но есть нюанс
?
?
? Паттерн Команда (Command)
Чтобы понимать решение проблемы, которое будет описано далее,
нужно сначала понять, что такое команда, с точки зрения архитектуры
В ООП, когда мы хотим, чтобы класс А выполнил какую‑то работу,
мы просто вызываем его метод, в котором и будет выполнена работа
Но при таком подходе, есть ограничения:
мы не можем отложить выполнение работы на потом
нам нужно знать про класс А здесь и сейчас
? На помощь приходит паттерн проектирования Команда
Для его реализации, нужно создать отдельный объект MyCommand
с методом execute
- Если для выполнения команды нужны дополнительные параметры,
то мы передаем их в constructor
команды
- Если мы хотим выполнять команды над каким‑то объектом, о котором еще не знаем,
то получаем его (например класс А) в параметры метода execute
В таком подходе, вместо выполнения действия на прямую,
мы создаем команду для выполнения этого действия,
после чего можем хранить или передавать эту команду,
чтобы выполнить ее тогда, когда это будет необходимо
Такие команды, можно выполнять несколько раз, выполнять их для разных экземпляров класса А или вообще не выполнять команду, если она окажется не нужна
Команды для класса А могут быть разными
И чтобы мы могли работать с различными командами через единый механизм,
нужно создать для них общий интерфейс Command
с функцией execute
Это позволит нам создавать очередь команд и управлять этой очередью в зависимости от текущего состояния системы
Теперь, ты можешь создавать команды для объектов о которых еще не знаешь,
а так же хранить эти команды в очереди и управлять ею
? MVP (Moxy)
Разберем, какой основной подводный камень скрывается в шаблоне MVP
Когда одна View уничтожается, ей на замену приходит новая View
Чтобы новая View показывала пользователю тоже самое состояние (например открытый диалог), нам нужно где‑то хранить это состояние
В Android есть механизм сохранения состояния в самих View
? На первый взгляд отличное место для этого
Но тут возникает другая проблема
Presenter, может захотеть изменить состояние в момент когда первая View уже уничтожена, а новая еще не прикрепилась, и это изменение будет потеряно
Тогда давай сохраним всё состояние в Presenter,
а он установит его в новую View, когда та добавится в него
Снова проблема, что Presenter не умеет на основе состояния определять какие функции View нужно вызывать и в каком порядке, чтобы все отобразилось правильно
Состояние может быть очень большим и сложным, и будет нереально описать нужные действия для каждого его варианта
?♂️➡️ Паттерн Команда спешит на помощь
Мы можем создать дополнительный слой между View и Presenter
Назовем его ViewState
Тут мы сразу получаем профит, от того, что у нас есть контракт ViewViewState
может реализовывать этот контракт, тем самым никак не влияя на сам Presenter
Наш Presenter будет обращаться к ViewState
, как к обычной View
Причем даже тогда, когда самой View может вообще не бытьViewState
будет создавать команды и складывать их в очередь
Команды будут складываться в очередь или буфер,
и применяться к текущей View, если она есть
Если ViewState
, получит новую View, то он достанет все команды из буфера
и поочередно применит их к новой View,
тем самым гарантировано добившись актуального состояния View
Чтобы более оптимально управлять буфером команд,
например, не выполнять 10 раз команду showMessage
, а выполнить только последнюю,
мы можем добавить для каждого типа команды правило ее буферизации
Такое правило называется Стратегией или Strategy
Вот пример базовых стратегий для команд, которых хватит для большинства ситуаций:
AddToEndStrategy — добавит пришедшую команду в конец очереди
Используется по умолчаниюAddToEndSingleStrategy — добавит пришедшую команду в конец очереди команд Причём, если команда такого типа уже есть в очереди, то уже существующая будет удалена
SingleStateStrategy — очистит всю очередь команд, после чего добавит себя в неё
SkipStrategy — команда выполнится сразу, если возможно и никак не изменит очередь
Добавим к этому сахар, поддержим в Presenter и ViewState
обработку жизненного цикла View, и получим замечательную библиотеку для MVP под названием Moxy
Теперь наш MVP полноценно походит для разработки под Android
Но все еще является слишком сложным и содержит много кода
?♂️ А мы, программисты, люди ленивые, и лишний код писать не любим
Поэтому пошли дальше и придумали MVVM, о котором и поговорим в следующей статье
sapeg
Почему же MVC точно не подойдёт для разработки под Андроид?