Injeção de dependência para Kotlin com injetor

Injeção de dependência para Kotlin com injetor

1. Introdução

Dependency Injection is a software development pattern where we separate object creation from the objects that are being created. Podemos usar isso para manter o código do nosso aplicativo principal o mais limpo possível. Isso, por sua vez, facilita o trabalho e o teste.

Neste tutorial, vamos explorar a estrutura Injekt que traz a injeção de dependência para Kotlin.

2. O que é injeção de dependência?

Injeção de Dependência é um padrão comum de desenvolvimento de software usado para facilitar a manutenção e construção de aplicativos. Usando esse padrão, separamos a construção de nossos objetos de aplicativo do comportamento real de tempo de execução deles. Isso significa que cada parte de nosso aplicativo fica sozinha e não depende diretamente de nenhuma outra parte. Em vez disso, quando construímos nossos objetos, podemos fornecer todas as dependências necessárias.

Usando injeção de dependência, podemos testar facilmente nosso código. Como controlamos as dependências, podemos fornecer diferentes no momento do teste. Isso permite o uso de objetos simulados ou stub para que nosso código de teste esteja no controle absoluto de tudo que está fora da unidade.

Também podemos alterar trivialmente as implementações de algumas partes do aplicativo sem que outras partes precisem ser alteradas. Por exemplo, pode substituir um objeto DAO baseado em JPA por um baseado em MongoDB e, desde que implemente a mesma interface, nada mais precisará mudar. Isso ocorre porque a dependência que está sendo injetada mudou, mas o código no qual ela é injetada não depende diretamente dela.

No desenvolvimento Java, a estrutura de injeção de dependência mais conhecida é Spring. No entanto, quando usamos isso, trazemos muitas funcionalidades adicionais que muitas vezes não precisamos nem queremos. No seu cerne absoluto, a Injeção de Dependência precisa ser apenas uma configuração em que construímos nossos objetos de aplicativo separados de como os usamos.

3. Dependências do Maven

Injekt é uma biblioteca Kotlin padrão e está disponível no Maven Central para inclusão em nosso projeto.

Podemos incluir isso, incluindo a seguinte dependência em nosso projeto:


    uy.kohesive.injekt
    injekt-core
    1.16.1

Para simplificar o código, é recomendável usar importações em estrela para trazer o Injekt para o nosso aplicativo:

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

4. Fiação de aplicativo simples

Quando o Injekt estiver disponível, podemos começar a usá-lo para conectar nossas classes para criar nosso aplicativo.

4.1. Iniciando nosso aplicativo

No caso mais simples, o Injekt fornece uma classe base que podemos usar para a classe principal de nossos aplicativos:

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()
    }
}

Podemos definir nossos beans no métodoregisterInjectables, e então o métodorun é o ponto de entrada real de nosso aplicativo. Aqui, podemos acessar qualquer um dos beans que registramos, conforme necessário.

4.2. Apresentando Objetos Singleton

Como vimos acima, podemos registrar objetos Singleton com nosso aplicativo usando o métodoaddSingleton. Tudo o que isso faz é criar um objeto e colocá-lo em nosso contêiner de injeção de dependência para que outros objetos acessem.

Isso também significa que não podemos fazer referência a outros beans no contêiner ao criá-los, porque o contêiner ainda não existe.

Como alternativa, podemos registrar um retorno de chamada para construir o singleton apenas quando for necessário.

Isso nos dá a capacidade de depender de outros grãos e também significa que não os criamos até que precisemos deles, o que nos torna mais eficientes:

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()) }
}

Observe que construímos nosso beanServer usando um método de retorno de chamada, e ele é fornecido com o objetoConfig necessário diretamente do contêiner Injekt.

Não precisamos dizer ao Injekt os tipos necessários aqui, porque ele pode inferi-los com base no contexto - onde precisa retornar um objeto do tipoConfig, então é isso que obtemos.

4.3. Apresentando objetos de fábrica

Ocasionalmente, queremos que um novo objeto seja criado toda vez que for usado. Por exemplo, podemos ter um objeto que é um cliente de rede para outro serviço, e todo lugar que o utiliza deve ter seu cliente injetado, com sua conexão de rede e tudo mais.

Podemos conseguir isso usando o métodoaddFactory em vez deaddSingletonFactory.

A única diferença aqui é que criaremos uma nova instância em cada injeção, em vez de armazená-la em cache e reutilizá-la:

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()) }
}

Neste exemplo, em todos os lugares em que injetamosClient obteremos uma instância totalmente nova, mas todas essas instâncias compartilharão o mesmo objetoConfig.

5. Acessando objetos

Podemos acessar objetos criados pelo contêiner de maneiras diferentes, dependendo do que for mais apropriado. Acima, já vimos que poderíamos injetar um objeto fora do contêiner em outro no momento da construção.

5.1. Acesso direto do contêiner

