Kotlin chain of responsibility pattern with generics

1.5k Views Asked by At

Using the chain of responsibility pattern I ran into a problem where next chain element was expected to have the same generic type of the first element. I know why this happens: The first handler expects the second handler to use the generic type "Apple". I just don't know exactly how to solve it.

There is an answer on how to handle it in java here, but since java doesn't have reified types and all that the approach should look different in Kotlin, right?

There are different options that come to my mind:

  1. Don't use generics - would lead to casting the collection types to specific subtypes and wouldn't look clean
  2. Try to use reified types (how?)

To illustrate the problem I am posting a demo code below.


data class Apple(val name:String, val color:Int) 
data class Orange(val circumference:Double)

object Main{
    @JvmStatic
    fun main(args: Array<String>) {
        val first = FirstHandler()
        val second = SecondHandler()
        first.setNextHandler(second)  // !!! wrong type here since <Apple> is expected
        first.process()
    } 
}

abstract class ChainHandler<T>{
    protected var nextHandlerInChain:ChainHandler<T>? = null
    fun setNextHandler(handler: ChainHandler<T>): ChainHandler<T> {
        this.nextHandlerInChain = handler
        return handler
    }

    abstract fun peel(): Collection<T>
    abstract fun process():MutableMap<String,Any> }

class FirstHandler : ChainHandler<Apple>() {
    override fun peel(): Collection<Apple> {
        return Collections.emptyList<Apple>()
    }
    override fun process(): MutableMap<String, Any> {
        val peeledApples = peel()
        val map = nextHandlerInChain?.process()
        map?.put("apples",peeledApples) ?:kotlin.run {
            val map = mutableMapOf<String,Any>()
            map.put("apples",peeledApples)
        }
        return map!!
    } }

class SecondHandler : ChainHandler<Orange>() {
    override fun peel(): Collection<Orange> {
        return Collections.emptyList<Orange>()
    }
    override fun process(): MutableMap<String, Any> {
        val peeledOranges = peel()
        val map = nextHandlerInChain?.process()
        map?.put("oranges",peeledOranges) ?:kotlin.run {
            val map = mutableMapOf<String,Any>()
            map.put("oranges",peeledOranges)
        }
        return map!!
    } 
}
2

There are 2 best solutions below

0
On BEST ANSWER

Kotlin has something called a star projection that might help you here. It basically tells the compiler that you don't really care what type of ChainHandler you get. You can use it to make your setNextHandler compile, like this:

abstract class ChainHandler<T>{
  // Note the star projection here
  protected var nextHandlerInChain: ChainHandler<*>? = null

  // Defining a type parameter S, so that the return type is equal to the input type.
  fun <S> setNextHandler(handler: ChainHandler<S>): ChainHandler<S> {
    this.nextHandlerInChain = handler
    return handler
  }

  ...
}

You can read more about star projections here: https://kotlinlang.org/docs/reference/generics.html#star-projections

As for reifying the type parameter: Reified type parameters work only for inline functions. Not for type parameters on a class.

0
On

This very much depends on specifics of how you want handlers to interact. For this code, just changing the type of next handler (in both var nextHandlerInChain and fun setNextHandler to ChainHandler<*> would work, since process() returns something independent of T anyway.