Favour Composition over Inheritance
When dealing with code duplication and reusabilty issues it is good to favour Composition over Inheritance.
Overview
- This is a universally accepted concept.
- Inheritance is not always bad.
- Composition is not suitable for or used in all circumstances.
- Kotlin encourages Composition and makes Inheritance harder.
- Design and document for Inheritance else prohitibt it.
- Composition should be the first design choice in mind.
- We should still use Inheritance if it is required and designed for.
Example 1
Let's look at an example. We add some an some animals
to a program.
class Dog {
fun bark()
fun eat()
}
class Cat {
fun meow()
fun eat()
}
We have dupication of the eat()
function so let's solve this with a base Animal
class.
class Animal {
fun eat()
}
class Dog: Animal {
fun bark()
}
class Cat: Animal {
fun meow()
}
However, we get the error This type is final, so it cannot be inherited from
.
This is because Kotlin helps us not abuse Interitance so by default classes are final
which means we need the special keyword open
to extend them (Java is the opposite).
Let's add the open
keyword and inherit from the base class.
open class Animal {
fun eat()
}
class Dog: Animal {
fun bark()
}
class Cat: Animal {
fun meow()
}
It looks good so far with Inheritance.
Now let's add some more functionality to the program. We now need to add some robots
.
class CleaningRobot {
fun clean()
fun drive()
}
class FeedingRobot {
fun feed()
fun drive()
}
There is duplication on drive()
so let's use Inheritance to solve this.
open class Robot {
fun drive()
}
class CleaningRobot: Robot {
fun clean()
}
class FeedingRobot: Robot {
fun feed()
}
All is well so far but now we add more functionality.
We now require a cleaning robot dog
. It needs to drive()
, clean()
and bark()
but doesn't eat()
.
It doesn't look good for Inheritance because we can only inherit from one class. We could try and make a new super class but you will end up with an unclear base class with too much responsibility.
We are stuck because Inheritance acts as a contract that is hard to break. Also the more features you add the tighter the coupling becomes and the tighter the coulping the more you add features!
This can be avoided by opting for Composition.
Composition
- It is the concept of a class referencing instances of other objects (every class basically does that)
- Gives same functionality as Inheritance without the coupling.
- If Inheritance is a good fit you should use it else use Composition.
- Kotlin is a great language for Composition.
This is an example of Composition.
Class A contains variable B
class A {
val b = B()
}
Kotlin favours Composition
- In Kotlin all classes are
final
and notopen
for Inheritance by default. - It has the keyword
Object
. This creates a single instance of a class in the compiler. - We can delegate the implementation of an Inerface to another class with the keyword
By
.
So let's use Composition to implement the cleaning robot dog
. We need three Interfaces for this:
interface Cleaner {
fun clean()
}
interface Drivable {
fun drive()
}
interface Barker {
fun bark()
}
Now the Interfaces need implmentation. Let's create three Objects (you can also use classes here).
object CleaningRobot: Cleaner {
override fun clean() { }
}
object DrivingRobot: Drivable {
override fun drive() { }
}
object BarkingAnimal: Barker {
override fun bark() { }
}
Now the can create the cleaning robot dog
class and use Composition.
The By
keyword allows us to implement the Interface and delegate the work to the Objects.
class CleaningRobotDog:
Cleaner by CleaningRobot,
Drivable by DrivingRobot,
Barker by BarkingAnimal
They can then be used again and again to compose classes that require the behavior. No Inheritance needed.
Using the By
keyword is called Delegation. We can use the functionality available from delegating class/object which in this case is drive()
, clean()
and bark()
.
Example 2
How about a more realistic example.
We require an analytics logger feature to log print statements for app lifecycle events start
and pause
. We need to include this behaviour in all our screen classes.
A beginner might use a static class for this but that will contain boilerplate code and coupling that is difficult to test.
So let's use Composition to implement the AnalyticsLogger
:
interface AnalyticsLogger {
fun registerLifeCycleOwner(owner: LifecycleOwner)
}
class AnalyticsLoggerImplementation: AnalyticsLogger, LifecycleEventObserver {
override fun registerLifeCycleOwner(owner: LifecycleOwner) {
owner.lifecycle.addObserver(this)
}
override fun onStateChanged(source: LifecycleOwner, event: Lifecyle.Event) {
when(event) {
Lifecycle.Event.ON_RESUME -> println("User opened the screen")
Lifecycle.Event.ON_PAUSE -> println("User closed the screen")
else -> Unit
}
}
}
We can now add the AnalyticsLogger
to our Activity class (screen class).
We just need to pass the LifecycleOwner inside of registerLifeCycleOwner
.
The By
keyword is then used delegate the work to the AnalyticsLoggerImplementation
.
Also because we use an Interface we can add more than one (unlike class Inheritance).
class MainActivity: ComposeActivity, AnalyticsLogger by AnalyticsLoggerImplementation {
override fun onCreate(saveInstanceState: Bundle?) {
super.onCreate(saveInstanceState)
registerLifeCycleOwner(this)
}
}
I hope you learned something new today. Merry Christmas.