Hello everyone I’m experimenting with a design exploring using composition over inheritance to combine reusable behaviors. These components have some shared and component specific config. Leveraging kotlin traits with interface delegation, delegated properties and default values.
The idea is that we have n-types of concrete Car
types, here: Sedan
, RaceCar
and FancyRaceCar
that are composed from reusable traits, this code is a toy example analog to a system I’m building and wondering if there is an opportunity to further reduce *Base
-class boilerplate.
I’m stuck on constructors lacking the ability to create local “variables” to pass instances to the superclass constructors and its interface delegates, but perhaps I’m missing something as I’m new to kotlin.
also on gist: Kotlin Composition/Trait Experiment · GitHub
abstract class Car(open val engine: Engine, open val entertainment: Entertainment) :
Engine by engine,
Entertainment by entertainment
interface Engine {
fun speed(): Int
}
interface Entertainment {
fun volume(): Int
}
// Global config potentially used by all components/behaviors, imagine config varying by base vs luxury vs sport model
class CarConfig(config: Any) {
val speedLimit: Int by ConfigValue(config, "global.speedLimit")
val soundLimit: Int by ConfigValue(config, "global.soundLimit")
}
// Config lookup mechanism, implementation is irrelevant, contains global and component specific config properties
class ConfigValue<T, R>(config: Any, key: String) : ReadOnlyProperty<T, R> {
override fun getValue(thisRef: T, property: KProperty<*>): R = TODO()
}
class DefaultEngine(config: Any, private val baseConfig: CarConfig) : Engine {
private val speed: Int by ConfigValue(config, "engine.speed") // component specific config
override fun speed(): Int = min(speed, baseConfig.speedLimit)
}
class RaceEngine(config: Any, private val baseConfig: CarConfig) : Engine {
private val speed: Int by ConfigValue(config, "engine.speed") // component specific config
override fun speed(): Int = min(2 * speed, baseConfig.speedLimit)
}
class DefaultEntertainment(config: Any, private val baseConfig: CarConfig) : Entertainment {
private val volume: Int by ConfigValue(config, "entertainment.volume")
override fun volume(): Int = min(volume, baseConfig.soundLimit)
}
// We don't want to do this obviously
class SedanWithDuplication(config: Any) :
Car(DefaultEngine(config, CarConfig(config)), DefaultEntertainment(config, CarConfig(config)))
// We can do this, leveraging inheritance and default values to simulate having "variables" in our constructor
class Sedan(config: Any) : SedanBase(config, CarConfig(config))
abstract class SedanBase(
config: Any,
baseConfig: CarConfig,
engine: Engine = DefaultEngine(config, baseConfig),
entertainment: Entertainment = DefaultEntertainment(config, baseConfig)
) : Car(engine, entertainment)
// But this gets more annoying as we'd have to create an extra Base-type for every combination...
class RaceCar(config: Any) : RaceCarBase(config, CarConfig(config))
abstract class RaceCarBase(
config: Any,
baseConfig: CarConfig,
engine: Engine = RaceEngine(config, baseConfig),
entertainment: Entertainment = DefaultEntertainment(config, baseConfig)
) : Car(engine, entertainment)
// And it gets worse with a level of indirection for every layer of dependency, eg.
// adding a dependency on Engine from Entertainment:
class SportEntertainment(config: Any, baseConfig: CarConfig, private val engine: Engine) :
Entertainment by DefaultEntertainment(config, baseConfig) {
fun displaySpeed() {
println(engine.speed())
}
}
class FancyRaceCar(config: Any) : FancyRaceCarBase(config, CarConfig(config)) {
init {
entertainment.displaySpeed()
}
}
abstract class FancyRaceCarBase(config: Any, baseConfig: CarConfig) :
FancyRaceCarBaseBase(config, baseConfig, RaceEngine(config, baseConfig))
abstract class FancyRaceCarBaseBase(
config: Any,
baseConfig: CarConfig,
override val engine: RaceEngine,
override val entertainment: SportEntertainment = SportEntertainment(config, baseConfig, engine)
) : Car(engine, entertainment)