Injeção de dependência de Kotlin com Kodein

Injeção de dependência de Kotlin com Kodein

1. Visão geral

Neste artigo, vamos apresentarKodein - uma estrutura Kotlin de injeção de dependência (DI) pura - e compará-la com outras estruturas DI populares.

2. Dependência

Primeiro, vamos adicionar a dependência Kodein ao nossopom.xml:


    com.github.salomonbrys.kodein
    kodein
    4.1.0

Observe que a última versão disponível está disponível emMaven Central oujCenter.

3. Configuração

Usaremos o modelo abaixo para ilustrar a configuração baseada em Kodein:

class Controller(private val service : Service)

class Service(private val dao: Dao, private val tag: String)

interface Dao

class JdbcDao : Dao

class MongoDao : Dao

4. Tipos de ligação

A estrutura Kodein oferece vários tipos de ligação. Vamos dar uma olhada em como eles funcionam e como usá-los.

4.1. Singleton

Com ligação deSingleton,a target bean is instantiated lazily on the first accesse reutilizada em todas as solicitações adicionais:

var created = false;
val kodein = Kodein {
    bind() with singleton { MongoDao() }
}

assertThat(created).isFalse()

val dao1: Dao = kodein.instance()

assertThat(created).isFalse()

val dao2: Dao = kodein.instance()

assertThat(dao1).isSameAs(dao2)

Nota: podemos usarKodein.instance() para recuperar beans gerenciados por destino com base em um tipo de variável estática.

4.2. Eager Singleton

Isso é semelhante à ligaçãoSingleton. A única diferença é quethe initialization block is called eagerly:

var created = false;
val kodein = Kodein {
    bind() with singleton { MongoDao() }
}

assertThat(created).isTrue()
val dao1: Dao = kodein.instance()
val dao2: Dao = kodein.instance()

assertThat(dao1).isSameAs(dao2)

4.3. Fábrica

Com a ligaçãoFactory, o bloco de inicialização recebe um argumento ea new object is returned from it every time:

val kodein = Kodein {
    bind() with singleton { MongoDao() }
    bind() with factory { tag: String -> Service(instance(), tag) }
}
val service1: Service = kodein.with("myTag").instance()
val service2: Service = kodein.with("myTag").instance()

assertThat(service1).isNotSameAs(service2)

Nota: podemos usarKodein.instance() para configurar dependências transitivas.

4.4. Multiton

A ligação deMultiton é muito semelhante à ligação deFactory. A única diferença é quethe same object is returned for the same argument in subsequent calls:

val kodein = Kodein {
    bind() with singleton { MongoDao() }
    bind() with multiton { tag: String -> Service(instance(), tag) }
}
val service1: Service = kodein.with("myTag").instance()
val service2: Service = kodein.with("myTag").instance()

assertThat(service1).isSameAs(service2)

4.5. Fornecedor

Esta é uma ligação sem argFactory:

val kodein = Kodein {
    bind() with provider { MongoDao() }
}
val dao1: Dao = kodein.instance()
val dao2: Dao = kodein.instance()

assertThat(dao1).isNotSameAs(dao2)

4.6. Instância

Podemosregister a pre-configured bean instance no contêiner:

val dao = MongoDao()
val kodein = Kodein {
    bind() with instance(dao)
}
val fromContainer: Dao = kodein.instance()

assertThat(dao).isSameAs(fromContainer)

4.7. Tagging

Também podemosregister more than one bean of the same type com tags diferentes:

val kodein = Kodein {
    bind("dao1") with singleton { MongoDao() }
    bind("dao2") with singleton { MongoDao() }
}
val dao1: Dao = kodein.instance("dao1")
val dao2: Dao = kodein.instance("dao2")

assertThat(dao1).isNotSameAs(dao2)

4.8. Constante

Este é um açúcar sintático sobre ligação marcada e é assumido comoto be used for configuration constants - tipos simples sem herança:

val kodein = Kodein {
    constant("magic") with 42
}
val fromContainer: Int = kodein.instance("magic")

assertThat(fromContainer).isEqualTo(42)

5. Separação de Ligações

O Kodein nos permite configurar os beans em blocos separados e combiná-los.

5.1. Módulos

Podemosgroup components by particular criteria - por exemplo, todas as classes relacionadas à persistência de dados -and combine the blocks to build a resulting container:

val jdbcModule = Kodein.Module {
    bind() with singleton { JdbcDao() }
}
val kodein = Kodein {
    import(jdbcModule)
    bind() with singleton { Controller(instance()) }
    bind() with singleton { Service(instance(), "myService") }
}

val dao: Dao = kodein.instance()
assertThat(dao).isInstanceOf(JdbcDao::class.java)

Nota: como os módulos contêm regras de ligação, os beans de destino são recriados quando o mesmo módulo é usado em várias instâncias do Kodein.

5.2. Composição

Podemos estender uma instância do Kodein de outra - isso nos permite reutilizar beans:

