Внедрение зависимостей для Kotlin с Injekt

Инъекция зависимостей для Kotlin с Injekt

1. Вступление

Dependency Injection is a software development pattern where we separate object creation from the objects that are being created. Мы можем использовать это, чтобы сохранить код нашего основного приложения как можно более чистым. Это, в свою очередь, облегчает работу и тестирование.

В этом уроке мы собираемся изучить платформу Injekt, которая обеспечивает внедрение зависимостей в Kotlin.

2. Что такое инъекция зависимостей?

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

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

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

В Java-разработке наиболее известной платформой Dependency Injection является Spring. Однако, когда мы используем это, мы добавляем множество дополнительных функций, которые нам часто не нужны и не нужны. По своей сути Dependency Injection должна быть лишь установкой, в которой мы создаем объекты нашего приложения отдельно от того, как мы их используем.

3. Maven Зависимости

Injekt - это стандартная библиотека Kotlin, которая доступна в Maven Central для включения в наш проект.

Мы можем включить это, включая следующую зависимость в наш проект:


    uy.kohesive.injekt
    injekt-core
    1.16.1

Чтобы сделать код проще, рекомендуется использовать импорт звездочек, чтобы добавить Injekt в наше приложение:

import uy.kohesive.injekt.*
import uy.kohesive.injekt.api.*

4. Простое подключение приложений

Как только Injekt станет доступен, мы сможем начать использовать его для соединения наших классов для создания нашего приложения.

4.1. Запуск нашего приложения

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

class SimpleApplication {
    companion object : InjektMain() {
        @JvmStatic fun main(args: Array) {
            SimpleApplication().run()
        }

        override fun InjektRegistrar.registerInjectables() {
            addSingleton(Server())
        }
    }

    fun run() {
        val server = Injekt.get()
        server.start()
    }
}

Мы можем определить наши bean-компоненты в методеregisterInjectables, и тогда методrun будет фактической точкой входа в наше приложение. Здесь мы можем получить доступ к любому из зарегистрированных нами bean-компонентов по мере необходимости.

4.2. Введение в одноэлементные объекты

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

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

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

Это дает нам возможность зависеть от других bean-компонентов, а также означает, что мы не создаем их, пока они нам не понадобятся, что делает нас более эффективными:

class Server(private val config: Config) {
    private val LOG = LoggerFactory.getLogger(Server::class.java)
    fun start() {
        LOG.info("Starting server on ${config.port}")
    }
}
override fun InjektRegistrar.registerInjectables() {
    addSingleton(Config(port = 12345))
    addSingletonFactory { Server(Injekt.get()) }
}

Обратите внимание, что мы создаем наш bean-компонентServer с помощью метода обратного вызова, и ему предоставляется требуемый объектConfig непосредственно из контейнера Injekt.

Нам не нужно сообщать Injekt о необходимых здесь типах, потому что он может вывести их на основе контекста - где ему нужно вернуть объект типаConfig, так что это то, что мы получаем.

4.3. Представляем фабричные объекты

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

Мы можем добиться этого, используя методaddFactory вместоaddSingletonFactory.

Единственное отличие состоит в том, что мы будем создавать новый экземпляр при каждой инъекции, а не кэшировать его и использовать повторно:

class Client(private val config: Config) {
    private val LOG = LoggerFactory.getLogger(Client::class.java)
    fun start() {
        LOG.info("Opening connection to on ${config.host}:${config.port}")
    }
}
override fun InjektRegistrar.registerInjectables() {
    addSingleton(Config(host = "example.com", port = 12345))
    addFactory { Client(Injekt.get()) }
}

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

5. Доступ к объектам

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

5.1. Прямой доступ из контейнера

Мы можем вызватьInjekt.get из любого места нашего кода в любое время, и он сделает то же самое. Это означает, что мы также можем вызвать его из нашего реального приложения в любое время для доступа к объектам из контейнера.

Это особенно полезно для Factory Objects, где мы будем получать новый экземпляр каждый раз во время выполнения вместо того, чтобы вводить один и тот же во время создания:

