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: 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.
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.