val persistenceContainer = Kodein {
    bind() with singleton { MongoDao() }
}
val serviceContainer = Kodein {
    extend(persistenceContainer)
    bind() with singleton { Service(instance(), "myService") }
}
val fromPersistence: Dao = persistenceContainer.instance()
val fromService: Dao = serviceContainer.instance()

assertThat(fromPersistence).isSameAs(fromService)

5.3. Substituindo

Podemos substituir as ligações - isso pode ser útil para testar:

class InMemoryDao : Dao

val commonModule = Kodein.Module {
    bind() with singleton { MongoDao() }
    bind() with singleton { Service(instance(), "myService") }
}
val testContainer = Kodein {
    import(commonModule)
    bind(overrides = true) with singleton { InMemoryDao() }
}
val dao: Dao = testContainer.instance()

assertThat(dao).isInstanceOf(InMemoryDao::class.java)

6. Multi-Bindings

Podemos configurarmore than one bean with the same common (super-)type no contêiner:

val kodein = Kodein {
    bind() from setBinding()
    bind().inSet() with singleton { MongoDao() }
    bind().inSet() with singleton { JdbcDao() }
}
val daos: Set = kodein.instance()

assertThat(daos.map {it.javaClass as Class<*>})
  .containsOnly(MongoDao::class.java, JdbcDao::class.java)

7. Injetor

Nosso código de aplicativo desconhecia o Kodein em todos os exemplos que usamos antes - ele usava argumentos de construtor regulares que foram fornecidos durante a inicialização do contêiner.

No entanto, a estrutura permitean alternative way to configure dependencies through delegated properties and Injectors:

class Controller2 {
    private val injector = KodeinInjector()
    val service: Service by injector.instance()
    fun injectDependencies(kodein: Kodein) = injector.inject(kodein)
}
val kodein = Kodein {
    bind() with singleton { MongoDao() }
    bind() with singleton { Service(instance(), "myService") }
}
val controller = Controller2()
controller.injectDependencies(kodein)

assertThat(controller.service).isNotNull

Em outras palavras, uma classe de domínio define dependências por meio de um injetor e as recupera de um determinado contêiner. Such an approach is useful in specific environments like Android.

8. Usando Kodein com Android

In Android, the Kodein container is configured in a custom Application class, and later on, it is bound to the Context instance. Todos os componentes (atividades, fragmentos, serviços, receptores de transmissão) são considerados estendidos a partir das classes de utilitários comoKodeinActivityeKodeinFragment:

class MyActivity : Activity(), KodeinInjected {
    override val injector = KodeinInjector()

    val random: Random by instance()

    override fun onCreate(savedInstanceState: Bundle?) {
        inject(appKodein())
    }
}

9. Análise

Nesta seção, veremos como o Kodein se compara a frameworks DI populares.

9.1. Spring Framework

O Spring Framework é muito mais rico em recursos que o Kodein. Por exemplo, Spring tem muitoconvenient component-scanning facility. Quando marcamos nossas classes com anotações específicas como@Component,@Service e@Named,, a varredura de componente seleciona essas classes automaticamente durante a inicialização do contêiner.

O Spring também tempowerful meta-programming extension points,BeanPostProcessoreBeanFactoryPostProcessor, o que pode ser crucial ao adaptar um aplicativo configurado a um ambiente específico.

Finalmente, o Spring fornece algunsconvenient technologies built on top of it, incluindo AOP, Transactions, Test Framework e muitos outros. Se quisermos usá-los, vale a pena ficar com o contêiner Spring IoC.

9.2. Adaga 2

O framework Dagger 2 énot as feature-rich as Spring Framework, but it’s popular in Android development devido a sua velocidade (gera código Java que realiza a injeção e apenas executa em tempo de execução) e tamanho.

Vamos comparar as contagens e tamanhos dos métodos das bibliotecas:

Kodein:image Observe que a dependência dekotlin-stdlib é responsável pela maior parte desses números. Quando o excluímos, obtemos 1282 métodos e tamanho de 244 KB DEX.

 

imagePodemos ver que a estrutura Dagger 2 adiciona muito menos métodos e seu arquivo JAR é menor.

Com relação ao uso - é muito semelhante, pois o código do usuário configura dependências (por meio deInjector no Kodein e anotaçõesJSR-330 no Dagger 2) e posteriormente as injeta por meio de uma única chamada de método.

No entanto, uma característica principal do Dagger 2 é que elevalidates the dependency graph at compile time, portanto, não permitirá que o aplicativo compile se houver um erro de configuração.

10. Conclusão

Agora sabemos como usar o Kodein para injeção de dependência, quais opções de configuração ele fornece e como ele se compara a algumas outras estruturas populares de DI. No entanto, cabe a você decidir se deseja usá-lo em projetos reais.

Como sempre, o código-fonte dos exemplos acima pode ser encontradoover on GitHub.