class Notifier {
    fun sendMessage(msg: String) {
        val client: Client = Injekt.get()
        client.use {
            client.send(msg)
        }
    }
}

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

5.2. Использовать как параметр по умолчанию

Kotlin позволяет нам определять значения по умолчанию для параметров. Мы также можем использовать Injekt, чтобы значение получалось из контейнера, если не указано альтернативное значение.

This can be especially useful for writing unit tests, где один и тот же объект может использоваться как в реальном приложении - получение зависимостей автоматически из контейнера, так и из модульных тестов - где мы можем предоставить альтернативу для целей тестирования:

class Client(private val config: Config = Injekt.get()) {
    ...
}

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

5.3. Использование делегатов

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

ДелегатinjectValue получит объект из контейнера сразу при создании класса, тогда как делегатinjectLazy получит объект из контейнера только при первом использовании:

class Notifier {
    private val client: Client by injectLazy()
}

6. Расширенное строительство объектов

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

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

6.1. Объекты по потоку

Once we start accessing objects from the container directly in our code, we start to run the risk of object contention. Мы можем решить эту проблему, получая каждый раз новый экземпляр, созданный с использованиемaddFactory, но это может обойтись дорого.

Кроме того, Injekt может создать новый экземпляр для каждого вызывающего его потока, но затем кэшировать экземпляр для этого потока.

Это позволяет избежать риска конфликтов - каждый поток может выполнять только одну вещь за раз - но также уменьшает количество объектов, которые нам нужно создать:

override fun InjektRegistrar.registerInjectables() {
    addPerThreadFactory { Client(Injekt.get()) }
}

Теперь мы можем получить объектClient в любое время, и он всегда будет таким же для текущего потока, но никогда таким же, как в любом другом потоке.

6.2. Ключевые объекты

We need to be careful not to get carried away with the per-thread allocation of objects. Это нормально, если количество потоков фиксировано, но если потоки создаются и часто уничтожаются, наша коллекция объектов может расти без нужды.

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

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

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

override fun InjektRegistrar.registerInjectables() {
    addPerKeyFactory { provider: String ->
        OAuthClientDetails(
            clientId = System.getProperty("oauth.provider.${provider}.clientId"),
            clientSecret = System.getProperty("oauth.provider.${provider}.clientSecret")
        )
    }
}

Теперь мы можем получить сведения о клиенте OAuth для указанного провайдера - например, «Google» или «Twitter». Возвращенный объект корректно заполняется на основе системных свойств, установленных в приложении.

7. Модульная конструкция приложения

Пока мы строили наш контейнер только в одном месте. Это работает, но со временем станет громоздким.

Injekt gives us the ability to do better than this but splitting our configuration up into modules. Это позволяет нам иметь меньшие, более целевые области конфигурации. Это также позволяет нам включать конфигурацию в библиотеки, к которым они применяются.

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

Модули - это объекты Kotlin, которые расширяют базовый классInjektModule и реализуют методregisterInjectables().

Мы уже сделали это с классомInjektMain, который мы использовали ранее. Это прямой подклассInjektModule и работает точно так же:

object TwitterBotModule : InjektModule {
    override fun InjektRegistrar.registerInjectables() {
        addSingletonFactory { TwitterConfig(clientId = "someClientId", clientSecret = "someClientSecret") }
        addSingletonFactory { config = TwitterBot(Injekt.get()) }
    }
}

Когда у нас есть модуль, мы можем включить его где угодно в нашем контейнере, используя методimportModule:

override fun InjektRegistrar.registerInjectables() {
    importModule(TwitterBotModule)
}

На этом этапе все объекты, определенные в этом модуле, доступны точно так, как если бы они были определены непосредственно здесь.

8. Заключение

В этой статье мы познакомились с внедрением зависимостей в Kotlin и рассказали, как библиотека Injekt упрощает эту задачу.

С помощью Injekt мы можем достичь гораздо большего, чем показано здесь. Надеюсь, это поможет вам начать путешествие к простой инъекции зависимостей.

И, как всегда, ознакомьтесь с примерами всей этой функциональностиover on GitHub.