Блог

Полезные статьи и новости о жизни WaveAccess

Пишем быстро: делим большой проект на несколько команд (Dagger2, Cicerone, gradle-модули)

Всем привет! Сегодня будет занимательная история от команды Android-разработки. Это статья о том, как сделать большой проект за маленький срок. Вам потребуется нанять достаточно  людей... и сделать так, чтобы они не мешали друг другу. И это как раз самое сложное: тут нужно хорошее техническое решение. Про него и поговорим.

Постановка задачи

У нас в работе было объемное мобильное приложение для крупной телеком-компании. В нем имелась “основная” функциональность и несколько разделов — тематическая функциональность,  которая дает дополнительные возможности, но приложение может работать и без нее.

В разработке мы обычно называем разделы с тематическим функционалом — “фичами” ("feature").

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

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

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

  • Общий код: когда люди из одной команды правят общий код, бывают merge-конфликты, а если люди из разных команд, то они общаются менее плотно и merge-конфликтов получается больше. В разных командах могут быть приняты разные стили/шаблоны, которые придется унифицировать.

  • Управление задачами: разные команды могут использовать разные подходы к управлению, разные трекеры задач, поэтому отслеживать общий статус проекта и обеспечивать синхронность может быть трудно.

  • Разделение ответственности: в случае, если есть проблемы/баги, то сложно определить, какая команда будет их исправлять (чей конкретно код был причиной возникновения проблемы).

Однако если сделать код максимально “разделенным” и несвязным, то эти сложности можно свести к минимуму. То есть, нужно разделить проект на модули — один модуль для основной части и по модулю на каждую фичу. Зависимости между модулями должны быть минимальны, чтобы можно было отдать каждый модуль отдельной команде, и разработка одного модуля не зависела от разработки других. В идеале, просто заранее определить API (интерфейсы) для каждого модуля, создать заглушки (“моки”), и можно будет не заботиться даже о порядке реализации модулей. Если соблюдаются интерфейсы, то заглушка безболезненно заменяется на рабочую версию и наоборот.

Проблема

Андроид сам по себе неплохо умеет делить проект — можно просто создавать для фич отдельные библиотечные gradle-модули. Но есть нюанс: зависимости между модулями нужно выстроить без циклов, иначе проект просто не соберется.

Например:

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

 

Общий модуль предоставляет для фич базовые возможности — ходить в сеть, хранить данные, взаимодействовать с ОС, обращаться к фундаментальной бизнес-логике приложения.

Вместе с тем, общий модуль встраивает в себя бизнес-логику, реализованную фичами. Один из примеров: в нашем приложении есть главный экран, на котором есть виджеты. Эти виджеты относятся к разным фичам, а главный экран — к общему модулю.

Из-за навигации тоже возникают зависимости. В Андроиде пользовательский интерфейс и  навигация основываются на активити (Activity), поэтому фичи определяют активити для своих экранов. Чтобы одна активити могла вызвать другую, ей нужно знать ее .class. Поэтому если активити общего модуля захочет сделать переход на активити фичи — то это зависимость (и наоборот, если активити фичи хочет навигироваться на общий модуль).

Технические подробности

Перед тем, как показать следующую картинку, расскажем про стек разработки. Мы используем Dagger2 для внедрения зависимостей и Cicerone для навигации. В целом следуем принципам clean architecture, для реализации слоя представления — MVVM (вью-модель из architecture components, биндинги реализуем на RX).

Вот наши циклические зависимости (здесь изображена только одна фича, чтобы не усложнять схему):

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

Data, Domain и Presentation — основные слои по классике clean architecture.

DI — построение графа зависимостей с помощью Dagger2.

В главном модуле есть несгруппированные компоненты (RxBus, App Lifecycle, Permissions, Utills) — это утилиты, обмен сообщениями внутри приложения и специфические для платформы вещи.

Presentation

В слое Presentation есть view-классы (Activity, Fragment) и ViewModel (потому что мы используем MVVM).

Главный модуль предоставляет базовые классы — Base Presentation Classes, например BaseActivity/BaseFragment/BaseViewModel/BaseFragmentNavigator. В них реализована организация биндингов, инжекты зависимостей и прочие скучные вещи :)

