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

Собственные решения для разработчика развивать и разрабатывать оказалось крайне удобно. Архитектура библиотеки и ее фичи разрабатывались под конкретные задачи в проектах. И особенный случай в проекте, заставил переосмыслить всем привычные квалификаторы для DI, и добавить что-то новое.

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

Зазеркалье

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

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

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

схема компонентов окна ожидания такси
схема компонентов окна ожидания такси

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

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

Свободное кресло разработчика

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

Оба репозитория переиспользуются на 2х экранах
Оба репозитория переиспользуются на 2х экранах

Теперь наши компоненты: репозитории и датасорс, гвоздями прибиты к жизненному циклу приложения. Для разработки удобно, что они доступны для любого экрана. Но вот для масштабирования дела обстоят иначе. Синглтон просто так не задублируешь. Теперь их надо различать между собой в 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 отдельных заказа.

Разделение приложения квалификаторами DI
Разделение приложения квалификаторами DI

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

@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
}

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

Hidden text

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

@Override
public void inject(MapScreen mapScreen) {
    mapScreen.setViewModelMain(new ProvideBuilder<MapViewModel>((_lc0) -> {
        TripInfoInMemory _lc1 = data().provideTripInfoInMemoryMain();
        TripInfoApi _lc3 = data().provideTripInfoApi();
        CurrentOrderInMemory _lc5 = data().provideCurrentOrderInMemoryMain();
        CurrentOrderApi _lc7 = data().provideCurrentOrderApi();
        TripInfoRepository _lc9 = repository().provideTripInfoRepositoryMain(_lc3, _lc1);
        Ref<List<TripInfoRepository>> _lc10 = () -> new ProvideBuilder<TripInfoRepository>((_lc21) -> {
            _lc21.add(repository().provideTripInfoRepositoryMain(_lc3, _lc1));
        }).all();
        CurrentOrderRepository _lc11 = repository().provideCurrentOrderRepositoryMain(_lc7, _lc5);
        Ref<List<CurrentOrderRepository>> _lc12 = () -> new ProvideBuilder<CurrentOrderRepository>((_lc22) -> {
            _lc22.add(repository().provideCurrentOrderRepositoryMain(_lc7, _lc5));
        }).all();
        Ref<List<MapItemsInteractor>> _lc14 = () -> new ProvideBuilder<MapItemsInteractor>((_lc23) -> {
            _lc23.add(intractors().provideMapItemsInteractorMain(_lc11, ListUtils.first(NullGet.let(_lc10, Ref::get))));
        }).all();
        MapViewModel _lc15 = viewmodels().provideMapViewModelMain(ListUtils.first(NullGet.let(_lc14, Ref::get)));
        _lc0.add(_lc15);
    }).first());
    mapScreen.setViewModelSecond(new ProvideBuilder<MapViewModel>((_lc25) -> {
        Ref<List<TripInfoInMemory>> _lc27 = () -> new ProvideBuilder<TripInfoInMemory>((_lc42) -> {
            _lc42.add(data().provideTripInfoInMemorySecond());
        }).all();
        TripInfoApi _lc28 = data().provideTripInfoApi();
        Ref<List<CurrentOrderInMemory>> _lc31 = () -> new ProvideBuilder<CurrentOrderInMemory>((_lc44) -> {
            _lc44.add(data().provideCurrentOrderInMemorySecond());
        }).all();
        CurrentOrderApi _lc32 = data().provideCurrentOrderApi();
        TripInfoRepository _lc34 = repository().provideTripInfoRepositorySecond(_lc28, ListUtils.first(NullGet.let(_lc27, Ref::get)));
        Ref<List<TripInfoRepository>> _lc35 = () -> new ProvideBuilder<TripInfoRepository>((_lc46) -> {
            _lc46.add(repository().provideTripInfoRepositorySecond(_lc28, ListUtils.first(NullGet.let(_lc27, Ref::get))));
        }).all();
        CurrentOrderRepository _lc36 = repository().provideCurrentOrderRepositorySecond(_lc32, ListUtils.first(NullGet.let(_lc31, Ref::get)));
        Ref<List<CurrentOrderRepository>> _lc37 = () -> new ProvideBuilder<CurrentOrderRepository>((_lc47) -> {
            _lc47.add(repository().provideCurrentOrderRepositorySecond(_lc32, ListUtils.first(NullGet.let(_lc31, Ref::get))));
        }).all();
        Ref<List<MapItemsInteractor>> _lc39 = () -> new ProvideBuilder<MapItemsInteractor>((_lc48) -> {
            _lc48.add(intractors().provideMapItemsInteractorSecond(_lc36, ListUtils.first(NullGet.let(_lc35, Ref::get))));
        }).all();
        MapViewModel _lc40 = viewmodels().provideMapViewModelSecond(ListUtils.first(NullGet.let(_lc39, Ref::get)));
        _lc25.add(_lc40);
    }).first());
}

Тысяча готова. И Еще на подходе

Мы в наших приложениях привыкли использовать компоненты в изолированных скоупах, ограниченных в рамках экранов, фрагментов, 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 компонент может легко отличить аргументы провайдинга как зависимости или идентификаторы. И предоставлять все или отдельные зависимости по идентификатору.

Hidden text

DI подставляет идентификаторы по всей цепочке раскручивания зависимостей

@Override
public void inject(MapScreen mapScreen, TripId tripId) {
    mapScreen.setViewModel(new ProvideBuilder<MapViewModel>((_lc0) -> {
        TripInfoInMemory _lc1 = data().provideTripInfoInMemory(tripId);
        TripInfoApi _lc3 = data().provideTripInfoApi();
        CurrentOrderApi _lc5 = data().provideCurrentOrderApi();
        Ref<List<CurrentOrderInMemory>> _lc8 = () -> new ProvideBuilder<CurrentOrderInMemory>((_lc20) -> {
            _lc20.add(data().provideCurrentOrderInMemory(tripId));
        }).all();
        Ref<List<TripInfoRepository>> _lc10 = () -> new ProvideBuilder<TripInfoRepository>((_lc21) -> {
            _lc21.add(repository().provideTripInfoRepository(tripId, _lc3, _lc1));
        }).all();
        CurrentOrderRepository _lc11 = repository().provideCurrentOrderRepository(tripId, _lc5, ListUtils.first(NullGet.let(_lc8, Ref::get)));
        Ref<List<MapItemsInteractor>> _lc14 = () -> new ProvideBuilder<MapItemsInteractor>((_lc23) -> {
            _lc23.add(intractors().provideMapItemsInteractor(tripId, _lc11, ListUtils.first(NullGet.let(_lc10, Ref::get))));
        }).all();
        MapViewModel _lc15 = viewmodels().provideMapViewModel(tripId, ListUtils.first(NullGet.let(_lc14, Ref::get)));
        _lc0.add(_lc15);
    }).first());
}

Сухой остаток

Одной из идей Stone - одна фабрика на все. И все было продумано, для того, чтобы избавиться от всяких фабрик и менеджеров-провайдеров viewModel'ей. А размытые скоупы в априори были призваны использовать минимальное кол-во DI скоупов на весь проект.

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

Заходите на wiki проекта и знакомьтесь с еще более продвинутыми фичами библиотеки.

Комментарии (0)