Podemos chamarInjekt.get de qualquer lugar em nosso código a qualquer momento, e ele fará a mesma coisa. Isso significa que também podemos chamá-lo em nosso aplicativo ao vivo a qualquer momento para acessar objetos do contêiner.

Isso é especialmente útil para o Factory Objects, onde obteríamos uma nova instância toda vez em tempo de execução, em vez de ser injetada a mesma no momento da construção:

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

Isso também significa que não estamos restritos a usar classes para nosso código. Também podemos acessar objetos do contêiner dentro das funções de nível superior.

5.2. Use como um parâmetro padrão

Kotlin nos permite especificar valores padrão para parâmetros. Também podemos usar o Injekt aqui para que um valor seja obtido do contêiner se um valor alternativo não for fornecido.

This can be especially useful for writing unit tests, onde o mesmo objeto pode ser usado tanto no aplicativo ao vivo - obtendo dependências automaticamente do contêiner ou de testes de unidade - onde podemos fornecer uma alternativa para fins de teste:

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

Podemos usar isso igualmente bem para parâmetros de construtor e parâmetros de método e para classes e funções de nível superior.

5.3. Usando delegados

O Injekt fornece alguns delegados que podemos usar para acessar objetos de contêiner automaticamente como campos de classe.

O delegadoinjectValue obterá um objeto do contêiner imediatamente na construção da classe, enquanto o delegadoinjectLazy obterá um objeto do contêiner apenas quando for usado pela primeira vez:

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

6. Construção Avançada de Objetos

Até agora, tudo o que fizemos foi possível sem o uso do Injekt, embora não tão limpo quanto ao usar o Injekt.

No entanto, existem ferramentas de construção mais avançadas que temos disponíveis, permitindo técnicas mais difíceis de gerenciar por conta própria.

6.1. Objetos por thread

Once we start accessing objects from the container directly in our code, we start to run the risk of object contention. Podemos resolver isso obtendo uma nova instância a cada vez - criada usandoaddFactory - mas isso pode sair caro.

Como alternativa, o Injekt pode criar uma nova instância para cada segmento que o chama, mas depois armazenar em cache a instância para esse segmento.

Isso evita o risco de contenção - cada thread pode fazer apenas uma coisa de cada vez - mas também reduz o número de objetos que precisamos criar:

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

Agora podemos obter um objetoClient a qualquer momento, e sempre será o mesmo para o encadeamento atual, mas nunca o mesmo que em qualquer outro encadeamento.

6.2. Objetos com chave

We need to be careful not to get carried away with the per-thread allocation of objects. Isso é bom se houver um número fixo de threads, mas se as threads estão sendo criadas e freqüentemente destruídas, então nossa coleção de objetos pode crescer desnecessariamente.

Além disso, às vezes precisamos ter acesso a diferentes instâncias da mesma classe ao mesmo tempo, para usar por diferentes razões. Ainda queremos poder acessar a mesma instância pelo mesmo motivo.

Injekt nos dá a capacidade de acessar objetos em uma coleção com chave, onde o chamador que solicita o objeto fornece a chave.

Isso significa que sempre que usarmos a mesma chave, obteremos o mesmo objeto. O método de fábrica também tem acesso a esta chave no caso de ser necessário para modificar a funcionalidade de alguma forma:

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

Agora, podemos obter os Detalhes do cliente OAuth para um provedor nomeado - por exemplo, "Google" ou "twitter". O objeto retornado é preenchido corretamente com base nas propriedades do sistema definidas no aplicativo.

7. Construção de aplicação modular

Até agora, só construímos nosso contêiner em um único lugar. Isso funciona, mas ficará complicado ao longo do tempo.

Injekt gives us the ability to do better than this but splitting our configuration up into modules. Isso nos permite ter áreas de configuração menores e mais direcionadas. Também nos permite incluir a configuração nas bibliotecas às quais elas se aplicam.

Por exemplo, podemos ter uma dependência que representa um bot do Twitter. Isso pode incluir um módulo Injekt, para que qualquer pessoa que o utilize possa conectá-lo diretamente.

Módulos são objetos Kotlin que estendem a classe baseInjektModule e que implementam o métodoregisterInjectables().

Já fizemos isso com a classeInjektMain que usamos anteriormente. Essa é uma subclasse direta deInjektModulee funciona da mesma maneira:

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

Assim que tivermos um módulo, podemos incluí-lo em qualquer outro lugar em nosso contêiner usando o métodoimportModule:

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

Neste ponto, todos os objetos definidos neste módulo estão disponíveis exatamente como se fossem definidos diretamente aqui.

8. Conclusão

Neste artigo, fornecemos uma introdução à injeção de dependência em Kotlin e como a biblioteca Injekt torna isso simples de alcançar.

Há muito mais do que podemos obter usando o Injekt, conforme mostrado aqui. Felizmente, isso deve iniciar a jornada para a injeção simples de dependência.

E, como sempre, veja os exemplos de toda essa funcionalidadeover on GitHub.