Factories
This is a pure extension that is compatible with all environments.
Many frameworks allow you to inject two kinds of components:
- Singletons, where a single object instance is injected wherever needed
- Factories (also known as transient), where any component that depends on it gets its own instance of the object.
Factories are very useful for objects like loggers or to get more advanced behavior depending on the requesting component.
By default, components put
in a Tegral DI environment are resolved as singletons. The Factories extension allows you to create factories in Tegral DI, which will create a new object each time the an object is requested.
Usage
You can create factories using the putFactory
method within your environment builder or within a module.
val myModule = tegralDiModule {
putFactory { Foo() }
}
val myEnvironment = tegralDi {
putFactory { Bar() }
}
Factories are injected using the regular scope()
syntax. For example, requesting the Bar
factory will look like this:
class INeedBar(scope: InjectionScope) {
val bar: Bar by scope()
}
Here is a full example using loggers:
class Logger {
fun logInfo(message: String) = println("INFO: $message")
fun logWarn(message: String) = println("WARN: $message")
}
class ServiceA(scope: InjectionScope) {
private val logger: Logger by scope()
fun doSomething() {
logger.logInfo("Doing something in A...")
}
}
class ServiceB(scope: InjectionScope) {
private val logger: Logger by scope()
private val a: ServiceA by scope()
fun doSomething() {
a.doSomething()
logger.logInfo("Doing something in B...")
}
}
val environment = tegralDi {
putFactory { Logger() }
put(::ServiceA)
put(::ServiceB)
}
environment.get<ServiceA>().doSomething()
// INFO: Doing something in A...
// INFO: Doing something in B...
Requestor-dependent object creation
It is possible to create objects based on which component is requesting them. The putFactory
block gives the requesting object as a parameter, which can then use to do anything.
This is useful for giving a logger a name, for example:
class Logger(private val name: String) {
fun logInfo(message: String) = println("($name) INFO: $message")
fun logWarn(message: String) = println("($name) WARN: $message")
}
// ServiceA and ServiceB are the same as before
val environment = tegralDi {
putFactory { requester -> Logger(requester::class.qualifiedName ?: "<anon>") }
put(::ServiceA)
put(::ServiceB)
}
environment.get<ServiceA>().doSomething()
// (org.example.ServiceA) INFO: Doing something in A...
// (org.example.ServiceB) INFO: Doing something in B...
You can go even further with annotations. Here's a fuller example that uses annotations to name loggers, with an additional fallback mechanism:
// The annotation's definition
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class LoggerName(val name: String)
// Reflection logic used to retrieve the name the logger of a class.
private val KClass<*>.loggerName: String
get() = findAnnotation<LoggerName>()?.name ?: qualifiedName ?: "<anon>"
// Our actual Logger object: nothing really special here...
class Logger(private val name: String) {
fun logInfo(message: String) = println("($name) INFO: $message")
fun logWarn(message: String) = println("($name) WARN: $message")
}
// Let's give ServiceA a custom name...
@LoggerName("Custom logger name!")
class ServiceA(scope: InjectionScope) {
private val logger by scope()
fun doSomething() {
logger.logInfo("Doing something in A...")
}
}
// ... but let's also leave ServiceB as is.
class ServiceB(scope: InjectionScope) {
private val logger by scope()
private val a by scope()
fun doSomething() {
a.doSomething()
logger.logInfo("Doing something in B...")
}
}
// Let's now create our environment:
val environment = tegralDi {
putFactory { requester -> Logger(requester::class.loggerName) }
put(::ServiceA)
put(::ServiceB)
}
// And test our logger:
environment.get<ServiceA>().doSomething()
// (Custom logger name!) INFO: Doing something in A...
// (org.example.ServiceB) INFO: Doing something in B...
Factories under the hood
It may help to first read a bit about Tegral DI's internals to better understand this part.
When you use putFactory
, two declarations are created:
- A regular
put
-like declaration for anInjectableFactory
. This object wraps the labmda you pass toputFactory
. - A resolvable declaration mapped against the type of objects created by your factory. The associated resolver is a
FactoryResolver
which will, when resolved, retrieve theInjectableFactory
and create an object from it, then return it.
If you were creating a factory of Foo
objects:
- The first declaration would be for the
InjectableFactory<Foo>
object (identifier withInjectableFactory::class
and atyped<Foo>()
qualifier). - The second declaration would be for the
Foo
object (identifier withFoo::class
and no qualifier).
Thus, when requesting a Foo
object, you will hit the FactoryResolver
, which will automatically grab the InjectableFactory
, create an object from it and return it.
Here's an overview of the process. Note that Component A
and Component B
will each get their own instance of Foo
.