1

I am exploring and actively using generics in production with Kotlin.

Kotlin + generics is a big puzzle for me, so maybe you can explain and help me understand how it works here, compared to Java.

I have class AbstracApiClient (not really abstract)

class AbstracApiClient {
  
  open protected fun makeRequest(requestBuilder: AbstractRequestBuilder) {
    // ... 
  }
}

AbstractRequestBuilder (not really abstract):

open class AbstractRequestBuilder {
  ...
}

ConcreteApiClient that inherits AbstractApiClient that should override makeRequest with ConcreteRequestBuilder inherited from AbstractRequestBuilder:

class ConcreteApiClient: AbstractApiClient() {
  
  protected override fun makeRequest(requestBuilder: ConcreteRequestBuilder) {
    // ... 
  }
}

class ConcreteRequestBuilder: AbstractRequestBuilder()

As I would have more concrete API clients. I would like to make an abstraction that I can pass inherited concrete requests builders and override `make requests method.

  1. I tried using it as it is but wouldn't work
  2. I tried this notation protected open fun <R: ApiRequestBuilder> make request(request builder: R) but it won't match overriding function which I want it to be: protected override fun make request(request builder: ConcreteRequestBuilder)

What other options do I have? Am I missing something here?

Note: I cannot use interface or abstract classes in this scenario, so ideally I would like to find a way with inheritance and functions overriding.

Dmytro Chasovskyi
  • 3,209
  • 4
  • 40
  • 82
  • Could you please explain a bit more what doesn't work with the current approach without generics? What error do you get / with which code? – Joffrey Dec 07 '21 at 09:40
  • The overriding method doesn't work in both scenarios, so method `protected override fun makeRequest(requestBuilder: ConcreteRequestBuilder)` doesn't get matched by any of the 2 specified methods I used. – Dmytro Chasovskyi Dec 07 '21 at 09:40
  • Ah yes, I see. I will try to explain in an answer – Joffrey Dec 07 '21 at 09:41

1 Answers1

2

You can't override a method with more specific argument types, because it breaks Liskov's substitution principle:

val client: AbstractApiClient = ConcreteApiClient()

client.makeRequest(AbstractRequestBuilder())

As you can see above, the ConreteApiClient implementation has to be able to handle all possible inputs of the parent class, because it could be accessed through the parent class's API.

To do what you want, you need to restrict the parent class itself via generics:

open class AbstractApiClient<R : AbstractRequestBuilder> {
  
  open protected fun makeRequest(requestBuilder: R) {
    // ... 
  }
}

class ConcreteApiClient: AbstractApiClient<ConcreteRequestBuilder>() {
  
  protected override fun makeRequest(requestBuilder: ConcreteRequestBuilder) {
    // ... 
  }
}

This way, any instance of AbstractApiClient<R> has to show which type of request builder it accepts (in the type argument). It prevents the above issue because now the parent type also carries information:

// doesn't compile
val client: AbstractApiClient<AbstractRequestBuilder> = ConcreteApiClient() 

// this compiles
val client: AbstractApiClient<ConcreteRequestBuilder> = ConcreteApiClient()

I tried this notation protected open fun <R: ApiRequestBuilder> make request(request builder: R)

Now regarding this attempt, it doesn't work because if you make the method generic (not the class) it means every implementation of the method has to handle all kinds of R (NOT one R per implementation). Putting the generic on the class allows to specify the generic argument once per instance of the class.

Joffrey
  • 32,348
  • 6
  • 68
  • 100
  • Thx, I tried something similar but I think stopped halfway. Am I correct that it is allowed in Java, so Java is allowed to break Liskov's principle? – Dmytro Chasovskyi Dec 07 '21 at 09:48
  • 2
    @DmytroChasovskyi Java doesn't allow to override methods with more concrete parameter types either. If you try to annotate the method with `@Override` it should not compile. – Joffrey Dec 07 '21 at 09:52
  • 2
    However Java does break a few things along those lines when it comes to generics because it allows [raw types](https://stackoverflow.com/a/2770692/1540818). Java's generics are broken because of this – Joffrey Dec 07 '21 at 10:00