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 проекта и знакомьтесь с еще более продвинутыми фичами библиотеки.