Composition and inheritance are two of the most popular principles of Object-oriented programming. While inheritance is super essential in making good softwares, there are some scenarios where we should avoid inheritance and prefer "composition before inheritance". In this blog, I have tried to explain such scenarios where composition can help us write better code.
Establishing the problem
One of the key characteristics of inheritance is that the child classes acquire all the properties and methods declared in the parent class. Sometimes, some of those properties or methods of the parent class might not be needed by the child class. Because of this, people often say inheritance make softwares "rigid".
Consider the code given below -
class ProgrammingLanguage{
private String name;
// constructor
public ProgrammingLanguage(String name){
this.name = name;
}
// methods
public void execute(){
System.out.println("Executing..");
}
public void compile(){
System.out.println("Compiling..");
}
// getters
public String getName(){
return name;
}
}
class Java extends ProgrammingLanguage {
public Java(){
super("Java");
}
// methods
public void typeCheck(){
System.out.println("Checking..");
}
}
class Python extends ProgrammingLanguage{
public Python(){
super("Python");
}
// methods
public void listFeatures(){
System.out.println("Some amazing technical features: ");
}
}
Pause for a second, and try to identify the problem in this code. Hint: Python is not a compiled language.
If you could not identify the problem, consider this code -
public static void main(String[] args) {
Java javaProgram = new Java();
javaProgram.compile();
Python pythonProgram = new Python();
pythonProgram.compile(); // here's the problem
}
Because python is not a compiled language, but extending Python
class from ProgrammingLanguage
class has made it rigid and the compile method from ProgrammingLanguage
class has made it to the Python
class as well. Clearly, this is redundant.
Solving the problem
To avoid this and make softwares "flexible", we can make use of composition. Composition is when we add objects of other classes in a class and delegate some of the functionality to the methods of that other class. So, this allows us to bring-in only those methods which are needed in our class.
Let's modify the above code to include composition and see how well it solves our problem.
First of all, we strip-off the compile method from the ProgrammingLanguage
class and make two separate classes called Compiler
and Interpreter
as shown below.
abstract class ProgrammingLanguage {
private String name;
// constructor
public ProgrammingLanguage(String name){
this.name = name;
}
// methods
abstract public void execute();
// getters
public String getName(){
return name;
}
}
class Compiler {
private String name;
// constructor
public Compiler(String name){
this.name = name;
}
// methods
public void compile(){
System.out.println("Compiling using " + name);
}
}
class Interpreter {
private String name;
public Interpreter(String name){
this.name = name;
}
// methods
public void interpret(){
System.out.println("Interpreting using " + name);
}
}
Now let's make use of Composition and inheritance together and see what comes out.
class Java extends ProgrammingLanguage {
// composition
private Compiler compiler;
public Java(){
super("Java");
this.compiler = new Compiler("javac");
}
// methods
@Override
public void execute(){
compiler.compile();
System.out.println("Executing..");
}
public void frameworks(){
System.out.println("Spring and spring boot");
}
}
class Python extends ProgrammingLanguage {
// Composition
private Interpreter interpreter;
public Python(){
super("Python");
this.interpreter = new Interpreter("CPython");
}
// methods
@Override
public void execute(){
interpreter.interpret();
System.out.println("Executing..");
}
public void applications(){
System.out.println("AI and ML");
}
}
If we try to make objects of Java
and Python
classes in the main program, it would look something like this
public static void main(String[] args) {
Java javaProgram = new Java();
javaProgram.execute();
// prints this
// Compiling using javac
// Executing..
Python pyProgram = new Python();
pyProgram.execute();
// prints this
// Interpreting using CPython
// Executing..
}
As you can see, we do not have have any redundant methods in any of the two classes. Also, even if we make a new programming language which is just a compiled language, or just an interpreted language or even a compiled + interpreted language, we have the flexibility to do whatever it asks us to do. This is the power of composition.
So, as I described composition above, we are composing a class with objects of other classes and delegating some of the functionality to these other classes which is making our software flexible. We define composition as "has-a" relationship. For example, Python "has-a" interpreter, Java "has-a" compiler. Whereas we define inheritance as "is-a" relationship. Python "is-a" programming language and similarly Java "is-a" programming language.
So clearly, there is no right answer for which is important - inheritance or composition. Both are useful in making good applications, it's just we need to know when to use what and how.
That's all for this blog. Right now, I am learning some advanced concepts in object-oriented design and I am planning to make a complete blog series on Object-oriented design. So, stay tuned for more great content! Till then, keep learning and keep having fun!