Effective Kotlin Item 36: Prefer composition over inheritance
Inheritance is a powerful feature, but it is designed to create a hierarchy of objects with an "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; instead, we should prefer a lighter alternative: class composition.
Simple behavior reuse
Let’s start with a simple problem: we have two classes with partially similar behavior. They should both display a progress bar before some action and hide it afterwards.
In my experience, many developers would extract this common behavior by extracting a common superclass:
This approach works for such a simple case, but it has important downsides we should be aware of:
We can only extend one class. Extracting functionalities using inheritance often leads to either 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 this method works.
These are strong reasons that should make us think about an alternative, and a very good one is composition. By composition, we mean holding an object as a property (we compose it) and using its functionalities. This is an example of how we can use composition instead of inheritance to solve our problem:
Notice that composition is harder. We need to include the composed object and use it in every single class. This is the key reason why many prefer inheritance. However, this additional code is not useless: it informs the reader that a progress bar is used and how it is used. It also gives the developer more power over how this progress bar works.
Another thing to note is that composition is better when we want to extract multiple pieces of functionality. For instance, information that loading has finished:
We cannot extend more than a single class. Therefore, if we wanted to use inheritance instead, we would be forced to place both functionalities in a single superclass. This often leads to a complex hierarchy of the types that are used to add these functionalities. Such hierarchies are very hard to read and often also to modify. Just think about what happens if we need an alert in two subclasses but not in a third one? One way to deal with this problem is to use a parameterized constructor:
This is a bad solution. Having functionality blocked by a flag (
showAlert in this case) is a bad sign. This problem is compounded when the subclass cannot block other unneeded functionality. This is a trait of inheritance: it takes everything from the superclass, not only what is needed.
Taking the whole package
When we use inheritance, we take everything from the superclass: methods, expectations (contract), and behavior. Therefore, it is a great tool for representing a hierarchy of objects, but it’s not so great when we just want to reuse some common parts. For such cases, composition is better because we can choose the behavior we need. As an example, let’s say that in our system we have decided to represent a
Dog that can
What if then we need to create a robot dog that can bark but can't sniff?
Notice that such a solution violates the interface-segregation principle as
RobotDog has a method it doesn’t need. It also violates the Liskov Substitution Principle as it breaks superclass behavior. On the other hand, what if your
RobotDog also needs to be a
Robot class because
Robot can calculate (i.e., it has the
calculate method)? Multiple inheritance is not supported in Kotlin.
These are serious design problems and limitations that do not occur when you use composition instead. When we use composition, we choose what we want to reuse. To represent type hierarchy, it is safer to use interfaces, and we can implement multiple interfaces. What has not yet been shown is that inheritance can lead to unexpected behavior.
Inheritance breaks encapsulation
To some degree, when we extend a class, we depend not only on how it works from the outside but also on how it is implemented inside. This is why we say that inheritance breaks encapsulation. Let’s look at an example inspired by the book Effective Java by Joshua Bloch. Let’s say that we need a set that will know how many elements have been added to it during its lifetime. Such a set can be created using inheritance from
This implementation might look good, but it doesn’t work correctly:
Why is that? The reason is that
HashSet uses the
add method under the hood of
addAll. The counter is then incremented twice for each element added using
addAll. This problem can be naively solved by removing the custom
However, this solution is dangerous. What if the creators of Java decided to optimize
HashSet.addAll and implement it in a way that doesn't depend on the
add method? If they did that, this implementation would break with a Java update. Together with this implementation, any other libraries which depend on our current implementation would break as well. The Java creators know this, so they are cautious of making changes to these types of implementations. The same problem affects any library creator or even developers of large projects. So, how can we solve this problem? We should use composition instead of inheritance:
One problem is that in this case we lose polymorphic behavior because
CounterSet is not a
Set anymore. To keep this behavior, we can use the delegation pattern. The delegation pattern means our class implements an interface, composes an object that implements the same interface, and forwards methods defined in the interface to this composed object. Such methods are called forwarding methods. Take a look at the following example:
The problem now is that we need to implement a lot of forwarding methods (nine, in this case). Thankfully, Kotlin introduced interface delegation support that is designed to help in this kind of scenario. When we delegate an interface to an object, Kotlin will generate all the required forwarding methods during compilation. Here is Kotlin’s interface delegation in action:
This is a case in which delegation is a good choice: we need polymorphic behavior and inheritance would be dangerous. However, delegation is not common. In most cases, polymorphic behavior is not needed or we use it in a different way, so composition without delegation is more suitable.
The fact that inheritance breaks encapsulation is a security concern, but in many cases the behavior is specified in a contract or we don’t depend on it in subclasses (this is generally true when methods are designed for inheritance). There are other reasons to choose the composition pattern, one of which is that it is easier to reuse and gives us more flexibility.
To prevent developers from extending classes that are not designed for inheritance, we can just keep them final. However, if for some reason we need to allow inheritance, all methods are still final by default. To let developers override them, they must be set to open:
Use this mechanism wisely and open only those methods that are designed for inheritance. Also, remember that when you override a method, you can make it final for all subclasses:
In this way, you can limit the number of methods that can be overridden in subclasses.
There are a few important differences between composition and inheritance:
Composition is more secure - We depend not on how a class is implemented but only on its externally observable behavior.
Composition is more flexible - We can extend only a single class but we can compose many. When we inherit, we take everything; but 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 have composed changes, it will only change our behavior if it has changed its contract with the outside world.
Composition is more explicit - This is both an advantage and a disadvantage. When we use a method from a superclass, we can do so implicitly, like methods from the same class. This 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, a superclass, the 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 a composed object explicitly. When we add some functionalities to a superclass, we often do not need to modify the subclasses. When we use composition, we more often need to adjust usages.
Inheritance gives us strong polymorphic behavior - This is also a double-edged sword. On one hand, it is convenient that a dog can be treated like an animal. On the other hand, it is very constraining: it must be an animal. Every subclass of an animal should be consistent with animal behavior. The superclass defines the contract, and the subclasses should respect it.
It is a general OOP rule to prefer composition over inheritance, but Kotlin encourages composition even more by making all classes and methods final by default and by making interface delegation a first-class citizen. This makes this rule even more important in Kotlin projects.
So, when is inheritance more reasonable? The rule of thumb is that we should use inheritance when there is a definite "is a" relationship. In other words, every class that uses inheritance needs to be its superclass. All unit tests written for superclasses should also pass for their subclasses, and every usage in production code should be exchangeable (Liskov substitution principle). Object-oriented frameworks for displaying views are good examples:
Application in JavaFX,
Activity in Android,
UIViewController in iOS, and
React.Component in React. The same is true when we define our own special kind of view element that always has the same set of functionalities and characteristics. Just remember to design these classes with inheritance in mind, and specify how inheritance should be used. Also, all the methods that are not designed for inheritance should be kept final.