Stone. Квалификаторы и идентификаторы
Использование библиотеки DI с новых взглядом набирает обороты. И автор хотел бы рассказать еще об одной идее, которую сподвигли сделать библиотекой такой, какой она сейчас является.
Собственные решения для разработчика развивать и разрабатывать оказалось крайне удобно. Архитектура библиотеки и ее фичи разрабатывались под конкретные задачи в проектах. И особенный случай в проекте, заставил переосмыслить всем привычные квалификаторы для DI, и добавить что-то новое.
Случай, кстати, оказался не новый, и автор много его встречал в различных проектах. Так что, думаю для многих из читателей такой пример покажется до боли знакомым.
Зазеркалье
Добро пожаловать на псевдо проект псевдо синего, зеленного или красного гиганта, который в штате содержит десятки человек на платформу. Несколько команд усердно пилят фичи. Ну а вы, как самый матерый разработчик, за всеми следите и планируете общую архитектуру проекта. Вы прослеживали рост проекта от самых зачатков до сегодняшних дней. И проект получился неплохим.
Не буду томить, ваше условное приложение является агрегатором такси. С огромной клиентской базой вы преуспеваете от ближайших конкурентов подробной аналитикой поездки, включая сегодняшнее настроение вызванного водителя и состояние его авто между тех. осмотрами. Каждый вызов такси сопровождается уникальным экраном ожидания со своими бонусами и пасхалками. Все это увеличивает вовлеченность клиента к приложению, а водителю понимание, что его не бросят посреди дороги.
И вот руководству понадобилось, что то новое. То, что обеспечит преимущество над всеми конкурентами разом - одновременный заказ 2х машин. Ну что мой опытный лид команды, боюсь даже представить, что будет с проектом, если ко всем условиям добавят сроки - один месяц. Ну а для подробности рассмотрим схему проекта.
Схема описана в упрощенном виде для понимания, но мы держим в уме, что дата слой может быть крайне раздутым. Со сложными механизмами сбора данных перед отправкой, а также внутренней аналитикой.
Все компоненты при этом развиваются распределенными командами. Просто так не зайдешь в код команды, развиваемый месяцами, и не попилишь его вдоль и поперек. Самым простым и быстрым в данном случае остается подход дублирование компонентов программы.
Свободное кресло разработчика
Ничего сложного нет в простой генерации компонентов под каждый экран (в данном случае примем единицей DI скоупа), автор бы хотел немного теперь усложнить схему наличием еще одного экрана, который использует те же интеракторы и репозитории, вынуждая их делать синглтонами приложения. Или локальными синглтонами, но это явление будем считать больше исключением чем практикой.
Теперь наши компоненты: репозитории и датасорс, гвоздями прибиты к жизненному циклу приложения. Для разработки удобно, что они доступны для любого экрана. Но вот для масштабирования дела обстоят иначе. Синглтон просто так не задублируешь. Теперь их надо различать между собой в DI.
Поваренная книга
И вот мы планомерно пришли к знакомству с инструментарием Stone библиотеки - квалификаторами. Думаю вы уже с ними знакомы и из других библиотек DI, но все же уточню правило их использования.
@Qualifier @Retention(RetentionPolicy.RUNTIME) @Documented annotation class MainTripQualifier
Тут ничего нового вы не заметите. Даже применение этого самого квалификатора выглядит также, как и у известных фреймворков. Указывается квалификатор для метода провайдинга.
И тот же квалификатор должен быть для аргумента зависимости или поля инжекта.
@MainTripQualifier @Provide(cache = Provide.CacheType.Strong) abstract fun provideTripInfoRepositoryMain( api: TripInfoApi, @MainTripQualifier cache: TripInfoInMemory, ): TripInfoRepository
Деление же нашего приложение теперь сводится к простому объявлению дополнительных квалификаторов. Теперь мы можем разделить заказ такси на основной и дополнительный. Все компоненты, аналитика взаимодействие разделяются на 2 отдельных заказа.
Мы оставили только репозитории доступными как сингтоны, чтобы не растаскивать квалификаторы на всех. Но даже тут нам пришлось по всем слоям создавать отдельные методы предоставления под каждый квалификатор и объект. В особенности получилась страшная картина в модуле провайдинга интеракторов.
@Module abstract class InteractorsModule { @MainTripQualifier abstract fun provideTripInfoInteractorMain( @MainTripQualifier ordersRepository: CurrentOrderRepository, @MainTripQualifier tripInfoRepository: TripInfoRepository, ): TripInfoInteractor @SecondTripQualifier abstract fun provideTripInfoInteractorSecond( @SecondTripQualifier ordersRepository: CurrentOrderRepository, @SecondTripQualifier tripInfoRepository: TripInfoRepository, ): TripInfoInteractor @MainTripQualifier abstract fun provideMapItemsInteractorMain( @MainTripQualifier ordersRepository: CurrentOrderRepository, @MainTripQualifier tripInfoRepository: TripInfoRepository, ): MapItemsInteractor @SecondTripQualifier abstract fun provideMapItemsInteractorSecond( @SecondTripQualifier ordersRepository: CurrentOrderRepository, @SecondTripQualifier tripInfoRepository: TripInfoRepository, ): MapItemsInteractor }
Что-ж, с таким решением не один месяц можно прожить, но что дальше, ведь такое решение совсем не масштабируемое. Для нескольких заказов не нагенерируешь таких квалификаторов в проекте. Что если вообще этих заказов может быть неограниченное кол-во.
Тысяча готова. И Еще на подходе
Мы в наших приложениях привыкли использовать компоненты в изолированных скоупах, ограниченных в рамках экранов, фрагментов, Activity или View. Для них можно делить, переносить и перетасовывать все новые компоненты, копировать множество архитектурных объектов сколько нужно. Но вот переиспользование таких компонентов в рамках локальных синглтонов становиться затруднительным. В одном компоненте DI просто так нельзя создавать, переиспользовать несколько экземпляров одного класса. Остается использовать независимые DI компоненты и использовать их через какой-нибудь менеджер этих самых компонентов. Немного выглядит как DI в DI.
В stone же можно использовать идентификаторы компонентов.
data class TripId( val tripId: String, ) @Component( identifiers = [ TripId::class ] ) interface AppComponent { // some code }
Идентификаторы позволяют теперь дублировать объекты в одном скоупе и обращаться к ним по идентификаторам.
@Module abstract class InteractorsModule { abstract fun provideTripInfoInteractor( tripId: TripId, ordersRepository: CurrentOrderRepository, tripInfoRepository: TripInfoRepository, ): TripInfoInteractor abstract fun provideMapItemsInteractor( tripId: TripId, ordersRepository: CurrentOrderRepository, tripInfoRepository: TripInfoRepository, ): MapItemsInteractor }
А все использование сводится к указанию нужного идентификатора на месте Inject'а объекта или его использования.
class MapScreen { @Inject lateinit var viewModel: MapViewModel init { DI.inject(mapScreen = this, tripId = TripId(argument.tripIdString)) } }
DI компонент может легко отличить аргументы провайдинга как зависимости или идентификаторы. И предоставлять все или отдельные зависимости по идентификатору.
Сухой остаток
Одной из идей Stone - одна фабрика на все. И все было продумано, для того, чтобы избавиться от всяких фабрик и менеджеров-провайдеров viewModel'ей. А размытые скоупы в априори были призваны использовать минимальное кол-во DI скоупов на весь проект.
Ну а пока вы противитесь идеям развивать свой DI, вендоры оболочек Android будут и дальше зарабатывать на людях предлагая клонировать ваше приложение под несколько аккаунтов.
Заходите на wiki проекта и знакомьтесь с еще более продвинутыми фичами библиотеки.