OOPs! An interview experience part 2

In the part 1 of this three part blog, we had taken a tour of inheritance and composition. Understanding when to use which, is very necessary while building complex applications. Now that we understand more about inheritance, it is time to move on to the other pillars.

Encapsulation

Encapsulation is the process of grouping relevant data and methods into a single block, often termed class in programming languages like Java, C++. According to GeeksForGeeks, encapsulation is defined as binding together the data and the functions that manipulate them. Encapsulation improves readability, maintainability, and security by grouping data and methods together.
An example would show the basics of what we achieve with encapsulation.

#include <iostream>
using namespace std;
class Calculator{    
    int a = 5;
    int b = 6;


public:
    Calculator(int first, int second) {
    this->a = first;
    this->b = second;
    }
    int add(){ return a + b ; }    
};

int main() {
Calculator C1(5,8);
int ans=C1.add();
cout<<ans<<endl;  // 13
return 0;    
}

Pretty simple right ? It doesn't seem to do us much in dwelling over what encapsulation is then. Let's come back to this while discussing about data hiding.

Abstraction

Abstraction is yet another pillar of OOP. Abstraction generally refers to hiding certain implementations to the end user and displaying only the necessary parts. A common example across multiple books would be the working of a gear box of a car.

However, when we talk about data hiding, we come across a similar concept in encapsulation as well. While this does talk about hiding data members by access specifiers for encapsulation, the same logic doesn't work with abstraction.
Event if we do consider that this also is abstraction in a way, then why does Abstraction in itself exist as a pillar of OOP and not just a concept/term.

So maybe, the way we look at abstraction needs a little refinement. Let's loop back to this in the end.

Polymorphism

Polymorphism is the ability of a message to be displayed in different forms. A common example can be a function named Area that returns the area of an object.
The returned value can be the area of a square or a rectangle or a circle, or even a hexagon.

Polymorphism in OOP are of two types:

  1. Compile time polymorphism (method / operator overloading )

  2. Run time polymorphism (method overriding)

Compile time polymorphism

When there are multiple functions/methods with the same name but different parameters, then the functions/methods are said to be overloaded. Functions can be overloaded by changing the number of arguments or/and changing the type of arguments.

#include <iostream>
#include <cmath> // For M_PI and pow
using namespace std ;
class AreaCalculator {
public:
    // Overloaded function to calculate area of a rectangle
    double calculateArea(double length, double width) {
        return length * width;
    }

    // Overloaded function to calculate area of a circle
    double calculateArea(double radius) {
        return M_PI * pow(radius, 2);
    }

    // Overloaded function to calculate area of a triangle
    double calculateArea(double base, double height, bool isTriangle) {
        return 0.5 * base * height;
    }
};

int main() {
    AreaCalculator calculator;

    double rectangleLength = 5.0;
    double rectangleWidth = 3.0;
    double circleRadius = 4.0;
    double triangleBase = 6.0;
    double triangleHeight = 2.5;

    cout << "Area of rectangle: " << calculator.calculateArea(rectangleLength, rectangleWidth) << endl;
    cout << "Area of circle: " << calculator.calculateArea(circleRadius) << endl;
    cout << "Area of triangle: " << calculator.calculateArea(triangleBase, triangleHeight, true) <<endl;

    return 0;
}

Which gives an output of

Area of rectangle: 15
Area of circle: 50.2655
Area of triangle: 7.5

Run time polymorphism

Run time polymorphism, also known as method overriding, occurs when a function call to an overridden method is resolved at runtime. This is typically achieved using inheritance and virtual functions in C++. When a base class declares a method as virtual, derived classes can override this method to provide specific implementations.

Here is an example demonstrating run time polymorphism in C++:

#include <iostream>
using namespace std;

// Base class
class Shape {
public:
    // Virtual function
    virtual double calculateArea() {
        return 0;
    }
};

// Derived class for Rectangle
class Rectangle : public Shape {
private:
    double length;
    double width;
public:
    Rectangle(double l, double w) : length(l), width(w) {}

    // Overriding the base class method
    double calculateArea() override {
        return length * width;
    }
};

// Derived class for Circle
class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}

    // Overriding the base class method
    double calculateArea() override {
        return 3.14159 * radius * radius;
    }
};

// Derived class for Triangle
class Triangle : public Shape {
private:
    double base;
    double height;
public:
    Triangle(double b, double h) : base(b), height(h) {}

    // Overriding the base class method
    double calculateArea() override {
        return 0.5 * base * height;
    }
};

int main() {
    // Creating objects of derived classes
    Shape* shape1 = new Rectangle(5.0, 3.0);
    Shape* shape2 = new Circle(4.0);
    Shape* shape3 = new Triangle(6.0, 2.5);

    // Calling the overridden methods
    cout << "Area of rectangle: " << shape1->calculateArea() << endl;
    cout << "Area of circle: " << shape2->calculateArea() << endl;
    cout << "Area of triangle: " << shape3->calculateArea() << endl;

    // Cleaning up
    delete shape1;
    delete shape2;
    delete shape3;

    return 0;
}

Which gives an output of:

Area of rectangle: 15
Area of circle: 50.2655
Area of triangle: 7.5

In this example, the Shape class has a virtual function calculateArea(), which is overridden in the derived classes Rectangle, Circle, and Triangle. The overridden methods are called at runtime based on the type of object, demonstrating run time polymorphism.

How it all connects?

While abstraction does involve hiding certain information from the end users, it solely is not connected to encapsulation. Being itself an important pillar, it has its connection to method overriding as well. The prime way of connecting it, is via the concepts of the abstract class, where not only we abstract the methods and pass them while executing code, but also the data to those methods are passed at run time and hence are hidden!

Also, in part 1 where we saw the benefits of using composition over inheritance, it kind of deemed inheritance as unessential. However , with the principle behind creating interfaces, the concept of abstract classes, are useful and well implemented.
A common use case is when, a particular subclass, does not require all of the methods extended by their parent classes, and needs to override a few methods.

Connecting the four pillars of OOPs are essential in understanding how it all works and what can you do to optimize your applications!