KA0709 — Внедрение зависимостей

  • Запись изменена:08.12.2022
  • Post category:Публикации
  • Reading time:17 минут чтения

Если оставить в стороне одноклеточные организмы, почти все в мире зависит от других существ, чтобы функционировать. Будь то что-то в природе или что-то, созданное человечеством, обычно требуется несколько вещей, чтобы создать работающий экземпляр чего-либо.

Представьте себе конвейер на автомобильном заводе. Они не создают двигатели и колеса на конвейере. Производители автомобилей передают многие детали на аутсорсинг другим компаниям. В конце концов, они привозят их все на сборочную линию, вводят каждую деталь в незавершенное производство, и появляется новая блестящая машина. Автомобиль зависит от других объектов. То же самое относится и к миру программного обеспечения.

Если бы вы моделировали Carкласс в качестве одной из его зависимостей, это был бы файл Engine. Объект автомобиля не должен отвечать за создание двигателя. Вы должны внедрить двигатель извне в сборочную линию — или в программную номенклатуру, конструктор или инициализатор.

Преимущества внедрения зависимостей

Внедрение зависимостей или DI имеет много преимуществ.

  • Ремонтопригодность : DI делает ваш код ремонтопригодным. Если ваши классы слабо связаны, вам будет легче выявлять ошибки и решать возможную проблему быстрее, чем если бы вы использовали сложный класс, не придерживающийся принципа единой ответственности.
  • Повторное использование : Возвращаясь к примеру с автомобильным заводом, вы можете повторно использовать одну и ту же модель колес для многих автомобилей, которые производит завод. Слабосвязанный код позволит вам повторно использовать многие части вашего кода по-разному.
  • Простота рефакторинга : в жизни вашего приложения может наступить момент, когда вам нужно будет внести изменения в кодовую базу. Чем менее связаны ваши занятия, тем проще будет процесс. Представьте, что вам нужно поменять двигатель, если вы хотите иметь новые фары!
  • Тестируемость : все возвращается к слабосвязанному коду. Если каждый объект автономен, вы можете проверить его функциональность независимо от других. Никто не хотел бы машину, двигатель которой не работал бы, когда сломался стеклоочиститель! Таким образом, каждая команда, ответственная за каждый модуль, будет тестировать свой продукт и передавать его другим командам.
  • Простота работы в командах : как неявно упоминалось в других пунктах, DI сделает продукт доступным для производства разными командами. Это также делает код более читабельным и понятным, поскольку он прост и не содержит ненужных дополнений.

Автоматический DI против ручного DI

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

Откройте стартовый проект в Android Studio. Затем откройте RemindersViewModel.kt из каталога презентаций в commonMain . Вы возьмете на себя ответственность за создание repositoryэкземпляра снаружи RemindersViewModel.

Удалите repositoryопределение и передайте его через конструктор следующим образом:

class RemindersViewModel( private val repository: RemindersRepository ) : BaseViewModel() {// ... }

Создайте проект, перейдя в меню « Сборка » и нажав «Создать проект» . Вы сразу увидите проблемы с компиляцией как в файлах RemindersView.kt на Android, так и на настольных компьютерах. Та же ошибка возникает и для RemindersView.swift , которую Android Studio не может отловить.

Рис. 9.1 — Не передано значение для репозитория

Рис. 9.1 — Не передано значение для репозитория

Примечание . Одной из реализаций, которая не будет отображаться в приведенном выше выводе, но ее все же необходимо обновить, является viewModelопределение в RemindersViewModelTest.kt . Пройти RemindersRepository()в RemindersViewModel()класс.

Вы должны перейти к каждому из этих файлов и предоставить экземпляр RemindersRepository. Что делать, если repositoryесть свои собственные зависимости? А что, если у этих зависимостей тоже есть свои зависимости? Это кроличья нора, в которую лучше не попасть!

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

Однако в мире Android некоторые библиотеки, решающие эту проблему, автоматизируют процесс создания и предоставления зависимостей. Они делятся на две категории:

  • Статические решения, которые генерируют граф зависимостей во время компиляции.
  • Решения, которые подключают зависимости во время выполнения.

Самая известная библиотека для первой категории — Dagger . Недавно Google представил Hilt , который они построили на базе Dagger. Google рекомендует Hilt как часть предложений по архитектуре приложений.

Загвоздка в том, что ни Dagger, ни Hilt не доступны для KMP. Таким образом, подход, который вы можете выбрать, — это делать DI вручную или использовать самую известную библиотеку второй категории: Koin .

Многие называют такие библиотеки, как Koin, которые разрешают зависимости во время выполнения , Service Locators . Те, кто предпочитает статические библиотеки DI, будут серьезно возражать, если вы назовете Koin библиотекой DI. Впрочем, здесь вы вольны называть это как угодно.

Настройка Коина

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

Откройте build.gradle.kts для проекта и добавьте константу верхнего уровня для версии Koin. На момент написания последней версией Koin была 3.1.5.

val koinVersion by extra("3.1.5")

Затем откройте build.gradle.kts для общего модуля.

Добавьте зависимость для исходного набора commonMain следующим образом:

implementation("io.insert-koin:koin-core:${rootProject.extra["koinVersion"]}")

Пока вы здесь, добавьте также тестовую зависимость в commonTest . Он понадобится вам позже в этой главе.

implementation("io.insert-koin:koin-test:${rootProject.ext["koinVersion"]}")

Затем откройте build.gradle.kts для androidApp и добавьте эти две зависимости. Второй необходим, потому что приложение использует Jetpack Compose.

implementation("io.insert-koin:koin-android:${rootProject.ext["koinVersion"]}") implementation("io.insert-koin:koin-androidx-compose:${rootProject.ext["koinVersion"]}")

И последнее, но не менее важное: откройте build.gradle.kts для desktopApp и добавьте эту зависимость в jvmMain:

implementation("io.insert-koin:koin-core:${rootProject.ext["koinVersion"]}")

Обязательно синхронизируйте Gradle после добавления всех этих зависимостей.

Объявление зависимостей вашего приложения для Koin

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

Есть три шага, чтобы начать использовать Koin:

