Context
Back in the autumn of 2022, during university on-campus placements, I sat for the second round of my interview at Persistent Systems, which was one of my best interview experiences. A purely technical round for which I was expecting questions about graphs, advanced data structures, and whatnot, as the previous round had been fully about DSA, I was stunned to hear "Let's do a discussion about Object Oriented Programming", to which an instant thought of all possible questions regarding OOPS came to my mind. Imagine my shock, and awe, when , there wasn't a single question about writing a friend function, or even about anything code!
In this blog, I'd be sharing an overview of our entire discussion as a technical content rather than an interview experience.
Basics of the four pillars
When talking about Object Oriented Programming, we are mostly taught a few things.
Abstraction - definition and a few examples of abstract class
Encapsulation - what's that and an introduction to classes.
Inheritance - the how's and why's
Polymorphism- a cat and a dog both walk ?
Friend function - They know your private information :)
The first four - Abstraction, Encapsulation, Inheritance, Polymorphism , are considered the four pillars of OOPs. Let's discuss a small overview about each, as we learn in our college degrees.
Abstraction
Abstraction means displaying only essential information and hiding the details. Data abstraction refers to providing only essential information about the data to the outside world, hiding the background details or implementation.
Consider a real-life example of a man driving a car. The man only knows that pressing the accelerator will increase the speed of the car or applying brakes will stop the car but he does not know how on pressing the accelerator the speed is actually increasing, he does not know about the inner mechanism of the car or the implementation of the accelerator, brakes, etc. in the car.
Encapsulation
Encapsulation is defined as binding together the data and the functions that manipulate them. By encapsulating data, developers can control access to it, thereby preventing unintended modification and ensuring data integrity. Encapsulation fosters modularity by hiding the internal implementation details of a class, allowing developers to interact with objects through well-defined interfaces.
Inheritance
Inheritance enables code reuse and promotes the creation of hierarchical relationships between classes. Through inheritance, a new class (subclass) can inherit properties and behaviors from an existing class (superclass), thereby extending and specializing its functionality.
Polymorphism
Polymorphism allows objects of different classes to be treated as instances of a common superclass. This concept enables developers to write code that can operate on objects of various types without knowing their specific class.
Part 1 : Inheritance or composition ?
When dealing with classes and objects, extendibility plays a major role in defining your object schema, data handling, and ensuring the scalability and maintainability of the codebase. There is a simple calculation that one can do to check the memory size consumed by an object. While understanding inheritance, it is imperative to understand about constructors and their order of execution. Consider the following modified example from geeks for geeks.
class Parent1
{
public:
// first base class's Constructor
Parent1()
{
cout << "Parent 1" << endl;
}
};
class Parent2
{
public:
// second base class's Constructor
Parent2()
{
cout << "Parent 2" << endl;
}
};
// child class inherits Parent1 and Parent2
class Child : public Parent1, public Parent2
{
public:
// child class's Constructor
Child()
{
cout << "Child" << endl;
}
};
int main() {
Child obj1;
return 0;
}
The output of the above program displays
Parent 1
Parent 2
Child
Therefore, the order of constructor call is something like this :
Child->Parent2->Parent1 and hence the output of Parent1 comes first.
One observable fact in this example is :
Even though, we never used anything from Parent1, or Parent2, their constructors still got executed right inside Child.
This in a way is standard considering inheritance behavior. However , consider a case where Parent2 has a huge overhead which apparently isn't that necessary for any of the Child method.
Why and when inheritance is bad?
Let's look at the following code:
#include <iostream>
using namespace std;
class Parent
{
int arr[100];
public:
char charArr[400];
float pi;
void def(string name="Parent"){
cout<<"Hi I'm def called from "<<name<<endl<<endl;
}
// second base class's Constructor
Parent()
{
cout << "Constructor of parent class " << endl;
}
};
// Child class inherits Parent
class Child: public Parent
{
int arr2[300];
public:
void callDef(){
def("inherited child");
}
// child class's Constructor
Child()
{
cout << "Constructor of inheritance child class" << endl;
}
};
int main() {
Child obj1;
cout<< "Size of inherited child: "<< sizeof(obj1)<<endl;
return 0;
}
The output of the above code is:
Constructor of parent class
Constructor of inheritance child class
Size of inherited child: 2004
The size of the child class is 2004 bytes, which isn't okay for a class doing almost nothing. Let's take a look at how the size comes to 2004 bytes.
int arr2[300]
from Child costs us 300*4= 1200 bytesint arr[100]
from Parent costs 100*4= 400 byteschar charArr[400]
from Parent costs 400*1=400 bytesfloat pi
from Parent costs 4*1= 4 bytesbringing the sum total to (1200+400+400+400)= 2004 bytes.
All that overhead with no real use, just takes up memory unnecessarily. Thus, inheritance is not suitable for situations like these mainly because:
Huge overhead from parent causes increase in child's size unnecessarily.
Since inheritance depicts a "is-a" relationship, the theoretical structuring is wrong since a child is not a parent. A child "has-a" parent.
So what to do?
When talking about relationships between classes and objects, there are mainly two forms: "is-a" and "has-a". While "is-a" is termed as inheritance, "has-a" is coined the term "Composition".
Object composition is used for objects that have a “has-a” relationship with each other. Therefore, the complex object is called the whole or a parent object whereas a simpler object is often referred to as a child object. Let's modify our code above to understand the difference between the two approaches.
#include <iostream>
using namespace std;
class Parent
{
int arr[100];
public:
char charArr[400];
float pi;
void def(string name="Parent"){
cout<<"Hi I'm def called from "<<name<<endl<<endl;
}
// second base class's Constructor
Parent()
{
cout << "Constructor of parent class " << endl;
}
};
// Child class inherits Parent
class Child: public Parent
{
int arr2[300];
public:
void callDef(){
def("inherited child");
}
// child class's Constructor
Child()
{
cout << "Constructor of inheritance child class" << endl;
}
};
// Child2 class compsites Parent
class Child2
{
int arr2[300];
public:
void callDef(){
Parent p1;
p1.def("composition child");
}
// child2 class's Constructor
Child2()
{
cout << "Constructor of composition child class" << endl;
}
};
int main() {
Child obj1;
Child2 obj2;
cout<< "Size of inherited child: "<< sizeof(obj1)<<endl;
obj1.callDef();
obj2.callDef();
cout << "Size of composition child: "<<sizeof(obj2)<<endl;
return 0;
}
If we were to look at the output of the code, we'd be amazed!
Constructor of parent class
Constructor of inheritance child class
Constructor of composition child class
Size of inherited child: 2004
Hi I'm def called from inherited child
Constructor of parent class
Hi I'm def called from composition child
Size of composition child: 1200
Let's understand the metrics.
When using composition, the size of the object reduced by half. None of the overhead of the parent class was carried onto the child. With composition, the constructor of the parent class was only called when we used the callDef() method and not in the beginning like inheritance. With these things in mind, we are ready to summarize the differences!
Differences between inheritance and composition
Aspect | Inheritance | Composition |
Memory overhead | Objects include all superclass members | Objects contain references to components |
Object size | Objects may be larger due to superclass members | Objects may be smaller, only contain references to components |
Flexibility & granularity | Less granular, inherits all superclass members | More flexible, components tailored to needs |
Dynamic memory allocation | May involve dynamic allocation for polymorphism | Limited dynamic allocation for component creation/destruction |
Object creation & initialization | Requires initialization of inherited members | Simpler initialization, only components need to be initialized |
Use cases | Employee hierarchy, UI component classes, Exceptions etc. | Authentication Services, Encryption Services, Logging Framework etc. |
Conclusion
Part 1 talked mostly about the basics of OOPs and the comparison between inheritance and composition, highlighting their best use cases, examples and when to use composition. We didn't yet answer the question of when to use inheritance completely.
Part 2 would be focused on a trickier approach to encapsulation, abstraction and polymorphism where we talk about how they are, what data binding and hiding refers to, and connecting everything together. Stay tuned folks!