How to override method with inherited class in Kotlin?

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.

>Solution :

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.

Leave a Reply