Search code examples
kotlingenericsdependency-injectionkotlin-reified-type-parameters

How to deal with library inline generic functions with reified and allow dependency injection?


I have a some library method, which I can't really change.

class Lib {
    inline fun <reified T> getSomething(): T {
        // ...
    }
}

It uses reified generic parameter to pass information about the generic return type. It sadly does not have a version with KClass<T> instead of reified.

I would like to make a class that wraps this library's funcionality to simplify the access to it and allows dependency injection so I can mock it and test classes that depend on it. I also have interface Something that restricts the generic parameter T in my app.

sealed interface Something {
    // ...
}

interface Wrapper {
    fun <T : Something> getSomething(): T
}

class LibWrapper : Wrapper {
    private val library = Lib()

    fun <T : Something> getSomething(): T {
        // ... it does some other stuff around as well
        val something = library.getSomething<T>()
        // ...
    }
}

This is obviously not working because the non-reified generics are only relevant at compile time. I tried several approaches unsuccessfuly:

  1. Propagating inline up into my code

This is not possible because virtual members cannot be inline and also you cannot override non-inline with inline, so no dependency injection.

internal interface Wrapper {
    inline fun <reified T : Something> getSomething(): T
}

internal class LibWrapper : Wrapper {
    private val library = Lib()

    override inline fun <reified T : Something> getSomething(): T {
        // ...
        val something = library.getSomething<T>()
        // ...
    }
}
  1. Using KClass<T>

This also does not work because T is still available only at compile time (I guess).

interface Wrapper {
    fun <T : Something> getSomething(type: KClass<out T>): T
}

class LibWrapper : Wrapper {
    private val library = Lib()

    override fun <T : Something> getSomething(type: KClass<out T>): T {
        // ...
        val something = library.getSomething<T>()
        // ...
    }
}

So what should I do? I know this is little bit of a duplicate of these to questions #1 #2, but I would like to expand on them by asking about solution for this general problem. I mean, I am sure that author of the library had his reasons for using reified and the answer "it is not possible" doesn't seem good enough, should I not do the dependecny injection then or not use generics at all here?

I have a few ideas:

Let's say I have two classes that implement Something

class A : Something {
    // ...
}

class B : Something {
    // ...
}
  1. Do a method for each type implementing Something interface

I could wrap A and B each into a method of its own.

interface Wrapper {
    fun getA(): A
    fun getB(): B
}

class LibWrapper : Wrapper {
    private val library = Lib()

    override fun getA(): A {
        // ...
        val something = library.getSomething<A>()
        // ...
    }

    override fun getB(): B {
        // ...
        val something = library.getSomething<B>()
        // ...
    }
}

This is kinda tedious if the number of implementers of Something begins to grow and if the Wrapper methods share common code (u could extract that into another private method tho). I have about 8 methods like this provided by Lib, so even with having lets say 5 implementers of Something it pretty quickly grows to about 40 methods, which seems pretty excessive considering that all of them do pretty much the same thing. Also this kinda seems to defeat the whole purpose of generics if I have to implement each method separately.

  1. Map KClass<T> to getSomething<T>() by hand

I can use when to make my own version of getSomething extension method on Lib that uses KClass<T> and calls corresponding getSomething method with concrete type. Then I can use the 2. approach above.

@Suppress("UNCHECKED_CAST")
fun <T : Something> Lib.getSomething(type: KClass<out T>): T {
    return when(type) {
        A::class -> getSomething<A>() as T
        B::class -> getSomething<B>() as T
        else -> throw NotImplementedError()
    }
}

This shares similar issues as the approach above. It is little bit less code, but is bit more confusing since you have to rememeber to put a new case for each implementer inside this extension. Also it for some reason requires the else case even though the interface is sealed so it should be impossible to execute.

  1. Some entirely different approach/architecture?

Should go about it in an entirely different way? My only constraints are that I would like dependency injection around the library and I (obviously) can't change the library methods. implementers of Something are @Serializable data class if it somehow helps.

What approach should I go for?


Solution

  • As was mentioned in the comments, there is really no reason for library not to have the KClass overload

    In my case it was just hidden behind SerializationStrategy type, so I missed it. I had to pass type.serializer() instead of just type.