Проблемы архитектуры в больших проектах

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


    Рассмотрим 5 важных тем. Конечно, большинство из них не раз обсуждались в сообществе, но за каждой из тем стоят боль, слезы, потерянное время и, самое главное, опыт, который оказался полезен для нас, и надеюсь, будет полезен и вам.


    image


    В начале разработки мобильного приложения перед лидом или проектировщиком стоит вопрос – какой архитектурный паттерн использовать? В нашей студии есть общепринятый архитектурный паттерн MVP. Чистый MVP, безусловно, хорош в чистом виде (см. изображение ниже), но мы бы не были настоящими разработчиками, если бы не доработали этот паттерн. На одном варианте не остановились, и у нас в появилось целых два ответвления от чистого MVP.


    image


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


    Давайте рассмотрим два наших MVP на стероидах.


    SurfMVP


    image


    На изображении видно, что, по сравнению с обычным MVP, не так много изменилось. Мы заметили некоторые проблемы при переходах между экранами в iOS приложениях. Большое количество логики по формированию новых экранов перед переходом сосредотачивается непосредственно во UIViewController, нам показалось это не совсем правильным, поэтому первым делом мы выделили отдельную сущность Router, которая отвечает за осуществление переходов между экранами в приложении.
    Model в SurfMVP – это сервисы, которые вызывает Presenter для получения данных. Зачастую, один сервис решает задачи для работы целого модуля, однако в сложных ситуациях приходится взаимодействовать с несколькими.
    За сборку отдельного модуля отвечает сущность Configurator, она инициализирует все необходимые компоненты и отвечает за простановку зависимостей между ними.


    image


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


    Рассмотрим их отдельно:


    ViewInput – реализует сама View, ссылку держит Presenter. Данный протокол описывает методы, при помощи которых Presenter может управлять View, передавать данные, изменять состояния и так далее.


    ViewOutput – реализует Presenter, ссылку на него держит View. Протокол описывает набор действий, которые могут произойти во View, и методы жизненного цикла, например, события взаимодействия пользователя с экраном.


    RouterInput – реализует Router, а ссылку на него держит Presenter, так как он является единственным ответственным за то, чтобы инициировать дальнейшую навигацию в приложении.


    ModuleTransitionable – реализуется View, ссылку на него держит Router. Это единственный «базовый» протокол в SurfMVP. Он нужен для того, чтобы предоставить Router набор методов для работы с навигацией по приложению.


    ModuleInput – реализует Presenter. Данный протокол должен содержать в себе методы, при помощи которых другой модуль, который держит ссылку на этот протокол, мог бы изменять состояния текущего модуля.


    ModuleOutput – реализует Presenter вызывающего модуля, ссылку держит Presenter вызываемого модуля. Если экран профиля можно отобразить с модуля новостей, то NewsPresenter должен реализовывать ProfileModuleOutput, а ProfilePresenter содержать на него ссылку.
    ModuleOutput передается в Configurator вызываемого модуля и там устанавливается в Presenter. Содержит в себе методы модуля, которые влияют на поведение вызывающего модуля.


    Проблема SurfMVP


    Исходя из всего вышеперечисленного, есть одна основная проблема – навигация. Хоть и посылом для выделения отдельной сущности Router были проблемы с навигацией, как оказалось, они ушли, но не надолго. SurfMVP успешно применялся на проектах с простой флат навигацией, без сложных DeepLinks и Push-Notifications.


    На изображении ниже схематично представлена навигация в приложении с SurfMVP. Каждый отдельный модуль связывается с другим через свой Router. Таким образом, выстраивается навигация любого флоу в приложении.


    image


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


    image


    Проблемы начинаются в момент, когда необходимо перенести пользователя из точки A в точку D, и это должно произойти без его участия. Например, если пользователь нажимает на Push-Notification или переходит по ссылке из стороннего приложения. В случае SurfMVP нам придется добавлять глобальный Router, который будет управлять навигацией вне зависимости от того, в какой точке приложения в данный момент находится пользователь. Чтобы решить эту проблему глобально мы решили использовать координаторы, перейдем к ним.


    image


    Coordinated SurfMVP


    image


    Coordinated SurfMVP – это архитектурный паттерн, в котором, в отличие от SurfMVP, мы убрали сущность Router, которая находилась внутри каждого отдельного модуля. Парадигма построения приложения немного изменилась. Модули теперь не являются полностью независимыми. Каждый модуль, за исключением полностью переиспользуемых, находится в отдельном обособленном UserFlow, который по задумке должен выполнять какое-то общее действие, приводящее пользователя к желаемому результату.


    Примером такого флоу у нас в приложении является флоу платежей. Платежи – это набор экранов, которые позволяют пользователю сделать перевод или платеж разными способами.


    В Coordinated SurfMVP сущность Router заменила сущность Coordinator, которая теперь отвечает за работу навигации не одного отдельного модуля, а набора модулей, которые связаны друг с другом логически. Это упрощает навигацию и работу с приложением. Схематично наше приложение выглядит так:


    image


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


    Если у нас в приложении есть Deeplinks или Push-Notifications, мы всегда можем задать правила инициализации и старта coordinators, чтобы они построили стек непосредственно до желаемой точки D, о которой мы говорили ранее.


    image


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


    Плюсы и минусы Coordinated SurfMVP


    Преимущества:


    1. Основной плюс подхода с координаторами — возможность переиспользовать целые блоки навигации внутри приложения. Теперь из любого места в приложении есть возможность вызвать этот координатор и не думать ни о чем, кроме как о завершении его работы.
    2. Так как логика навигации обособлена внутри отдельного координатора, теперь гораздо удобней следить за навигацией: достаточно открыть один файл и вся картина перед глазами. Нет больше необходимости протыкивать все отдельные модули, чтобы понять, что за чем тянется, собирать приложение и смотреть в дизайн.
    3. Удобнее проектировать в больших командах. Достаточно на этапе проектирования отдельной новой фичи выделить время на построение всей навигации и инициализации всех модулей, после чего делегировать разработку большому количеству разработчиков, и уже намного меньше будет возникать проблем с интеграцией этих экранов между друг другом.
    4. Интеграция Deeplinks и Push-Notifications перестала быть головной болью.

    Недостатки:


    Как в любом архитектурном подходе, в Coordinated SurfMVP есть минусы.


    1. Большие координаторы – это больно. Из-за концентрации всей логики в одном месте становится гораздо сложнее не утонуть в большом количестве строк кода. Если не следить за соблюдением принципа единой ответственности, то конечно, координатор может вырасти в большого монстра, и все плюсы по читаемости кода легко испарятся.
    2. Приходится много писать, чтобы достичь красоты в коде. Из-за большого количества слоев в приложении, каждый из которых отвечает за отдельное действие, приходится пробиваться через эти слои, чтобы дойти до желаемого координатора.
    3. Memory Leaks – проблема не нова, но стоит следить за этим делом, чтобы не попасть в просак. Основная причина появления утечек памяти при работе с координаторами – это retain-циклы в коллбеках модулей. Так что нужно очень внимательно следить за сильными ссылками внутри замыканий.

    Типовой кейс

    Типовой кейс — инициализация нового Координатора и реализация closure finishFlow. Захват weak coordinator является обязательным, иначе Координатор будет ссылаться сам на себя, что повлечет утечку в виде AuthCoordinator.


        func runAuthFlow() {
            let coordinator = AuthCoordinator(router: MainRouter())
            coordinator.finishFlow = { [weak self, weak coordinator] in
                self?.removeDependency(coordinator)
            }
            self.addDependency(coordinator)
            coordinator.start()
        }

    Выводы


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


    Когда использовать Coordinated SurfMVP


    На самом деле, когда вам захочется, тогда и используйте, но мы придерживаемся следующих условий:


    • Структура экранов сложна и подвержена изменениям;
    • Есть Deeplinks и/или Push-Notifications с навигацией;
    • Предстоит работать в большой команде;

    Когда использовать SurfMVP


    Мы не забыли о нашем первом архитектурном паттерне. Его мы по-прежнему используем в студии при разработке проектов если он удовлетворяет следующим условиям:


    • Проект достаточно маленький и не планирует быстро развиваться;
    • На проекте очень простая структура экранов, и она не подвержена сильным изменениям.

    Дополнительные материалы



    В этой статье я поделился проблемой с архитектурой, с которой мы столкнулись во время работы. Конечно же выбор, какую именно архитектуру использовать, остаётся за вами. В следующей статье я поделюсь проблемами backend в больших проектах и расскажу как же мы их решали. Stay tuned!

    Surf
    51,04
    Компания
    Поделиться публикацией

    Похожие публикации

    Комментарии 2

      0
      Спасибо за статью. Можете привести пример как в вашем подходе взаимодействовать с TabBarController и его табами?
        0
        Да, конечно. В этом репозитории можно найти полноценный тестовый проект, в котором реализован UITabBarController и выстроена полноценная навигация

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое