OOPs! An interview experience Part 1

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 bytes

  • int arr[100] from Parent costs 100*4= 400 bytes

  • char charArr[400] from Parent costs 400*1=400 bytes

  • float pi from Parent costs 4*1= 4 bytes

    bringing 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

AspectInheritanceComposition
Memory overheadObjects include all superclass membersObjects contain references to components
Object sizeObjects may be larger due to superclass membersObjects may be smaller, only contain references to components
Flexibility & granularityLess granular, inherits all superclass membersMore flexible, components tailored to needs
Dynamic memory allocationMay involve dynamic allocation for polymorphismLimited dynamic allocation for component creation/destruction
Object creation & initializationRequires initialization of inherited membersSimpler initialization, only components need to be initialized
Use casesEmployee 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. Stay tuned folks!