Home > Software design >  Class constructor with generic class
Class constructor with generic class

Time:09-27

In my application I have generic classes, and I have to construct new objects according to these generic classes. Is it possible to get a generic constructor which handle sub classes ?

I'll be more understandable with code :

I have for exemple a parent classe with two children

open class Fruit (
   val name: String
)

class Apple (
   name: String,
   val count: Int
) : Fruit(name) {

   constructor(name: String) : this(name, 0)

}

class Strawberry (
   name: String,
   val weight: Float
) : Fruit(name) {

   constructor(name: String) : this(name, 0f)

}

As you can see, my 3 classes have the same constructor (with just a name). Now I have a class, which contains a generic class of Fruit. In this class, I want to have method to create new Fruit, without care about if it's a Fruit, an Apple or a Strawberry :

class Farmer<GenericFruit: Fruit>(val name: String, val age: Int) {

   var cart = arrayListOf<GenericFruit>()

   fun cultivate() {

      for (_ in 1..10) {
         val fruit = cultivateFruit()
         cart.add(fruit)
      }

   }

   fun cultivateFruit() : GenericFruit {

      // Here call constructor of GenericFruit which just the parameters "name"
      val fruit = GenericFruit(name = "name of the fruit")

      return fruit
   }

}

I've tried to use reified in the method cultivateFruit like this :

inline fun <reified T: GenericFruit> cultivateFruit() : GenericFruit? {

   val constructor = T::class.constructors.find { 
      it.parameters.size == 1 && it.parameters.first().name == "name"
   }
   
   return constructor?.call("name of the fruit")
}

But the issue is that now I need to now the class in the method cultivate and Kotlin won't let me do that :

fun cultivate() {

   for (_ in 1..10) {
      val fruit = cultivateFruit<GenericFruit>() // Here there is an error, GenericFruit can't be called
      cart.add(fruit)
   }

}

The issue is that I have no reference to the generic constructor. I even not have in the cultivateFruit access to the class T. I know it is possible to achieve that on swift, I'm wondering if is it possible too with Kotlin ?

CodePudding user response:

The most honest would be to have a static Map of String name to the Class<? extends Fruit>. The name is also a class level constant, and thus should not be a constructor parameter. The constructor should be the default constructor of every child class.

public class Fruit ...
    public String getName() {
        return fruitClasses.get(getClass());
    }

    private static<String, Class<? extends Fruit>> fruitClasses = new HashMap<>();

    protected static register(String name, Class<? extends Fruit> fruitClass) {
        Objects.requireNull(fruitClasses.put(name, fruitClass));
    }

    public static Fruit createFruit(String name) {
        Class<? extends Fruit> type = fruitClasses.get(name);
        if (type == null) {
            throw new IllegalArgumentException(
                    "No fruit class registered for "   name);
        }
        return type.getConstructor().newInstance();
    }
}

class Apple extends Fruit {
    static { Fruit.register("Apple", Apple.class); }

Here every Fruit child class takes care of its own registration.

It could also be done traditionally with a FruitFactory, which seems a bit simpler for everybody. The disadvantage being that the FruitFactory either would need to import every child class, or there is no difference with the Fruit above.

With the new sealed classes in java, you define a closed domain of child classes, listing them all. Hence you could register all classes in Fruit itself.

Last but not least one may wonder whether inheritance, multiple children, is such a good idea. One can also think of capabilities, features dynamically looked up:

public interface JuicePressable {
    float press();
}

public interface HasSeeds {
    int seeds();
    void removeSeeds();
}

Optional<JuicePressable> jp = fruit.as(JuicePressable.class);
jp.ifPresent(fr -> ... fr.press() ...);

fruit.as(HasSeeds.class).ifPresent(fr -> fr.removeSeeds());

The rationale here, that if you have a categorical treatment, and would need filtering for specific actions, and the number of operational categories is open: then create a lookup of capabilities.

CodePudding user response:

Kotlin answer.

Since class types can't be reified, your Farmer would need to have a property hold a reference to class type to be able to use reflection to create the fruit. You could add a companion object reified function pseudo-constructor to avoid having to explicitly pass the class type.

class Farmer<T: Fruit>(private val fruitClass: KClass<T>, val name: String, val age: Int) {

    companion object {
        inline fun <reified T: Fruit> Farmer(name: String, age: Int) = Farmer(T::class, name, age)
    }

    private val fruitConstructor = fruitClass.constructors.find {
        it.parameters.size == 1 && it.parameters.first().name == "name"
    } ?: error("Fruit class must have a constructor with a single argument of \"name\"")

    var cart = arrayListOf<T>()

    fun cultivate() {
        repeat(10) {
            val fruit = cultivateFruit()
            cart.add(fruit)
        }
    }

    private fun cultivateFruit(): T {
        return fruitConstructor.call("name of the fruit")
    }

}

Personally, I wouldn't want to rely on reflection for this. You could add a constructor parameter for the fruit's constructor. It's a little less convenient at the call site, but you would have compile-time checking so the code would be more robust.

class Farmer<T: Fruit>(private val fruitCreator: (String)->T, val name: String, val age: Int) {
    var cart = arrayListOf<T>()

    fun cultivate() {
        repeat(10) {
            val fruit = cultivateFruit()
            cart.add(fruit)
        }
    }

    private fun cultivateFruit(): T {
        return fruitCreator("name of the fruit")
    }

}

When constructing a Farmer, you can pass the constructor as the argument:

val strawberryFarmer = Farmer(::Strawberry, "Foo", 40)
  • Related