Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Component Map injection and improved documentation #1

Merged
merged 3 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 36 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

A very simple library for dependency injection in Kotlin, using modern Kotlin features. Simple DI is:

- Fast and efficient: it only does what it's supposed to do, without hidden control flows or tricks for supporting
- **Fast and efficient**: it only does what it's supposed to do, without hidden control flows or tricks for supporting
features you won't ever use
- Elegant: inject what you need using Kotlin's delegated properties or constructors, without using ugly annotations
- **Elegant**: inject what you need using Kotlin's delegated properties or constructors, without using ugly annotations
everywhere
- Usable: it supports circular dependencies and multiple scopes of dependency injection (see below)
- **Usable**: it supports circular dependencies and multiple scopes of dependency injection (see below)
- **Easy to integrate**: simple DI comes with no extra dependency, which makes it extremely easy to integrate into any
project, new or existing

## Installation

Expand All @@ -18,13 +20,13 @@ Simple DI is available on Maven Central. Add the dependency to your project:
**Gradle Kotlin**

```kt
implementation("dev.zodiia:simple-di:<latest version>")
implementation("dev.zodiia:simple-di:1.1.0")
```

**Gradle Groovy**

```groovy
implementation 'dev.zodiia:simple-di:<latest version>'
implementation 'dev.zodiia:simple-di:1.1.0'
```

**Maven**
Expand All @@ -33,7 +35,7 @@ implementation 'dev.zodiia:simple-di:<latest version>'
<dependency>
<groupId>dev.zodiia</groupId>
<artifactId>simple-di</artifactId>
<version>latest version</version>
<version>1.1.0</version>
</dependency>
```

Expand Down Expand Up @@ -144,6 +146,13 @@ If you do not provide any scope, the `RUNTIME` scope is used.
When injecting constructor parameters, the same scope as the one used to request the instance currently being
constructed will be used.

Here is a diagram explaining the different scope levels (excluding REQUEST, as its instance is never actually stored):

![injection patterns diagram](./docs/injection-pattern.svg)

In this example, `ThreadObject` could actually use the THREAD scope as well to inject `InstanceObject`, although in
other cases the result will be different.

#### Runtime (`InjectionScope.RUNTIME`)

It will only ever inject one and only one instance of the requested type, for all classes requesting it.
Expand Down Expand Up @@ -246,6 +255,27 @@ fun main() {
}
```

### Multiple `ComponentMap`s

`ComponentMap`s are the main entry points for each injection request which scope is either RUNTIME or THREAD. It also
stores all component instances (hence its name) in the different scopes. An advanced use case could require the use of
different `ComponentMap`s in different context. This can be achieved pretty easily.

When injecting through class constructors, the same `ComponentMap` will be used for injecting the constructor
parameters, you do not need to do any additional work here.

When injecting using the `injection` delegated property, the method takes as second parameter a `ComponentMap`, which,
when specified, will be used for the injection instead of the global one. You can also retrieve the current
`ComponentMap` in which the current instance is by adding it to your constructor. For example:

```kt
class Foo(componentMap: ComponentMap) {
val bar by injection(componentMap = componentMap)
}
```

In this example, `bar` will be injected using the same `ComponentMap` that was used to inject `Foo` in the first place.

## Getting help

If you need help, feel free to open a discussion in the discussion tab of the repository.
Expand Down
4 changes: 2 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import io.gitlab.arturbosch.detekt.Detekt
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
kotlin("jvm") version "1.9.0"
kotlin("jvm") version "1.9.22"
id("io.gitlab.arturbosch.detekt") version "1.22.0"
`maven-publish`
signing
}

group = "dev.zodiia"
version = "1.0.1"
version = "1.1.0"
description = "Simple dependency injection library for Kotlin (JVM)"

