Всем привет! Меня зовут Базикалов Илья, я являюсь Андроид разработчиком в компании Broniboy. В нашем клиентском приложении мы используем библиотеку Koin для внедрения зависимостей. В своей статье я хочу вам показать, с какими проблемами мы столкнулись при работе с данной библиотекой и каким образом их решили (хоть и не полностью). Koin довольно простой DI фреймворк, который позволяет быстро организовать внедрение зависимостей. Но по мере роста проекта, поддерживать граф зависимостей становится все труднее. Я уверен, что каждый, кто хоть раз использовал эту библиотеку, сталкивался с такой ситуацией: Чем больше подобных классов, тем больше становится проблем с поддержанием DI. Мы стали чаще сталкиваться с падениями в рантайме из-за невнимательного рефакторинга графа зависимостей. Помимо этого, становится проблематично ответить на 2 вопроса: что? и откуда?. Что идет на вход данному классу (в данном случае BasketPresenter)? Откуда эти зависимости приходят? Где объявлены? И объявлены ли вообще? Давайте шаг за шагом начнем исправлять ситуацию, чтобы ответить на эти вопросы стало легче. Начнем с использования именованных аргументов.   Это самое простое решение, которое позволит нам при беглом просмотре модуля понять, какие зависимости используется тем или иным классом.    Уже лучше, хотя тут есть проблема. Имя аргумента не всегда корректно дает нам понять, какая зависимость требуется классу. Хотелось, чтобы вместо get() мы имели конкретный ключ, по которому всегда будет приходить связанный с этим ключом класс.    Qualifier - это фишка Koin, позволяющая объявить ключ, по которому будет вестись поиск класса в графе зависимостей. По сути своей - это интерфейс, в котором необходимо переопределить текстовое поле, являющееся тем самым ключом. Создавать отдельные реализации этого интерфейса не требуется, воспользуемся методом named, поставляемым вместе с Koin.   Помимо строки, в метод named можно передать тип класса, либо Enum. Вроде как уже лучше, но у такого подхода есть серьезная проблема. Очень легко перепутать ключи, либо можно объявить неправильный класс в factory/single методе модуля, например так (обратите внимание на название ключа и создаваемого класса):   В обоих случаях мы получим падение в рантайме.  Чтобы снизить риск ошибки при объявлении классов в модулях Koin, мы решили воспользоваться делегатами. Но прежде чем объявлять делегат, надо определиться, что он будет возвращать.  Основой нашего решения является класс KoinQualifier:   По сути он является оберткой вокруг основных методов получения зависимостей из Koin’а: get/inject - для получения экземпляра класса, factory/single - для его объявления. При этом он наследуется от интерфейса Qualifier, что позволит использовать этот класс в качестве ключа для объявления и получения зависимостей в Koin. Чтобы самим не реализовать интерфейс, делегируем его реализацию входному Qualifier.   Для получения экземпляра KoinQualifier, создадим делегат KoinQualifierDelegate:    При создании KoinQualifier, в качестве ключа передается строка с названием класса и поля, в которой будет хранится созданный делегат. Осталось только сделать метод для получения делегата:   Для начала надо объявить ключ, через который мы будет объявлять и получать необходимую зависимость. В качестве примера возьмем BasketRepository:   После создания ключа необходимо использовать его при объявлении класса в модуле:   this в данном случае - это ссылка на модуль, которую мы получаем при объявлении модуля. И воспользоваться данным ключом для получения зависимости: С таким подходом мы решили для себя несколько проблем. Во-первых стало проще ориентироваться в графе зависимостей. Через поиск мест использования легко найти место объявления класса. Во-вторых внедрение зависимостей стало более типобезопасным. Метод get() класса KoinQualifier возвращает тот класс, что был объявлен в делегате by koinKey<Тип>(). В-третьих, благодаря обертке вокруг методов single/factory нельзя ошибиться при объявлении класса. Компилятор не даст создать класс, которые не является наследником, либо прямой реализацией того типа, что был объявлен в koinKey.   В начале своей статьи я отметил, что мы смогли только частично решить проблемы, связанные с внедрением зависимостей через Koin. Первая и на мой взгляд самая важная проблема - это возможность объявить и использовать ключ без реализации класса внутри модуля Koin. Как было изначально, есть вероятность забыть объявить класс, либо в процессе рефакторинга удалить объявление класса.  Вторая проблема, которая уже стала актуальной для нашего подхода, связано с объявлением типа в koinKey и создаваемым классом внутри модуля. Рассмотрим такую ситуацию: У нас есть интерфейс A, который реализует класс B. Ключ A_KEY возвращает класс, который реализует интерфейс A. В модуле метод factory возвращает наследника интерфейса A. На первый взгляд кажется, что код исправен, но если вы попытаетесь его запустить, то получите ошибку в рантайме: org.koin.core.error.NoBeanDefFoundException: No definition found for class:'com.broniboy.app.A' & qualifier:'Keys-A_KEY'. Связано это с тем, что Koin ведет поиск не только по qualifier’у, но и по названию класса. Исправить ситуацию можно, если явно указать тип в методе factory/single: factory и single - это inline методы, которым передается reified тип создаваемого класса. Из-за этого нет возможности использовать тот тип, что был указан в koinKey. Не смотря на всю свою простоту, Koin очень быстро может превратиться из удобной DI библиотеки в тяжело поддерживаемого монстра, где очень легко выстрелить себе в ногу. Благодаря нашему подходу, мы получили следующие преимущества: Упростилась навигация по графу зависимостей. Снизилось количество NoBeanDefFoundException в крашлитиксе, из-за строгой связки ключ-возвращаемый тип. Нельзя указать неверный класс в factory/single методах Koin’а (за исключением ситуации в разделе “Подводные камни”) Полный код KoinNamedDelegate