HandsAppMVP: iOS-архитектура для студии аутсорс разработки

image

Хороший код начинается с архитектуры, и iOS-приложения не исключение. Есть много стандартных паттернов, но цель этой статьи рассказать не о них, а об опыте адаптации одного из них и выработке собственного. Мы назвали эту адаптацию HandsAppMVP.

image

В iOS-разработке архитектура в первую очередь определяет организацию классов и зависимостей для одного конкретного ViewController. Впрочем, центральным компонентом может выступать не только он, но и просто UIView. Выбор зависит от конкретной задачи.

Сравнение архитектур

Существует несколько стандартных архитектурных шаблонов для iOS: MVC, MVP, MVVM, VIPER (ссылки на описание каждого можно найти в конце статьи).

Выбирая архитектуру для разработки, мы выделили основные параметры, которым она должна соответствовать: скорость разработки, гибкость и низкий порог входа. Далее мы занялись сравнением трех известных архитектур с учетом этих параметров (шаблон MVC iOS-комьюнити давно закопало из-за грубого несоблюдения single responsibility).

Для аутсорс-команды особенно важна скорость разработки. VIPER — самая сложная и «медленная» архитектура, быстрее разработка идет с применением чистого MVP или MVVM, так как в них меньше компонентов.

Гибкость подразумевает безболезненное добавление или удаление функционала в приложении. Этот параметр сильно коррелирует со скоростью разработки на всех этапах жизни приложения, кроме начального. Также гибкость тесно связана с простотой тестирования — автоматические тесты дают разработчику уверенность в том, что он ничего не сломает, и позволяют избежать багов. Классическая MVP плохо покрывается тестами, особенно если не использовать рассмотренные далее интерфейсы классов. MVVM с точки зрения тестирования также имеет плохие показатели, потому что тестирование реактивного кода занимает значительно больше времени. VIPER отлично подходит для написания тестов, потому что в нем максимально соблюдается принцип единственной ответственности и классы зависят от абстракций.

И последний параметр, который мы рассматривали, — порог входа. Он показывает, насколько быстро новые разработчики (в первую очередь — джуны) вникают в архитектуру. Здесь MVVM с применением сторонних реактивных библиотек (RxSwift, PromiseKit и т. п.) занимает почетное последнее место по очевидным причинам. VIPER также довольно сложная архитектура в силу большого количества компонентов. MVP имеет самый низкий порог входа.

Взвесив все за и против, мы пришли к выводу, что нам нужно что-то такое же простое, как MVP, и такое же гибкое, как VIPER. Так и родилась идея создать на их основе свою архитектуру — HandsAppMVP.

Расширяем MVP

Основные компоненты нашей архитектуры — Model, View, Presenter. Они выполняют те же функции, что и в классической MVP по известной схеме:
image
[Схема классического MVP]

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

Интерфейсы

Первым делом мы добавили в эту классическую схему интерфейсы ViewInput и ViewOutput. Учли пятый принцип SOLID — принцип инверсии зависимостей. Он является скорее не дополнением, а уточнением для MVP. Зависимость от абстракций помогает избавиться от строгой связанности компонентов и позволяет нормально писать тесты. Схема с учетом интерфейсов выглядит так:
image
[Добавление интерфейсов ViewInput и ViewOutput]

Маленький прямоугольник — интерфейс.

Внимательный разработчик спросит, где интерфейсы для Model? Сейчас к ним переходим.

Работа с данными

Модель данных в мобильных архитектурах — собирательное понятие. Стандартный пример: приложение стучится в сеть для взаимодействия с сервером, затем сохраняет данные в CoreData для офлайн-работы, некоторую простую информацию записывает в UserDefaults и хранит JWT в Keychain. Все эти данные, с которыми ведется взаимодействие, составляют Model.

Класс, который отвечает за взаимодействие с контейнером данных конкретного типа, мы называем сервисом данных. Для каждого контейнера (удаленная база данных, локальная база данных, UserDefaults и пр.) в HandsAppMVP добавляется сервисный класс, который взаимодействует с презентером. Теперь можно также добавить интерфейсы input/output для каждого сервиса данных:
image
[Добавление сервисов для работы с данными]

Не каждый сервисный класс необходимо подключать к презентеру с помощью интерфейса, как, например, при использовании Moya. Moya — open-source-библиотека для работы с сетью. Moya предоставляет готовый сервисный класс (MoyaProvider), и при написании тестов нам не приходится делать mock-объект, заменяющий ApiProvider. В Moya предусмотрен специальный тестовый режим, при включении которого MoyaProvider не стучится в сеть, а возвращает тестовые данные (подробнее можно почитать по ссылке). Презентер при этом ссылается не на абстракцию MoyaProvider, а на реализацию. А обратную связь от этого сервиса мы получаем с помощью замыканий. Пример реализации можно посмотреть в демопроекте.

Этот пример скорее исключение, чем правило, и показывает, что беспрекословное соблюдение SOLID не всегда лучшее решение.

Навигация

Навигацию в приложении мы рассматриваем как отдельную ответственность. Для нее в HandsAppMVP используется специальный класс — Router. Router содержит weak-ссылку на View, с помощью которой может показать новый экран или закрыть текущий. Router также взаимодействует с презентером c помощью интерфейса RouterInput:
image
[Добавление компонента для навигации (Router)]

Сборка компонентов

Последнее дополнение классического MVP, которое мы используем, это Assembly — класс-сборщик. Он используется для инициализации View и остальных компонентов HandsAppMVP, а также для внедрения зависимостей. Assembly содержит единственный открытый метод — `assemble() -> UIViewController`, результатом выполнения которого является нужный UIViewController (или UIView) c необходимым графом зависимостей.

Мы не будем показывать Assembly на схеме архитектуры, так как он не связан с компонентами MVP и его жизненный цикл заканчивается сразу после их создания.

Кодогенерация

Для экономии времени мы автоматизировали процесс создания классов HandsAppMVP с помощью Generamba. Используемые шаблоны для Generamba можно найти в нашем репозитории. Пример конфига для Generamba есть в демопроекте.

В результате генерации конкретного экрана получаем набор классов, соответствующий схеме HandsAppMVP, набор unit-тестов для создания и внедрения компонентов, а также шаблонный класс для тестов презентера.

Что получилось

Если сравнить лоб-в-лоб HandsAppMVP и VIPER, то можно заметить, что они очень похожи и первая отличается только отсутствием компонента Interactor. Но, избавившись от прослойки между сервисами и презентом (интерактора), а также упростив взаимодействие с сетью с помощью Moya, мы получили ощутимый прирост скорости разработки.

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

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

В заключении порекомендуем несколько хороших статей на тему архитектуры iOS-приложений, которые помогли нам разобраться в тонкостях и определиться с выбором:

1. «Архитектурные паттерны в iOS» habr.com/ru/company/badoo/blog/281162
2. «iOS Swift: MVP Architecture» medium.com/@saad.eloulladi/ios-swift-mvp-architecture-pattern-a2b0c2d310a3
3. «Разбор архитектуры VIPER на примере небольшого iOS-приложения на Swift 4» habr.com/ru/post/358412
4. «Реализация MVVM в iOS с помощью RxSwift» habr.com/ru/post/273455

Также очень помогла и вдохновила открытая документация компании SurfStudio — github.com/surfstudio/Surf-iOS-Developers/blob/master/Surf_MVP.md

Наконец, прикладываем ссылку на демопроект, написанный на HandsAppMVP, который мы не раз упоминали в статье.
github.com/HandsAppDevelopment/HandsAppMvp-Demo
Источник: habr.ru