Альтернативный взгляд на MVC, MVP и MVVM

Когда я только пришел в iOS разработку, то довольно долгое время считал, что архитектура приложения сводится к выбору набора из нескольких букв. MVC для новичков, MVVM для опытных, а VIPER для самых крутых пацанов. Многие разработчики по-прежнему считают, что архитектура всего приложения обязана укладываться в эти аббревиатуры.
Это первый пост из серии “спорим с архитектором”.

Сегодня поговорим о том, почему MVC, MVP и MVVM это лишь малая часть архитектуры приложения.

Начнем с самого известного и популярного UI дизайн паттерна.

Изначально внешний вид MVC паттерна отличался от того, что мы привыкли сегодня видеть во множестве туториалов под iOS, также, как и проблемы, для борьбы с которыми он был придуман.
Классический MVC решал одну специфическую задачу — обработку эвентов, исходящих от пользователя и их отображение во View. Примером типичного use case может быть клик мышки или нажатие клавиши на клавиатуре, которые генерировали событие, обрабатываемое Controller’ом. Controller создавал своего рода команду и передавал ее в Model. Mодель содержала в себе бизнес-логику и занималась манипуляцией данными, а после обработки команды от контроллера броадкастила результат через различные механизмы в виде нотификейшенов, колбэков или обзерверов. View занималась тем, что следила(подписывалась) за изменениями в модели и при их возникновении форматировала результат, после чего рендерила его на экране. Весь этот флоу отлично показан на схеме выше.

Стоит обратить внимание на то, как модель изолирована ото всех и у нее нет ни одной явной зависимости(исходящие стрелки) к окружающим ее сущностям. Единственная неявная зависимость — колбэки/нотификейшены. Это означает, что модель может быть легко переиспользована, вынесена в другие модули, протестирована и использована с другими View и Controller’ами.

Основной поинт здесь в том, что этот паттерн фокусируется на работе только с user interface.

Я специально делаю такой акцент на том, что это именно UI design pattern, а не архитектура, т.к. в нашем приложении происходит еще много всего помимо отрисовки вьюшек. Например: хранение данных в бд, сетевые соединения и т.д. Все перечисленное не является зоной ответственности MVC, оно жило в других местах системы. MVC не был предназначен для решения этих задач. Все, что он делал — занимался решением проблемы взаимодействия пользователя с user interface.

Дальше на сцену выходит Apple MVC. Флоу в этом паттерне сильно отличается от классического MVC, т.к. в данном случае события на обработку приходят к нам от самого View, поскольку пользователь нажимает на тачскрин с View, который уведомляет Controller о событии. Сам Controller теперь выступает в роли своеобразного медиатора, который получает события от View через различные механизмы(target-action, delegation, callback’и), конвертирует их также в своего рода команды и передает их модели. Однако ответ из модели теперь приходит не напрямую во View, как это было в классическом MVC, а обратно в Controller(через уведомления, колбэки, обзерверы), после чего контроллер уже обновляет View. Именно такое взаимодействие предлагают ребята из Apple. Некоторые разработчики все равно умудряются связать обновление View напрямую из модели, однако из описания паттерна становится понятно, что модель ничего не должна знать ни о контроллере, ни о View.

Еще одно серьезное изменение, которое коснулось данного UI паттерна — это то, что теперь его начинают называть архитектурным паттерном.

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

Различные ответственности приложения

Я наглядно изобразил различные, часто возникающие задачи в приложении. Пока наше приложение маленькое, то MVC легко справится со всеми задачами. Стоит приложению начать разрастаться, как мы почти сразу получим бардак с точки зрения архитектуры.

Для примера, попробуем раскидать все эти ответственности не выходя за рамки MVC.
Начнем с бизнес-логики. Основная рекомендация сводится к тому, что бизнес-логика должна жить в модели, даже не смотря на это, часто ее пытаются воткнуть в контроллер. Storage(DB) тоже по-хорошему должен обитать в модели, часто его кладут в контроллер, но common advise все же сводится к хранению в модели. User event handling живет в контроллере, а Rendering живет во View (все как в классическом MVC). Однако Presentation(форматирование) теперь живет в Controller’e, потому что если его убрать во View, то мы нарушим принцип separation of concerns и View будет иметь прямую связь с моделью, что противоречит правилам в Apple MVC. Networking и Routing обычно живут в контроллере, нередко можно увидеть API-запросы из контроллера, а Parsing обычно встречается в модели.
В итоге мы получаем архитектуру приложения выглядящую следующим образом:

Классический Massive View Controller aka MVC

Даже если мы попытаемся внести некий баланс и распределим ответственности несколько иначе, например вот так:

Мы все равно получаем архитектуру, где один из слоев делает слишком много. Даже если перенести Networking обратно в контроллер, то все равно не удастся получить оптимального решения. Можно переорганизовывать ответственности как угодно, здесь нет оптимального разделения в принципе, и чем больше становится приложение — тем сильнее это бросается в глаза.

Проблема заключается в том, что весь экран — это один большой MVC. Иногда можно увидеть, как его дробят на несколько MVC, но это все так же не решает сути проблемы. Мы заканчиваем тем, что получаем массивный контроллер, который содержит несоклько View и моделей, которые делают слишком много. К тому же, в iOS View и Controller завязаны на UIKit, что еще сильнее усугубляет ситуацию с точки зрения тестирования.

Следующий популярный UI дизайн-паттерн — MVVM.
Здесь Controller был переименован во ViewModel, user event’ы нам, как и в apple MVC, приходят от View, и через “binder” обрабатываются ViewModel’ью. ViewModel передает сообщения в Model, и когда модель изменится, то ViewModel с помощью observable properties уведомит View. Основное отличие здесь в том, что ViewModel не содержит прямой ссылки на View, в то время как в MVC сам Controller содержал ссылку на View. Это хорошо видно на схеме. Обычно View также не содержит ссылок на ViewModel. Вместо этого есть так называемый binder. Когда Microsoft создавали паттерн MVVM, то они пытались решить проблему boilerplate кода для соединения презентуемых данных с View. В этом помогал фреймворк на основе XAML, который поддерживал View в актуальном состоянии за счет логики ViewModel, при этом не требуя от разраба ни строчки кода.

Теперь хорошо видно, что MVVM решает абсолютно ту же задачу, что и MVС, но с меньшим количеством boilerplate кода за счет фреймворка. Просто более чистый и автоматизированный подход. Теперь вновь взглянем на задачи приложения, но уже в рамках использования MVVM.

По правилам MVVM, бизнес-логика должна находиться в модели, презентация и обработка эвентов от юзера — во VIewModel, а рендеринг в View.

MVVM, ровно как и MVC, не решает задачи нетворкнига, парсинга, хранения данных и роутинга. Можно жонглировать ответственностями как нам вздумается, но данные UI паттерны не создавались для решения этих задач.

В iOS MVVM выглядит следующим образом. UIViewController и UIViews объединяются во View из-за жесткой зависимости от UIKit’a. UIViewController имеет жесткую зависимость от ViewModels, которые зависят от моделей. Модель, как и в MVC, ничего не знает о других сущностях и имеет только неявную связь в виде колбэков, уведомлений или обзерверов. В качестве Binder’a в iOS чаще всего используются различные фреймворки, такие как RxSwift, RxCocoa и Combine.

Теперь взглянем на MVP. Идея данного UI паттерна заключается в том, чтобы снова избавиться от Controller’а, переименовав его в Presenter, и наделить его более конкретными ответственностями. В чем отличия данного паттерна от MVVM? Здесь Presenter снова зависит от View(как MVC), но эта зависимость неявная и достигается это за счет Dependency Inversion принципа через интерфейс/протокол. Теперь Presenter(или другими словами Presentation layer) не зависит от UI(как в MVC). Он зависит от абстракции, а не от конкретной реализации View. View соответственно должна реализовать этот протокол.
Presenter также, через интерфейс, общается и со слоем модели. Получает события от модели он тоже через интерфейс. Когда Presenter получил данные от Model, то он создает ViewData и передает ее View через протокол, который она имплементит, после чего View уже рендерит эти данные. Простой пример MVP я использовал в предыдущем посте, чтобы продемонстрировать Virtual Proxy pattern для сохранения чистой архитектуры.

Итак, решает ли MVP задачи нетворкинга, роутинга, парсинга и хранения данных? Давайте взглянем.

MVP рекомендует хранить бизнес логику в Model layer за интерфейсом. Обработка событий от пользователя и презентация находятся в Presenter. Рендеринг соответственно во View.

Что касается всего остального, то MVP точно также не был создан для того, чтобы решать эти задачи и четкой рекомендации здесь нет.

MVP, как и остальные MV* паттерны создавался для решения задачи взаимодействия user interface с моделью. Все эти паттерны хорошо подходят исключительно для того, чтобы организовать UI слой приложения.

И где тогда архитектура?

В следующем посте я подробно опишу процесс принятия архитектурных решений, что на них влияет и какая архитектура “самая правильная”.

Полезные материалы

Общий обзор “архитектурных” паттернов в iOS с примерами кода от Bohdan Orlov

--

--