Еще в этом слое есть навигация. Мы используем Cicerone, и поэтому появляются:

  • AppNavigator: главный навигатор приложения, который умеет делать переходы между “разделами” (модулями),

  • MainNavigator: навигатор главного модуля, который делает переходы между экранами этого модуля,

  • ActivityNavigator: умеет делать переходы между конкретными активити (AppNavigator и MainNavigator используют его чтобы фактически исполнять свои команды).

Domain

Главный модуль содержит бизнес-логику, общую для всего приложения (Repositories, Interactors). Кроме того, там есть интерфейсы репозиториев (Repository Interfaces). Они нужны для того, чтобы одна фича могла использовать репозиторий из другой, или чтобы главный модуль мог использовать репозиторий фичи.

Data

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

DI

Здесь происходит формирование графа зависимостей. На схеме показано только проблемное место и не показаны модули, в которых провайдятся зависимости, связанные с сетью, базой данных, общей бизнес-логикой, etc. FeatureComponent зависит от AppComponent, чтобы нужные зависимости попали в Feature-модуль. AppComponent должен предоставить репозитории, интерфейсы которых есть в App-модуле, а реализация находится в Feature-модуле.

Зависимости

App -> Feature:

  • Наследование от Base Presentation Classes

  • Каждая активити должна содержать поле с ActivityNavigator, чтобы в любой момент можно было выполнять команды навигации.

  • Реализация интерфейсов репозиториев.

  • Использование бизнес-логики общей для всего приложения.

  • Использование утилит, обмена сообщениями внутри приложения и специфических для платформы инструментов.

  • FeatureComponent зависит от AppComponent.

Feature -> App:

  • Использование view из feature модуля. Как уже говорилось выше — есть главный экран с виджетами. Виджеты относятся к разным фичам, а главный экран — к общему модулю.

  • Использование .class активити из Feature-модуля в ActivityNavigator.

  • Вызов конструкторов репозиториев Feature-модуля, чтобы сделать FeatureModule и запровайдить их в AppComponent (ведь ими пользуется главный модуль и другие фичи).

Решение

Базовая идея выхода из циклической зависимости — добавить еще один элемент.

(картинка 6)

В нашем случае все немного сложнее, чем на схеме из трех кружков, но смысл тот же. Если мы разделим модули на части для представления и логики плюс добавим “общий” модуль, проблема уйдет. (А еще получим архитектурную выгоду, потому что станет меньше связности. То есть параллельная разработка логики и внешнего вида будет проще).

Что есть что: 

feature

  • содержит view-классы feature модуля,

  • активити здесь объявлены абстрактными, потому что они не могут содержать в себе навигатор,

app

  • содержит view-классы главного модуля,

  • наследует абстрактные активити из feature модуля, добавляя в них навигатор,

feature-logic

  • содержит бизнес-логику, вью-модели и логику навигации feature модуля,

  • реализует интерфейсы репозиториев, которые вынесены в common модуль,

app-logic

  • содержит бизнес-логику, вью-модели и логику навигации главного модуля,

  • реализует интерфейсы репозиториев, которые вынесены в common модуль,

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

common-module

  • содержит объявления интерфейсов для тех репозиториев/интеракторов/etc, которые используются более чем в одном модуле,

  • содержит Base Presentation Classes.

И для самых стойких — финальная схема архитектуры:

Итог

Words are cheap, show me the code metrics. Сейчас докажем, что перевернуть проект с ног на голову было эффективнее, чем продолжить писать код той же командой.

Первая стадия (до разбиения на модули) = 37296 строк кода, 104 дней работы, 7 человек в команде. На перестройку архитектуры и подключение внешних команд ушло 14 дней.

Вторая стадия (с модулями) = 72043 строк кода и 92 дня работы. Если бы мы продолжали тем же составом, нам пришлось бы потратить на вторую стадию 201 день. Это на 109 дней больше. 

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

Спасибо за внимание!

 Если ваш проект требует ускоренной разработки, или вы нуждаетесь в восстановлении проекта после предыдущего подрядчика, — свяжитесь с нами и мы ответим на все ваши вопросы: hello@wave-access.com 

Заказать звонок

Удобное время:

Отменить

Пишите!

Присоединить
Файл не больше 30 Мб.
Отменить