repositories {
Expand Down
50 changes: 50 additions & 0 deletions docs/injection-pattern.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
@startuml

hide empty members

class Application {
val globalObject by injection(scope = InjectionScope.RUNTIME)
}
class GlobalObject {
val threadObject by injection(scope = InjectionScope.THREAD)
}

Application *-- GlobalObject

package "Thread 0" {
class "ThreadObject" as tm0 {
val instanceObject by injection(scope = InjectionScope.INSTANCE)
}
class "InstanceObject" as im0
GlobalObject *-- tm0
tm0 *-- im0
}

package "Thread 1" {
class "ThreadObject" as tm1 {
(...)
}
class "InstanceObject" as im1
GlobalObject *-- tm1
tm1 *-- im1
}

package "Thread 2" {
class "ThreadObject" as tm2 {
(...)
}
class "InstanceObject" as im2
GlobalObject *-- tm2
tm2 *-- im2
}

package "Thread 3" {
class "ThreadObject" as tm3 {
(...)
}
class "InstanceObject" as im3
GlobalObject *-- tm3
tm3 *-- im3
}

@enduml
1 change: 1 addition & 0 deletions docs/injection-pattern.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 26 additions & 2 deletions src/main/kotlin/dev/zodiia/simpledi/ComponentMap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,29 @@ import kotlin.reflect.KParameter
import kotlin.reflect.KType
import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.typeOf

/**
* A class responsible for holding all injectable instances of classes in the
* application runtime.
*
* Usually, you only want one component map in the application, and you do not
* have to manage it. However, you can create multiple component maps and pass
* them to the [injection] delegate. Use this feature if you know what you're
* doing.
* them to the [injection] delegate. Injection through class constructors will
* also use the component map that contains the parent object. Use this feature
* if you know what you're doing.
*
* When trying to use multiple component maps with the [injection] delegate, add
* a [ComponentMap] to your constructor to pass the currently used component map
* through, making it available for further [injection]s.
*/
class ComponentMap {
private val components = HashSet<InjectableInstance<*>>()

init {
addInstance(this, componentMapType, InjectionScope.RUNTIME)
}

/**
* Add an instance to the component map for future injections.
*
Expand All @@ -33,10 +43,19 @@ class ComponentMap {
makeInjectable(type, instance, scope, pid)
}

private fun prepareComponentMapForPid(pid: Long) {
if (!hasInstanceFor(componentMapType, InjectionScope.THREAD, pid)) {
addInstance(this, componentMapType, InjectionScope.THREAD, pid)
}
}

/**
* Request an instance for a given scope and parameters.
*/
fun <T : Any> requestInstance(type: KType, thisRef: Any?, scope: InjectionScope, pid: Long? = null): T {
if (pid != null) {
prepareComponentMapForPid(pid)
}
if (scope.global) {
components.find {
try {
Expand Down Expand Up @@ -104,6 +123,11 @@ class ComponentMap {
}

companion object {
private val componentMapType = typeOf<ComponentMap>()

/**
* The default [ComponentMap].
*/
val default = ComponentMap()
}
}
Expand Down
38 changes: 36 additions & 2 deletions src/test/kotlin/dev/zodiia/simpledi/ComponentMapTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ class ComponentMapTest {
class BarWithOptionalConstructor(val foo: IFoo = Foo())
class BarWithRequiredConstructor(val foo: IFoo)

class BarWithInjectedComponentMap(val componentMap: ComponentMap) {
val foo by injection<Foo>(InjectionScope.RUNTIME, componentMap)
val threadFoo by injection<Foo>(InjectionScope.THREAD, componentMap)
val threadComponentMap by injection<ComponentMap>(InjectionScope.THREAD, componentMap)
}

@Test
fun `Test request scope`() {
val componentMap = ComponentMap()
Expand Down Expand Up @@ -52,7 +58,7 @@ class ComponentMapTest {
val componentMap = ComponentMap()
val bar1 = Bar(InjectionScope.THREAD, componentMap)
val bar2 = Bar(InjectionScope.THREAD, componentMap)
var rand: String = bar1.foo.rand
var rand = bar1.foo.rand

val th1 = Thread {
bar1.foo.num = 1
Expand Down Expand Up @@ -150,8 +156,10 @@ class ComponentMapTest {

@Test
fun `Test injecting invalid types`() {
val componentMap = ComponentMap()

Assertions.assertThrows(IllegalStateException::class.java) {
val foo by injection<IFoo>()
val foo by injection<IFoo>(componentMap = componentMap)

foo.num = 1
}
Expand Down Expand Up @@ -186,4 +194,30 @@ class ComponentMapTest {
bar.foo.num
}
}

@Test
fun `Test component map injection`() {
val componentMap = ComponentMap()
val bar by injection<BarWithInjectedComponentMap>(componentMap = componentMap)

Assertions.assertEquals(0, bar.foo.num)
Assertions.assertTrue(componentMap.hasInstanceFor(typeOf<ComponentMap>(), InjectionScope.RUNTIME))
Assertions.assertEquals(componentMap, bar.componentMap)
}

@Test
fun `Test component map injection in threads`() {
val componentMap = ComponentMap()
val bar by injection<BarWithInjectedComponentMap>(componentMap = componentMap)

val thread = Thread {
bar.threadFoo.num = 1
}
thread.start()
thread.join()

Assertions.assertEquals(0, bar.threadFoo.num)
Assertions.assertTrue(componentMap.hasInstanceFor(typeOf<ComponentMap>(), InjectionScope.THREAD, thread.id))
Assertions.assertEquals(componentMap, bar.threadComponentMap)
}
}