How to derive type from other property type

I am relatively new to Kotlin and typed languages. Looking for some advice on to best implement my data models in Kiotlin.

Here is my current code:

data class Commitment(
	val type: CommitmentType = CommitmentType,
	val configuration: CommitmentConfiguration,
	etc...
)

sealed interface CommitmentConfiguration {
    val title: String get() = ""
	etc...
}

data class BindingCommitmentConfiguration(
    val strictMode: Boolean = false,
	etc...
) : CommitmentConfiguration


enum class CommitmentType {
    Binding,
    NonBinding,
    Legal
}

Given a Commitment instance, what is the “correct” way to infer the CommitmentConfiguration type, from the CommitmentType?

Currently, I am doing the following:

when(commitment.type){
	Binding -> doSomething(commitment.config as BindingCommitmentConfiguration),
	else {...}
}

This seems inefficient, as I have to explicitly cast the config, even though it’s type is fixed given the CommitmentType.

I have considered using generics,

data class Commitment<T: CommitmentConfiguration>(
    val type: CommitmentType = CommitmentType,
	val configuration: T,
	etc...
)

Would make more sense to use <T: CommitmentType>, but I don’t see how to automatically derive the configuration type based on that.

As well as subtypes,

data class BindingCommitment(
    val type: CommitmentType = CommitmentType,
	val configuration: T,
	etc...
) : Commitment(type, configuration)

This would result in lots of duplication of parameters and really large classes.

I understand this is a subjective question. But I am just trying to get a better understand on how this would typically be done in Kotlin.

As you already use a sealed interface for CommitmentConfiguration, would it be sufficient for you to ignore the type property at all and simply check the type?

when(val configuration = commitment.configuration){
    is BindingCommitmentConfiguration -> doSomething(configuration)
        ...
}

This is a common pattern in languages that support sealed types. They often replace enums entirely.

1 Like

That’s not a bad idea. Though it requires passing the Configuration as a separate argument, as opposed to something like a generic or subclass:

doSomething(commitment<BindingType>) { 
.. can reference both commitment and configuration
}

I’m not sure, what do you mean. In both cases we start with commitment and this is the only thing we need. If you mean I created the configuration variable, then I did this to make it easier to use, but this is not at all required:

when(commitment.configuration){
    is BindingCommitmentConfiguration -> doSomething(commitment.configuration)
        ...
}

This code should work as long as Commitment.configuration is guaranteed to not change (as in your case).

This gets into questions of loose vs. tight coupling, and I think the ‘right’ answer will depend on the bigger picture (and how that might evolve).

A when/switch/case-type selection based on the object type can be a bit of a code smell. Perhaps the most obvious way to refactor it would be move the relevant code into a new method specified by the interface. In this case, that might look like:

sealed interface CommitmentConfiguration {
    val title: String get() = ""
    fun doSomething()
    // etc...
}

Each class implementing the interface must implement that method too. You can then dispense entirely with the when block, and simply call commitment.doSomething(), as each implementation will know what it needs to do.


Whether that’s a better design depends on what doSomething() does, and how it relates to the rest of the code.

If it’s something that’s closely bound up with what the implementing classes represent — if it’s closely coupled to them — then it makes good sense to implement it in those classes. (Other code might want to call it too, and it can make use of the class’s internals.)

However, if it’s NOT closely connected to them (especially if the various implementations are related more to each other than to the classes they operate on, and/or it makes sense in only one one area of the code), then it’s probably better to keep it all together — in which case a when block or similar may be a better choice after all. (Perhaps following broot’s suggestion.)

For example, consider getting a string representation of an object, perhaps to show in a UI. If an object has an inherent text form that would always make sense wherever it appears in the interface, then it would fit naturally as implementing some common method. (Perhaps even the existing toString() method!)

But if different screens need different, custom descriptions of the objects, then it would probably be better for each screen to do the string conversion itself, as only it knows what sort of text it needs — even though it would have to check the object type and do its own thing for each one.

Ultimately, I think this is about knowledge, and where to put it. Does it make more sense for each object to know how to doSomething() on itself? Or for that knowledge to be collected in one place, away from the objects themselves?

1 Like

If the function also requires the commitment itself, then it requires explicitly passing both the commitment, and configuration separately e.g. doSomething(commitment, config). This in contrast to a sub-class or generic. Not the end of the world though.