Johann
Johann

Reputation: 29877

Pass different generic types to function

I have a function in Kotlin that can take a generic object as a parameter. The two objects are unrelated and do not share any base types. They both however implement the same functions. I would like to re-use those functions within my function. Something along these lines:

fun storeUser(datastore: Any) {
   datastore.storeName("John")
}

// Call the function
val datastore1 = DataStore1()
storeUser(datastore1)

val datastore2 = DataStore2()
storeUser(datastore2)

Both the DataStore1 and DataStore2 have a function called "storeName". Is there a way in Kotlin to re-use this function in the storeUser function? I tried playing around with Generics but this does not seem possible.

The example code above is simple. In my real app, there are many more functions beside storeName. If I can't have a common function to store my data, I will need to create two separate functions and duplicate the storage for both. That kind of sucks.

Upvotes: 0

Views: 358

Answers (2)

Adam Millerchip
Adam Millerchip

Reputation: 23137

As I said in the comment to the question, it would really be better to write a common interface for these classes. If that's not possible because the classes come from an external dependency, the second best thing to do would be to wrap the code as Héctor did.

Kotlin is a statically typed language, so unfortunately wrapping code like this results in a lot of duplication. If you didn't want to write a new wrapper for every new instance of the DataStore, you could use reflection to call it dynamically. This way you only have to write the definition once. However, you forego all the compile-time benefits of static checks, so it's not really a good idea. It was good to do as an exercise though. 😎

class WrappedDataStore<T : Any>(private val dataStore: T) {
    private fun callDynamically(methodName: String, vararg args: Any?) {
        val argTypes = args.map { it?.let { it::class.java} }.toTypedArray()
        dataStore.javaClass
            .getMethod(methodName, *argTypes)
            .invoke(dataStore, *args)
    }

    fun storeName(name: String) = callDynamically("storeName", name)
}

fun <T : Any> storeUser(dataStore: WrappedDataStore<T>) =
    dataStore.storeName("John")

fun main() {
    val one = WrappedDataStore(DataStore1())
    val two = WrappedDataStore(DataStore2())

    one.storeName("foo")
    two.storeName("bar")

    storeUser(one)
    storeUser(two)
}

class DataStore1 {
    fun storeName(foo: String) = println("DataStore1 $foo")
}

class DataStore2 {
    fun storeName(bar: String) = println("DataStore2 $bar")
}

Output:

DataStore1 foo
DataStore2 bar
DataStore1 John
DataStore2 John

Upvotes: 0

H&#233;ctor Valls
H&#233;ctor Valls

Reputation: 26084

I recommend using a common interface for both classes. If they are provided by a thid-party library, you could wrap them in your own classes and interface.

If you don't want to do that, you could just check the type of the parameter in the storeUser function:

 fun storeUser(datastore: Any) {
   when(datastore) {
     is DataStore1 -> datastore.storeName("John")
     is DataStore2 -> datastore.storeName("John")
     else -> throw IllegalArgumentException()
   }
 }

But note that if you have another datastore in the future, you will need to add one more is clause to this function. That makes this code not very maintainable...

Better solution

If you create an interface Datastore:

interface Datastore {
  fun storeName(name: String)
} 

and the make your datastores implement it:

class Datastore1 : Datastore {
  //Datastore1.storeName implementation
}

class Datastore2 : Datastore {
  //Datastore2.storeName implementation
}

Then, you don't need to check the types in storeUser function. Just change its parameter type to Datastore:

fun storeUser(datastore: Datastore) {
  datastore.storeName("John")
}

If Datastore1 and Datastore2 are provided by a third-party library, you can wrap them in your own classes and implement your Datastore interface:

class FirstDatastore : Datastore {

  private val datastore = DataStore1()

  override fun storeName(name: String) {
    datastore.storeName(name)
  }

}

class SecondDatastore : Datastore {

  private val datastore = DataStore2()

  override fun storeName(name: String) {
    datastore.storeName(name)
  }

}

So you can call your function using your classes:

val datastore1 = FirstDatastore()
storeUser(datastore1)

val datastore2 = SecondDatastore()
storeUser(datastore2)

Upvotes: 3

Related Questions