Composition over inheritance
Have you ever heard of “composition over inheritance”? It's one of my favorite concepts in software design. According to Wikipedia, it is:
Classes should achieve polymorphic behavior and code reuse by their composition (by containing instances of other classes that implement the desired functionality) rather than inheritance from a base or parent class
Does it make sense to you? Even if not, don’t worry. I will show what it looks like and why it is good.
For instance, let’s suppose that you are an Engineering Manager having some Java or Kotlin repositories. As a manager, you work with both developers.
Since Kotlin is a newer language that is 100% compatible with Java, you may assume Kotlin developers can write also Java. You can assign a Kotlin developer to a Java project. In this case, how do you define JavaDeveloper
and KotlinDeveloper
classes?
What does code look like using inheritance?
First, let's try using classical inheritance. Simply you will make KotlinDeveloper inherit JavaDeveloper.
Here, you have to declare JavaDeveloper with open
keyword because, in Kotlin, classes are final
by default. It looks like Kotlin doesn't recommend us using inheritance, does it? You might hear of the SOLID principle. They are mostly about good practices using inheritances. Things are more and more complicated when the inheritance becomes deeper and deeper.
The question is, how do we add functionalities without using inheritance? Here is where we introduce composition.
What does code look like using composition (delegation)?
The title of this blog is composition-over-inheritance. The composition
there means that adding functionalities to a class should be achieved by implementing interfaces and delegation, rather than inheritance. Let's see an example.
It might look awkward that a developer receives another developer and ask them to work but, it is just an example. You can assume that a Kotlin developer is injected with the capability of working as a JavaDeveloper. The point is that now Kotlin developers can also act as JavaDeveloper without inheritance. Correspondingly, the JavaDeveloper class doesn't have to have open
modifier anymore.
It will minimize software design issues caused by inheritance. It is cool, isn't it?
But it has some drawbacks such as:
- We have to inject dependencies
- We have to write tons of override functions that just delegate messages to dependencies.
Let's see how we address them.
Address drawbacks of composition
The first one — injecting dependencies — can be handled by dependency inject frameworks such as Dagger, Kodein, Koin etc... If you work on Android apps, probably you use Hilt.
The second one — overriding functions — is managed by Kotlin's built-in delegation mechanism. That's one of my favorite language specs in Kotlin. You can simply use by
keyword for interfaces and delegate implementation to dependencies.
We successfully removed the override fun writeJava
that doesn't do anything in KotlinDeveloper class.
Why is composition better than inheritance?
We can rephrase the question to “Why is shallow inheritance good?” since composition doesn’t require class hierarchy. And it is good because, less and less inheritance we have, less and less maintenance cost we pay. Let’s suppose that we have deep inheritance, when we want to change behaviors at some level, we have to traverse parents to establish consistency. The below article is not really about composition but it shows quite well what kind of problem we will have with deep inheritance.
When do we use inheritance, not composition?
In many cases, composition is preferred over inheritance but it doesn’t mean we don’t use inheritance at all. The link put below is an Android app sample published by an official Android team.
In the link, they define AddEditTaskFragment
extending Fragment
. It totally makes sense because: