Архитектура №2. MVP — попробуем разгрузить контроллер

В прошлом посте мы остановились на довольно знакомой ситуации.

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

Получили Massive View Controller.

Хорошо. Проблему увидели. Что с ней делать?

Самая очевидная мысль — давайте хотя бы перестанем принимать все решения внутри контроллера.

Пусть UIViewController занимается тем, что у него получается естественно: получает действия пользователя и обновляет интерфейс.

А решение о том, что именно нужно сделать, вынесем в отдельный объект. Так появляется Presenter.

Если совсем упростить идею MVP: View — показывает интерфейс и сообщает о действиях пользователя. Presenter — принимает решения. Model — данные и бизнес-логика.

Например, раньше контроллер мог сам проверять email: func didTapLogin() { guard !email.isEmpty else { errorLabel.text = “Введите email” return } authService.login(…) }

Теперь View просто сообщает: presenter.didTapLogin(email: email)

А Presenter уже решает, что делать дальше: проверить данные, начать авторизацию, показать загрузку или ошибку.

На первый взгляд мы просто переложили несколько строк из одного файла в другой. Но меняется важная вещь — ответственность.

Контроллеру больше не нужно знать, почему пользователь может или не может войти. Он просто отображает результат.

Presenter при этом можно проверить отдельно от UIKit. Передали пустой email — ожидаем ошибку. Авторизация прошла — ожидаем успешное состояние. Не нужно поднимать UIViewController, искать кнопку, имитировать нажатие и надеяться, что тест не развалится из-за интерфейса.

Получается неплохо.

Мы решили часть проблемы MVC: экран стал легче, логика — заметнее, тестировать её проще. - Можно расходиться? - Конечно нет.

Потому что сложность мы не уничтожили. Мы её переместили. Сначала Presenter проверяет ввод. Потом ему нужно получить данные. Потом обработать ошибку. Потом отправить аналитику. Потом решить, куда перейти дальше.

И через некоторое время рядом с Massive View Controller появляется его преемник — Massive Presenter.

То есть сама идея «вынесем логику из View» правильная, но она не отвечает на следующий вопрос: - а сколько ответственности вообще должен получить Presenter?

Есть и другая цена. Связей становится больше. Теперь простой сценарий выглядит примерно так: View → Presenter → Service → Presenter → View

Добавляются протоколы, зависимости, тестовые реализации. Для сложного экрана это нормальная цена. Для кнопки «Закрыть» иногда возникает ощущение, что мы построили небольшую государственную систему управления.

И здесь снова возвращаемся к теме архитектурной сложности. MVP обычно содержит больше сущностей, чем MVC, но при этом может быть проще в работе, потому что ответственность распределена понятнее. Но если для понимания одного действия приходится открыть шесть файлов, выигрыш уже становится не таким очевидным. Поэтому MVP для меня — это довольно логичный следующий шаг после MVC.

Мы столкнулись с проблемой: контроллер знает слишком много.

Попробовали решение: вынесли принятие решений в отдельный объект.

Стало лучше. Но появилась новая проблема: теперь нужно управлять самим разделением ответственности и связями между объектами.

И именно так, на мой взгляд, интереснее смотреть на эволюцию архитектур. Не: MVC плохой → MVP хороший.

А: MVC перестал справляться с конкретной сложностью → мы добавили новый уровень → решили одну проблему и получили следующую.

Следующий логичный вопрос: а обязательно ли Presenter должен командовать View: «покажи загрузку», «покажи ошибку», «обнови экран»?

А что, если вместо команд мы просто будем описывать состояние, которое интерфейс должен показать?

Вот здесь мы уже постепенно приходим к MVVM.

Архитектура №2. MVP — попробуем разгрузить контроллер | Сетка — социальная сеть от hh.ru