With the introduction of OOPs, Inheritance and Composition entered our senses and still confusing us.
Choosing between Composition and Inheritance is not easy. Often, the decision depends upon the information which is not yet there.
The structure of an application plays an essential role in adding changes. Composition and Inheritance are the building blocks of an application structure. It is imperative to know when to use which one.
Inheritance is a way of reusing code by inheriting the structure from the referred class or Type. The referred class is called
parent
or base
class and the referring class is called child
or subclass
.Inheriting the structure suggests that subclass inherits public members (API) of the parent class. A subclass can override or add new behaviour or extend the existing behaviour of the parent class.
// Reference class
class Animal {
fun walk() {
println("animal walk");
}
fun speak() {
println("...")
}
}
// Referring Class
// Cat class "inherits the structure" from the Animal class
class Cat : Animal {
// overriding existing behaviour
fun speak() {
println("meow")
}
}
// in main
val cat = Cat();
cat.walk() // Cat can "reuse" parent class behavior
cat.speak() // meow
Composition is a way of reusing code by interacting with the referred class without any structural imposition.
// Reference Class
class Line (val x: Int, val y:Int);
fun draw() {
println("line drawing")
}
}
// Refering class
class Shape (val sides: Array<Line>) {
fun shapeDraw() {
for(side in sides) {
side.draw(); // code refering
}
}
}
Uniformity
I had a situation to ignore JsonNaming annotation¹ provided by Jackson library (Java ecosystem). There is no direct API provided by the library to facilitate the same.
A possible solution to have a new class dictating the desired functionality and injecting the new class object to the library. Luckily, the chosen library provides the API to inject custom class (Thanks to open-close design).
Should I compose or inherit from the existing Class? - inherit.
// class for library
class JSONIntrospector {
// .. rest code
public Object findNamingStrategy(AnnotatedClass ac) {
JsonNaming ann = _findAnnotation(ac, JsonNaming.class);
return (ann == null) ? null : ann.value();
}
// .. rest code
}
// custom class
class IgnoreJsonNaming extends JSONIntrospector {
@Override
public Object findNamingStrategy(AnnotatedClass ac) {
return null;
}
}
// inject code
ObjectMapper mapper = new ObjectMapper();
mapper.setAnnotationIntrospector(new IgnoreJsonNaming());
Inheritance is more suitable in this context as it is expected to have structural similarity or uniformity.
Composition would not have worked : If I preferred to go with Composition, I would have to implement unnecessary methods to maintain structural similarity.
Imagine, If in future, library developers add new behaviours to the referred Class, I would end up changing in the latest Class to have structural similarity.
Inheritance and Composition not only allow to reuse the code; they also define a relationship semantic between two classes.
Inheritance : is-a
Composition : has-_ (_ means any quantifier)
Apple is a Fruit. (Inheritance)
Apple has seeds. (Composition)
Human is a mammal. (Inheritance)
Human has two hands. (Composition)
These keywords assist in basic analysing the requirements.
Whenever we write software, we introduce so many concepts from Specification and Implementation point of view.
Through rigorous analysis, these concepts end up with numerous classes. Shaping these classes (object decomposition) and connecting these classes become our primary responsibility.
Implementation Concepts: Factory, Manager, Service, Client, Provider
e.g. RestClient, EmailService, ValidtorFactory
Sometimes, it is better to start with code at first, then evolve the design. You are free to play until the first release. Once the software is released, it is hard to reverse most of the decisions.
Inheritance does not befit well for multi-dimensions concepts or variants or attributes. If applied, it leads to Combinatorial Explosion of classes.
Say, you are modelling Pizza (concept 1) with two allowed Toppings (concept 2) Onion and Tomato. If you choose Inheritance to connect Pizza and Topping, then you would end with four classes.
// with inheritance
class Pizza { // without topping
fun getPrice() {
return 100;
}
}
class PizzaWithTomatoToppings : Pizza {
fun getPrice() {
return 120;
}
}
class PizzaWithOnionTopping : Pizza {
fun getPrice() {
return 120;
}
}
class PizzaWithOnionAndTomatoToppings : Pizza {
fun getPrice() {
return 140;
}
}
With Composition, two classes are sufficient.
// with composition
class Pizza {
var t1 : Topping? = null ;
var t2 : Topping? = null ;
fun getPrice() {
val basePrice = 100;
return basePrice + t1?.getPrice() ?: 0 + t2?.getPrice() ?: 0
}
}
class Topping(val price: Int) {
fun getPrice() {
return price;
}
}
Previously published at https://themightyprogrammer.dev/article/inheritance-composition