Prefer Composition over Inheritance
Inheritance is a powerful feature, but it is designed to create a hierarchy of objects with the is-a relationship.
Inheritance is a powerful feature, but it is designed to create a hierarchy of objects with the is-a relationship. When such a relationship is not clear, inheritance might be problematic and dangerous. When all we need is a simple code extraction or reuse, inheritance should be used with caution, and we should instead prefer a lighter alternative: class composition.
Problem in Inheritance
We can only extend one class: Extracting functionalities using inheritance often leads either to excessively complex hierarchies of types, or to huge BaseXXX classes, that accumulate many functionalities.
When we extend, we take everything from a class, which leads to classes that have functionalities and methods they don’t need (a violation of the Interface Segregation Principle).
Using superclass functionality is much less explicit. In general, it is a bad sign when a developer reads a method and needs to jump into superclasses many times to understand how the method works.
Those are strong reasons that should make us think about an alternative, and a very good one is composition.
Inheritance is not always the best approach to achieve code reuse and other benefits of object-oriented programming. Kotlin offers other mechanisms that can be used instead of inheritance, such as:
Composition: Composition is the technique of creating a new class by combining existing classes. This is achieved by creating an instance of the existing class within the new class and delegating the required functionality to the instance. The composition can be used to achieve code reuse without creating complex inheritance hierarchies.
class Room(val furniture: Furniture) {
fun describe() {
println("This room has a ${furniture.description}.")
}
}
class Furniture(val description: String)
In this example, the Room
class is composed of a single Furniture
object, which is passed in the constructor. The describe()
method of the Room
class then delegates to the Furniture
object to provide a description of the room.
Interfaces: Interfaces are a contract that specifies a set of methods that a class must implement. By using interfaces, you can define a set of behaviors that a class must provide, without specifying how those behaviors are implemented. This allows you to achieve polymorphism without creating complex inheritance hierarchies.
interface Vehicle {
fun start()
}
class Car : Vehicle {
override fun start() {
println("Starting the car.")
}
}
class Bike : Vehicle {
override fun start() {
println("Starting the bike.")
}
}
In this example, the Vehicle
interface defines a single start()
method. The Car
and Bike
classes then implement this interface and provide their own implementation of the start()
method.
Delegation: Delegation is a technique of delegating a task to another object. This can be used to separate concerns and achieve code reuse. In Kotlin, you can use the “by” keyword to delegate the implementation of an interface to another class.
interface Pet {
fun makeSound()
}
class Dog : Pet {
override fun makeSound() {
println("Woof!")
}
}
class LoudDog(private val dog: Dog) : Pet by dog {
override fun makeSound() {
dog.makeSound()
println("Woof woof!")
}
}
In this example, the Pet
interface defines a single makeSound()
method. The Dog
class implements this interface and provides its own implementation of the makeSound()
method. The LoudDog
class then delegates to a Dog
object using the by
keyword. It overrides the makeSound()
method to first delegate to the Dog
object, and then add an additional "woof" to make the dog sound louder.
Summary
There are a few important differences between composition and inheritance:
Composition is more secure — We do not depend on how a class is implemented, but only on its externally observable behavior.
Composition is more flexible — We can only extend a single class, while we can compose many. When we inherit, we take everything, while when we compose, we can choose what we need. When we change the behavior of a superclass, we change the behavior of all subclasses. It is hard to change the behavior of only some subclasses. When a class we composed changes, it will only change our behavior if it changed its contract to the outside world.
Composition is more explicit — Thisisbothanadvantageand a disadvantage. When we use a method from a superclass, we can do that implicitly, like methods from the same class. It requires less work, but it can be confusing and is more dangerous, as it is easy to confuse where a method comes from (is it from the same class, superclass, top-level or is it an extension). When we call a method on a composed object, we know where it comes from.
Composition is more demanding — We need to use composed objects explicitly. When we add some functionalities to a super-class, we often do not need to modify subclasses. When we use composition, we more often need to adjust usages.
Inheritance gives us a strong polymorphic behavior — This is also a double-edged sword. From one side, it is comfortable that a dog can be treated like an animal. On the other side, it is very constraining. It must be an animal. Every subclass of the animal should be consistent with animal behavior. Superclass set contract and subclasses should respect it.