evant / kotlin-inject

Dependency injection lib for kotlin
Apache License 2.0
1.14k stars 51 forks source link

Question: multiple interfaces and one implementation #386

Closed sobotkami closed 2 weeks ago

sobotkami commented 1 month ago

Hi all, I am struggling with this design: ONE adapter implements TWO interfaces. How do I define this correctly? Thanks!

interface UserReadPort {
    fun findAll(): Set<UserEntity>
    fun findById(id: UserId): UserEntity?
}

interface UserWritePort {
    fun addUser(user: UserEntity)
    fun remove(id: UserId)
    fun removeAll()
}

@Inject
class UserInMemoryAdapter : UserReadPort, UserWritePort { // ...

@Inject
class CreateUserUseCase(lazyUserWritePort: Lazy<UserWritePort>, lazyUserReadPort: Lazy<UserReadPort>) {
    private val userWritePort by lazyUserWritePort
    private val userReadPort by lazyUserReadPort

@Component
abstract class AppComponent(private val port: Int) {
    abstract val createUserUseCase: CreateUserUseCase

    @Provides
    fun userReadInMemoryAdapter(userReadPort: UserReadPort): UserReadPort = userReadPort

    @Provides
    fun userWriteInMemoryAdapter(userWritePort: UserWritePort): UserWritePort = userWritePort

produces

Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property userWritePort has not been initialized
    at InjectAppComponent$createUserUseCase$1.invoke(InjectAppComponent.kt:15)
    at InjectAppComponent$createUserUseCase$1.invoke(InjectAppComponent.kt:11)
...

Generated code


public fun KClass<AppComponent>.create(port: Int): AppComponent = InjectAppComponent(port)

public class InjectAppComponent(
  port: Int,
) : AppComponent(port) {
  override val createUserUseCase: CreateUserUseCase
    get() = CreateUserUseCase(
      lazyUserWritePort = lazy {
        run<UserWritePort> {
          lateinit var userWritePort: UserWritePort
          userWriteInMemoryAdapter(
            userWritePort = userWritePort
          ).also {
            userWritePort = it
          }
        }

      },
      lazyUserReadPort = lazy {
        run<UserReadPort> {
          lateinit var userReadPort: UserReadPort
          userReadInMemoryAdapter(
            userReadPort = userReadPort
          ).also {
            userReadPort = it
          }
        }

      }
    )
}
sobotkami commented 1 month ago

One possible solution:

instead of

@Provides
fun userReadInMemoryAdapter(userReadPort: UserReadPort): UserReadPort = userReadPort

@Provides
fun userWriteInMemoryAdapter(userWritePort: UserWritePort): UserWritePort = userWritePort

do the following:

protected val UserInMemoryAdapter.bindRead: UserReadPort
    @Provides get() = this

protected val UserInMemoryAdapter.bindWrite: UserWritePort
    @Provides get() = this

But, is there a solution to get rid of these definitions?

evant commented 1 month ago
fun userReadInMemoryAdapter(userReadPort: UserReadPort): UserReadPort = userReadPort

You are requesting a dependency to provide itself. This should actually fail to compile with a cycle error, the fact it generates invalid code instead is a bug

As you note below,

protected val UserInMemoryAdapter.bindRead: UserReadPort
    @Provides get() = this

Is the correct way to do this, providing the impl to the interface. The equivlent using the function syntax you used originally would be

fun userReadInMemoryAdapter(userReadPort: UserInMemoryAdapter): UserReadPort = userReadPort

But, is there a solution to get rid of these definitions?

No, you need a definition in your component for each interface you are providing. There have been some requests for an anvil-like feature to provide them with an annotation on the class itself instead of the component, https://github.com/evant/kotlin-inject/issues/212 if you are curious.

evant commented 1 month ago

Adding the bug label for the invalid code gen. This is likely related to https://github.com/evant/kotlin-inject/issues/313