In the real world, complex systems are built from simple parts. For example, we might decide to make a kitchen. A kitchen would have a refrigerator, a stove, a dishwasher, and countertops. The stove would consist of burgers, switches to turn the burners on and off, and gas values. The gas valve may have a cutoff mechanism and a pressure regulator.
All of these complex objects are created by smaller and simpler components. It is also worth noting that the Kitchen has a stove. The Stove has a burner, and so on. In OOP, the creation of complex objects from simpler components is known as composition. We create classes that are of small concern and scope and then combine them into larger classes.
Kotlin seems to favor composition by the way. You may notice that classes are final by default and that developers need to mark classes as open prior to using inheritance. Likewise, methods need to be marked as open before they can be overridden. Kotlin has a delegation mechanism where the compiler can create delegate methods when using another object. Finally, our interfaces can even have properties and default behaviors. Even if composition wasn’t the intention, all of these mechanisms in the language tend to steer a developer towards composition.
All of which is desirable. We could create classes and add more features to them by using inheritance. Nevertheless, classes in Kotlin are only allowed to have one parent class which means we can’t use two or more parent classes to create a single type. Furthermore, we could end up creating instability in the code as changes in base classes could end up breaking child classes. Finally, unit testing classes become more difficult because do we continue to test superclass behavior in child classes to prevent regressions?
Composition addresses many such issues. Complexe classes use simple objects to break down a problem into manageable scopes. Classes that implement interfaces become loosely coupled and allow for additional maintainability. We can easily unit test small classes and then move onto to unit testing larger classes. Finally, we can swap or add components as needed to keep the code maintainable.
Inheritance describes an is-a relationship. As in, a Truck is-a a vehicle. Composition describes a has-a relationship as in a Cook has-a spatula. We certainly would not want to extend a Cook from a Spatula or a Spatula from a Cook. The relationship would make no sense. Would we really want methods that target Cooks also be able to accept Spatula objects through polymorphism? Should all Cooks get the same properties and behaviors as a Spatula?
We solve such problems by having Cooks own a spatula. Both classes are distinct entities that have different concerns. Our Cooks should not only be able to posses Spatulas but also other cooking utensils as needed. Each utensil may be used for a different purpose. For example, the cook uses the thermometer for checking food temperature and a spatula for flipping a burger patty.
It’s not difficult to model such a relationship in Kotlin. We only need to use classes and interfaces to put such a relationship together. Let’s start with a Utensil interface.
interface Utensil { val name : String fun interact(f : Food) }
Now we only need to create two classes that implement the Utensil interface. Here is the thermometer.
class Thermometer : Utensil { override val name: String get() = "Thermometer" override fun interact(f: Food) { println("The ${f.name} has a temperature of 160") } }
Followed by the Spatula
class Spatula : Utensil{ override val name: String get() = "Spatula" override fun interact(f: Food) { println("Flipping the ${f.name}") } }
Our Cook class can use either tool.
class Cook(var utensil: Utensil) { val name = "Bob" fun cook() : Food { val burger = object : Food { override val name: String get() = "Burger" } println("Bob is cooking") println("Now Bob is using the ${utensil.name}") utensil.interact(burger) println("Bob is done cooking") return burger } }
Since Cook has a Utensil property, he is free to use either the Thermometer or the Spatula. This flexibility would have been almost impossible to model using inheritance and equally difficult to maintain. However, since our Cook possesses Utensil objects, we can freely swap out different utensils as needed.
Putting it Together
Use composition when you need to create complex objects using simple objects. Try and remember that each object should have its own unique concern. Finally, use interfaces when possible to avoid tight coupling between classes. The following program demonstrates a program that uses compositions and interfaces to create a complex object.
package ch5.interfaces.composition /** * We shouldn't tie any object to one specific kind of Food */ interface Food { val name : String } /** * Likewise, we shouldn't tie any one specific object to a specific * Utensil. We should always try and think generally. */ interface Utensil { val name : String fun interact(f : Food) } /** * The Spatula is a Utensil */ class Spatula : Utensil{ override val name: String get() = "Spatula" override fun interact(f: Food) { println("Flipping the ${f.name}") } } /** * The Thermmometer is another Utensil */ class Thermometer : Utensil { override val name: String get() = "Thermometer" override fun interact(f: Food) { println("The ${f.name} has a temperature of 160") } } /** * Cook is a complex object that is made up of Utensils and outputs Food */ class Cook(var utensil: Utensil) { val name = "Bob" fun cook() : Food { //Notice the return type is Food val burger = object : Food { override val name: String get() = "Burger" } println("Bob is cooking") println("Now Bob is using the ${utensil.name}") utensil.interact(burger) println("Bob is done cooking") return burger } } fun main(args : Array<String>){ val bob = Cook(Thermometer()) bob.cook() println() bob.utensil = Spatula() bob.cook() }
Output
Bob is cooking Now Bob is using the Thermometer The Burger has a temperature of 160 Bob is done cooking Bob is cooking Now Bob is using the Spatula Flipping the Burger Bob is done cooking