Kotlin idiomatic interface method cache WITHOUT spring's cache?

Viewed 209

Is there any way to intercept method from interface and cache result “WITHOUT” spring’s method cache ? Take a simple calculator for example :

interface ICalculate {

  fun multiply(a: Int, b: Int): Int
}

It just multiplies a and b , and return the result . Suppose it is a heavy computation work . And there are two implementations :

class CalculatorDumb : ICalculate {
  override fun multiply(a: Int, b: Int): Int {
    var sum = 0
    (1..a).forEach {
      (1..b).forEach {
        sum++
      }
    }
    return sum
  }
}

The dumb implementation just add one by one .

And there is a smart implementation :

class CalculatorSmart : ICalculate {

  override fun multiply(a: Int, b: Int): Int {
    return a * b
  }
}

This smart implementation just returns a * b .

OK , here is the point . I hope client can initialize no matter dumb or smart implementation , and can get result if the parameter is identical.

There is a Memoize pattern , described here : https://jorgecastillo.dev/kotlin-purity-and-function-memoization :

class Memoize<in T, out R>(val f: (T) -> R) : (T) -> R {
  private val values = mutableMapOf<T, R>()
  override fun invoke(x: T): R {
    return values.getOrPut(x) { f(x) }
  }
}

fun <T, R> ((T) -> R).memoize(): (T) -> R = Memoize(this)

I can use it in the implementation class , like this :

class CalculatorSmart : ICalculate {

  data class CacheKey(val a: Int, val b: Int)

  private val innerCalculate: (CacheKey) -> Int = { key: CacheKey ->
    println("cache miss")
    key.a * key.b
  }.memoize()

  override fun multiply(a: Int, b: Int): Int {
    return innerCalculate(CacheKey(a, b))
  }
}

But it seems it’s hard to apply it in the interface layer.

I wonder if there are any patterns to achieve :

  1. Each implementation class ( dumb or smart in this example) doesn’t need to implement its own cache .
  2. There are NO two public versions of method defined in interface ( for example : actual multiply() and cachedMultiply() while cachedMultiply checks cache and redirect to multiply if necessary )
  3. Client only knows one method of the interface , No matter the client initialize smart or dumb class , the result of the same parameter will be cached and returned.

For example : such scenario is OK

val calSmart: ICalculate = CalculatorSmart()
println(calSmart.multiply(3, 7)) // cache miss
println(calSmart.multiply(3, 7)) // cache hit

val calDumb: ICalculate = CalculatorDumb()
println(calDumb.multiply(3, 7)) // cache miss
println(calDumb.multiply(3, 7)) // cache hit

It will work like Spring’s method cache . but I hope there will be a kotlin-idiomatic style , maybe more functional , just like the memoization pattern above . Is there any idea ?

Thanks.

1 Answers

I don't believe there is anything in the language to support that at the interface level, but I see 2 "obvious" alternatives:

  1. an abstract class doing the caching, implementations would just extend it and implement particular protected methods
  2. a decorator class to wrap any implementation to add caching (so implementors don't need to know about caching at all, but clients opt-in by instantiating a wrapper)

Using an abstract class

The interface is about defining the function itself, while caching is an implementation detail. If you want to "share" this implementation detail across implementations, it seems to me the most appropriate would be to have an abstract class implementing this interface and providing an abstract method for the actual computation in subclasses:

interface ICalculate {

  fun multiply(a: Int, b: Int): Int
}

abstract class CachedCalculator {
  override fun multiply(a: Int, b: Int): Int = computeMultiply().memoize()

  protected abstract fun computeMultiply(a: Int, b: Int): Int
}

Now subclasses extending CachedCalculator would benefit from caching:

class CalculatorSmart : CachedCalculator() {

  override fun computeMultiply(a: Int, b: Int): Int {
    return a * b
  }
}

Using a decorator

class CachedCalculator(private val calculator: ICalculate): ICalculate {
  override fun multiply(a: Int, b: Int): Int = calculator.multiply().memoize()
}

Then on client side:

val calSmart: ICalculate = CachedCalculator(CalculatorSmart())
println(calSmart.multiply(3, 7)) // cache miss
println(calSmart.multiply(3, 7)) // cache hit
Related