Open inheritance only where substitution makes sense
Kotlin classes and members are final unless marked open or abstract. An override should honor the base contract: callers using the base type must still receive valid behavior.
open class Formatter {
open fun format(topic: String): String = topic.trim()
}
class UppercaseFormatter : Formatter() {
override fun format(topic: String): String = super.format(topic).uppercase()
}
fun main() {
val formatter: Formatter = UppercaseFormatter()
check(formatter.format(" Kotlin ") == "KOTLIN")
}
Dynamic dispatch selects the subclass implementation. The super call deliberately reuses the base normalization step. Do not expose inheritance solely to reuse a few lines; composition often makes independent policies easier to test and replace.
Avoid requiring clients to know which subclass they received. If one implementation throws for inputs accepted by the base contract, the hierarchy is misleading.
Exercise
Create a formatter that adds a prefix while preserving trimming. Then implement the same behavior by wrapping another formatter.
Check: compare which design permits combining uppercase and prefix policies without adding a subclass for every combination.