  1. Объявите свои модули . Модули — это объекты, которые Koin позже внедряет в различные части вашего приложения по мере необходимости. У вас может быть столько модулей, сколько вы хотите.
  2. Запуск Koin : один вызов startKoinфункции, передающий модули в вашем приложении, сделает экземпляр Koin готовым для выполнения работы по внедрению в вашем приложении.
  3. Выполнение инъекции : использование некоторых специальных ключевых слов, предоставленных Koin, позволяет вам вводить экземпляры объектов по желанию.

Внутри общего модуля в каталоге commonMain создайте одноуровневый файл для Platform.kt с именем KoinCommon.kt . Вы собираетесь написать коды установки Koin там.

Во-первых, создайте объект, в котором вы можете хранить ссылки на модули.

object Modules { val repositories = module { factory { RemindersRepository() } } }

Определите модуль, используя moduleблок. A factory— это определение, которое будет давать вам новый экземпляр каждый раз, когда вы запрашиваете этот тип объекта. Если вы хотите иметь один экземпляр или синглтон на протяжении всего жизненного цикла вашего приложения, используйте singleключевое слово. Он больше всего подходит для таких вещей, как базы данных и сетевые менеджеры.

Во-вторых, добавьте константу для модуля ViewModel внутри Modulesобъекта.

val viewModels = module { factory { RemindersViewModel(get()) } }

Новичок в городе — это get()функция. Это общая функция, которая разрешает зависимость компонента. Когда вы используете эту функцию, Koin просматривает предоставленное вами объявление и находит соответствующий вызов. Как вы помните, в своем конструкторе RemindersViewModelтребуется экземпляр a RemindersRepository, и вы только что определили его как модуль.

Итак, Коин готов к работе! Имейте в виду, что Koin разрешает эту зависимость во время выполнения. Следовательно, если вы используете get()без соответствующего объявления, ваше приложение, скорее всего, выйдет из строя.

Наконец, ниже создайте глобальную функцию Modules, которую вы будете вызывать с каждой платформы.

fun initKoin( appModule: Module = module { }, repositoriesModule: Module = Modules.repositories, viewModelsModule: Module = Modules.viewModels, ): KoinApplication = startKoin { modules( appModule, repositoriesModule, viewModelsModule, ) }

Эта функция принимает три параметра.

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

Второй и третий параметры предназначены для репозиториев и моделей представления со значениями по умолчанию. Позже вы увидите, что вам нужно пройти их в определенных сценариях.

Возвращаемый тип initKoin— это экземпляр KoinApplication. Вы получаете экземпляр этого типа, вызывая startKoin, передавая все определенные вами модули. Это отправная точка Koin и клей, который держит все вместе.

Использование Koin на каждой платформе

Теперь, когда у вас есть все части, пришло время использовать Koin по-настоящему.

Андроид

Откройте OrganizeApp.kt в модуле androidApp . Добавьте onCreateфункцию ниже внутри класса и запустите там Koin.

override fun onCreate() { super.onCreate() initKoin( viewModelsModule = module { viewModel { RemindersViewModel(get()) } } ) }

Вызовите initKoinметод, который вы определили ранее. Вы используете viewModelблок из org.koin.androidx.viewmodel.dsl.viewModelпакета, чтобы объявить модель представления Android. Разница между моделями представления Android и другими заключается в том, что они будут учитывать изменения конфигурации Android, такие как ротация устройства. Для этого требуется особая инициализация, которую Koin для Android делает за вас.

Откройте RemindersView.kt в модуле androidApp и замените RemindersViewопределение функции следующим образом:

@Composable fun RemindersView( viewModel: RemindersViewModel = getViewModel(), onAboutButtonClick: () -> Unit, ) { // ... }

Вызов getViewModel()функции расширения, которую предоставляет Koin, выполняет весь процесс создания и внедрения.

Создайте и запустите приложение для Android. Приложение должно вести себя так, как вы привыкли, но на этот раз с DI.

Рис. 9.2 — Android-приложение после интеграции Koin.

Рис. 9.2 — Android-приложение после интеграции Koin.

iOS

Koin — это библиотека Kotlin. Происходит много мостов, если вы хотите использовать его с файлами и классами Swift и Objective-C. Чтобы упростить задачу, вам лучше создать несколько вспомогательных классов и функций.

Откройте KoinIOS.kt в каталоге iosMain .

Создайте функцию внутри объекта для инициализации Koin на iOS. Swift не объединяет функции Kotlin с параметрами по умолчанию. Эта функция предназначена для компенсации этого ограничения.

object KoinIOS { fun initialize(): KoinApplication = initKoin() }

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

fun Koin.get(objCClass: ObjCClass): Any { val kClazz = getOriginalKotlinClass(objCClass)!! return get(kClazz, null, null) }

Здесь вы проходите nullза qualifierи parameter. Если вам нужно передать параметры при запросе зависимости, вы также можете добавить эту функцию расширения:

fun Koin.get(objCClass: ObjCClass, qualifier: Qualifier?, parameter: Any): Any { val kClazz = getOriginalKotlinClass(objCClass)!! return get(kClazz, qualifier) { parametersOf(parameter) } }

Примечание . На устройствах Mac с Apple Silicon вы можете обнаружить, что Android Studio не может импортировать необходимые пакеты для совместимости с Objective-C. Обязательно добавьте директивы импорта следующим образом:

import kotlinx.cinterop.ObjCClass import kotlinx.cinterop.getOriginalKotlinClass

Далее открываем Xcode и переходим в Koin.swift и пишем класс следующим образом:

«import shared
final class Koin {
//1
private var core: Koin_coreKoin?
//2
static let instance = Koin()
//3
static func start() {
if instance.core == nil {
let app = KoinIOS.shared.initialize()
instance.core = app.koin
}
if instance.core == nil {
fatalError(«Can’t initialize Koin.»)
}
}
//4
private init() {
}

//5
func get() -> T {
guard let core = core else {
fatalError(«You should call start() before using (#function)»)
}
guard let result = core.get(objCClass: T.self) as? T else {
fatalError(«Koin can’t provide an instance of type: (T.self)»)
}
return result
}

  1. Сохраните ссылку на основной тип Koin. Это позволит просить объекты.
  2. Создайте статическое свойство для вновь созданного класса, чтобы использовать его как синглтон.
  3. Вызовите эту функцию при запуске приложения. Здесь вы вызываете Kotlin для инициализации Koin. KoinIOS.sharedэто способ, которым Kotlin раскрывает objectсозданный вами ранее. Если по какой-либо причине эта процедура завершится неудачно, приложение выйдет из строя.
  4. Пометьте инициализатор для этого класса как закрытый. Это предотвратит случайную инициализацию Koinкласса Swift людьми не так, как вы предполагали.
  5. Этот метод использует getметоды расширения, которые вы написали для Koin в Kotlin. Сначала он проверяет, coreне является ли это nil. Затем он пытается привести Anyк общему типу T. Это сделает эту функцию безопасной для типов на месте вызова.

Осталось два шага для использования Koin на iOS.

Сначала откройте iOSApp.swift и запустите Koin во время инициализации.

@main struct iOSApp: App { init() { Koin.start() } // ... }

Наконец, откройте RemindersViewModelWrapper.swift и выполните инициализацию viewModelследующим образом:

let viewModel: RemindersViewModel = Koin.instance.get()

Стройте и запускайте. Приложение будет работать как прежде.

Рис. 9.3 — Приложение для iOS после интеграции Koin.

Рис. 9.3 — Приложение для iOS после интеграции Koin.

Рабочий стол

Это самая простая из всех платформ. Сначала откройте Main.kt и добавьте ссылку на объект Koin . Инициализируйте его в mainфункции.

`lateinit var koin: Koin
fun main() {
koin = initKoin().koin

return application { // … }
//`

Затем откройте RemindersView.kt в модуле desktopAppRemindersView и измените определение составной функции следующим образом:

@Composable fun RemindersView( viewModel: RemindersViewModel = koin.get(), onAboutButtonClick: () -> Unit, ) { // ... }

Используйте koinсозданный вами экземпляр и воспользуйтесь get()функцией.

Стройте и запускайте — наслаждайтесь!

Рис. 9.4 — Приложение для рабочего стола после интеграции Koin.

Рис. 9.4 — Приложение для рабочего стола после интеграции Koin.

Обновление AboutViewModel

Теперь вы знакомы с процессом, пришло время обновиться AboutViewModel, чтобы использовать DI. Отложите книгу и посмотрите, сможете ли вы сделать все это самостоятельно.

Вот что вы должны сделать:

Откройте AboutViewModel.kt из каталога commonMain/presentation и переместите platformопределение в конструктор следующим образом:

class AboutViewModel( platform: Platform ) : BaseViewModel() { // ... }

Затем откройте KoinCommon.kt в каталоге commonMain и добавьте factoryв модуль еще один блок viewModels.

val viewModels = module { factory { RemindersViewModel(get()) } factory { AboutViewModel(get()) } }

Объявите Koin, как разрешить зависимость AboutViewModelкласса, который имеет тип Platform. Создайте константу, вызываемую coreвнутри Modulesобъекта, для хранения этой и некоторых других зависимостей, которые появятся в следующей главе.

object Modules { val core = module { factory { Platform() } } // ... }

И последнее, но не менее важное: в этом файле обновите определение, initKoinчтобы принять новые определенные вами модули:

fun initKoin( appModule: Module = module { }, coreModule: Module = Modules.core, repositoriesModule: Module = Modules.repositories, viewModelsModule: Module = Modules.viewModels, ): KoinApplication = startKoin { modules( appModule, coreModule, repositoriesModule, viewModelsModule, ) }

Следуя обычному подходу, вы обновите коды для платформ по порядку.

Андроид

Откройте AndroidView.kt в модуле androidAppAboutView и измените составное определение на это:

fun AboutView( viewModel: AboutViewModel = getViewModel(), onUpButtonClick: () -> Unit ) { // ... }

Вы снова используете getViewModel()метод из библиотеки Koin Android.

Наконец, откройте OrganizeApp.kt для приложения Android и также удалите модуль для AboutViewModel:

initKoin( viewModelsModule = module { viewModel { RemindersViewModel(get()) } viewModel { AboutViewModel(get()) } } )

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

iOS

Это довольно просто. Откройте AboutView.swift и измените определение viewModelследующим образом:

@StateObject private var viewModel: AboutViewModel = Koin.instance.get()

Вот и все! Создайте и запустите приложение для iOS. Откройте страницу «О программе», чтобы убедиться, что DI работает правильно.

Рабочий стол

Это тоже кусок пирога. Откройте AboutView.kt в модуле desktopAppAboutView и измените определение составной функции:

fun AboutView( viewModel: AboutViewModel = koin.get() ) { ContentView(items = viewModel.items) }

На этом интеграция Koin в модели представления всех трех приложений завершена.

Соберите, запустите и убедитесь, что приложение ведет себя как раньше.

Тестирование

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

Проверка интеграции с Koin

Как вы уже знаете, Koin разрешает граф зависимостей во время выполнения. Стоит проверить, может ли он разрешить все зависимости, предоставляющие объявленные вами модули.

В каталоге commonTest создайте файл с именем DITest.kt в качестве родственного файла PlatformTest.kt .

Создайте класс с именем DITestи добавьте в него эту тестовую функцию:

class DITest { @Test fun testAllModules() { koinApplication { modules( Modules.viewModels, ) }.checkModules() } }

Здесь вы создаете экземпляр KoinApplicationи предоставляете список модулей. А пока добавьте viewModelsмодуль. Это checkModules()функция, которая выполняет проверку интеграции, о которой вы читали ранее. Он проверяет зависимости всех определений, запускает все модули, а затем проверяет, могут ли выполняться определения.

Запустите тест и проверьте результат.

Рис. 9.5 — Тест Koin checkModules не прошел

Рис. 9.5 — Тест Koin checkModules не прошел

Тест провалился, и причина довольно очевидна. Предоставляя только viewModelsмодуль, Koin не может создать экземпляр RemindersViewModelили AboutViewModel. Чтобы это исправить, просто добавьте Modules.repositoriesи Modules.coreв список модулей в тестовой функции выше. Таким образом, модули, которые вы передаете, будут такими:

modules( Modules.core, Modules.repositories, Modules.viewModels, )

Запустите тест еще раз, и он пройдет успешно.

Примечание . Тест проходит только на ПК и iOS. На Android не получается. Причина этого кроется в процессе создания ScreenInfo. Если вы посмотрите на реальную реализацию этого класса в Android, вы увидите, что он вызывает Resources.getSystem(). При выполнении тестов этот вызов завершится ошибкой, так как система Android недоступна во время модульного тестирования. Для решения этой проблемы необходимо создать макет Resourcesкласса, что выходит за рамки этой главы.

Хороший гражданин не мусорит, как и хороший разработчик при тестировании Koin. Всякий раз, когда вы создаете экземпляр KoinApplicationили вызываете startKoin, обязательно остановите его, когда он вам больше не понадобится. Как вы знаете, хорошее место для этого — создать функцию с @AfterTestаннотацией.

Добавьте эту функцию в DITestкласс, который использует предоставленный Koin stopKoinметод для очистки.

@AfterTest fun tearDown() { stopKoin() }

Обновление RemindersViewModelTest

Откройте RemindersViewModelTest.kt . Есть lateinitсвойство, которое содержит ссылку на экземпляр RemindersViewModel. В setupметоде вы инициализируете это свойство следующим образом:

viewModel = RemindersViewModel(RemindersRepository())

Фу! Это больше никому не нравится. Лучше вызвать Коина для выполнения задания!

Есть несколько шагов, которые необходимо предпринять, чтобы интегрировать Koin в тесты.

Во- первых, сделайте RemindersViewModelTestсоответствие KoinTest. Это соответствие сделает тестовый класс KoinComponent. Интерфейс KoinComponentздесь, чтобы помочь вам получить экземпляры непосредственно из Koin, используя некоторые специальные ключевые слова.

Затем сделайте RemindersViewModelTestрасширение из KoinTestи измените viewModelопределение свойства на это:

class RemindersViewModelTest : KoinTest { private val viewModel: RemindersViewModel by inject() //... }

Ключевое byслово в Kotlin делегирует реализацию средств доступа к свойству другому объекту. Используя by inject(), Koin будет лениво извлекать ваши экземпляры из графа зависимостей. Функция inject()является расширением KoinTest.

Последняя часть — это инициализация Koin перед тестом и его остановка после теста — очень похоже на то, когда вы проверяли целостность Koin.

Реализуйте эти две функции в тестовом классе:

@BeforeTest fun setup() { initKoin() } @AfterTest fun tearDown() { stopKoin() }

В setupметоде вам больше не нужно инициализировать viewModelсвойство самостоятельно. Вы просто вызываете initKoinметод, который вызывали при запуске приложения. Он предоставляет значения по умолчанию для модулей. Функция tearDownточно такая же, как и в DITestклассе.

Запустите тесты для RemindersViewModelTestкласса, и они пройдут, как и раньше.

Ключевые моменты

  • Классы должны соблюдать принцип единой ответственности и не должны создавать свои собственные зависимости.
  • Внедрение зависимостей — это необходимый шаг для создания поддерживаемой, масштабируемой и тестируемой кодовой базы.
  • Вы можете внедрить зависимости в классы вручную или использовать библиотеку, которая сделает за вас весь шаблонный код.
  • Koin — популярная декларативная библиотека для DI, поддерживающая Kotlin Multiplatform.
  • Вы объявляете зависимости как модули, и Koin разрешает их во время выполнения, когда это необходимо.
  • При тестировании кода вы можете использовать Koin для инъекций.
  • Вы можете проверить, может ли Koin разрешать зависимости во время выполнения, используя checkModulesметод.
  • Соответствуя KoinTest, ваши тестовые классы могут получать экземпляры объектов из Koin, используя некоторые операторы, такие как by inject().

Куда пойти отсюда?

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

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

Как упоминалось в главе, существуют и другие библиотеки DI. Однако либо они не работают в мультиплатформенных сценариях, либо не так известны, как Koin.

Если вы не нацелены на многоплатформенность, вы можете обратиться к Hilt and Dagger на Android. В мире iOS нет доступной библиотеки. Тем не менее, Resolver в последнее время набирает обороты.

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