Principles of Object Oriented Programming (OOPS)

Abstraction, Encapsulation, Inheritance, and Polymorphism are four fundamental pillars of object-oriented programming. These oops principles provide advantages like modularity, code reusability, feature extensibility, data hiding, etc. In this blog, we will learn how these OOPS principles work together to make a well-designed application.

Let's understand this idea via an interesting story

Once upon a time in the world of object-oriented programming, Mr Rahul (our product manager) was given the task to create a website for CarsInfo.com (a one-stop shop for buying and selling old cars). To encourage healthy competition within the team, he gave this project to two developers, Mohan and Rajesh, who had recently joined the company.

Mohan was an OOPS enthusiast, while Rajesh believed in taking the shortest path to get things done. To make things interesting, Rahul decided to offer a promotion to the developer who will display a 3D view of all the different cars available in their store on their website.

Rajesh: "This is a piece of cake! I just need to create a Car class to store all the properties of a car and a provide method for rendering that data. Look what I have done."

.....
class Car {
    String model;
    Engine engine;
    float horsepower;
    float mileage;
    float price;
    Car (String model) {
        this.model = model;
    }
}

.....

void renderCar(Car car) {
    if(car.model.equals("Hyundai i20"))
        renderHyundaiI20 (car);
    if(car.model.equals("Mahindra Bolero"))
        renderMahindraBolero(car);
}

void renderMahindraBolero(Car car) {
    // logic for rendering mahindra bolero
}

void renderHyundaiI20 (Car car) {
    // logic for rendering hyundai i20
}

.....
// Sample usage
void main() {
    Car mahindraBolero = new Car("Mahindra Bolero");
    Car hyundaiI20 = new Car("Hyundai i20");
    CarRenderer.renderCar(mahindraBolero);
    CarRenderer.renderCar(hyundaiI20);
}

Mohan, meanwhile, thought to himself, “Who are the key players here? How will they evolve in the future?”. So he took a slightly different path and decided to make separate classes for each car. With rendering logic of each car baked into the class itself.

.....
class MahindraBolero {
    Engine engine;
    float horsePower;
    float mileage;
    float price;
    void render();
}

class HyundaiI20 {
    Engine engine;
    float horsePower;
    float mileage;
    float price;
    render();
}
.....
// sample usage
main() {
    MahindraBolero mahindraBolero = new MahindraBolero();
    Hyundai I20 hyundai I20 = new Hyundai I20();
    mahindraBolero.render();
    hyundai I20.render();
}

When Rajesh saw Mohan's code, he laughed at the repetition of similar code in different classes. "There's no way Rahul will approve this," he thought to himself and smiled. The next day, Rahul called both developers to his office. Rajesh anticipated a salary increase, but Rahul had different news. Rahul: "Guys, there's been a change of plans. Our new product designer thinks it would be much cooler if our cars could play sound as well."

The specs changed!

Disappointed, Rajesh ran towards his cubicle to complete this before Mohan. He quickly added another method, playSound(). He tested the changes, submitted his code, and started dreaming about his trip to Thailand after getting promoted.

playSound(Car car) {
    if(car.model.equals("Mahindra Bolero")) {
        System.out.prinln("Honk Honk!");
    }
    else if(car.model.equals("Hyundai i20")) {
        System.out.prinln("Beep Beep!");
    }
} 

After a few days of learning and development, Rahul once again called both into his office. Rahul: "Great work, both of you. After careful consideration, we have decided to promote Mohan to a senior dev position." Rajesh: "What? Mohan had duplicate code in his implementation! How is he being promoted and not me?"

To understand why Mohan was promoted, it's important to know about his encounter with the "four core principles of OOPS" also known as the "four pillars of Object-Oriented Programming."

Abstraction

Abstraction refers to an object's ability to present higher-level functionality to the user while hiding implementation details. For example, when you think about a car, you don't consider the thousands of parts that make it up. Instead, you likely think of it as a well-defined machine with four wheels that can take you from one place to another. Cars come in different makes and models, such as Ferrari, Tesla, or BumbleBee, but they are all still considered "cars." In other words, the implementation of cars may vary, but the abstraction of a "car" remains the same.

Abstraction allows you to program to an interface and hide implementation details. There are several ways to implement abstraction, but the most common ones in Java are through interfaces and abstract classes.

  • Abstraction through abstract classes: This is achieved using the "abstract" keyword with a class. In Java, marking a class as "abstract" means that it cannot be instantiated.
  • Abstraction via Interfaces: Similar to abstract classes, we can use the "interface" keyword to define an interface.

Mohan: "But what's the point of creating a class that can't be instantiated?"

"The point is that we are using an abstract class to define a contract. A contract about what things the class must do, not how those things are done. To utilize the power of abstract classes, think about extension rather than initialization." - The wise developer replied.

abstract class Car {
    start(String key);
    stop();
    accelerate();
    decelerate();
    getTopSpeed();
    applyBreaks();   
}  
interface class Car {
    start(String key);
    stop();
    accelerate();
    decelerate();
    getTopSpeed();
    applyBreaks();  
}   

Both abstract classes and interfaces can be used to achieve abstraction in Java, but they have some differences. The main reason interfaces exist is to allow an object to implement multiple abstractions, since Java does not allow a class to extend more than one class due to issues with multiple inheritance. Other differences between abstract classes and interfaces include:

  • Type of methods: Interfaces can only have abstract methods, while abstract classes can have both abstract and non-abstract methods.
  • Type of variables: Abstract classes can have final, non-final, static, and non-static variables, while interfaces only have static and final variables.
  • Accessibility of data members: Members of a Java interface are public by default, while a Java abstract class can have private, protected, and other access modifiers.

Encapsulation

Encapsulation example in oops

Encapsulation in OOP is the ability of a system to hide information in such a way that it cannot be accessed or manipulated directly by other entities. Mohan: "But why would someone want that? If I hide something inside an object, what's the point of creating that attribute in the first place?"

"Encapsulation allows us to have better control and avoid unpredictable state changes to your system. For example, in the previous car example, we directly exposed our engine object, don't you think anyone could call engine.ignite() without the keys?" - The wise developer replied.

Mohan: "Yeah, that's true. So how do we use encapsulation in our system?"

Wise developer: "We can use access modifiers to achieve encapsulation in our system."

Access modifiers in Java specify the accessibility or scope of a field, method, constructor, or class.

The concept of getters and setters is a common programming technique used to protect the data within an object. This is done by marking attributes of a class as "private" or "protected" and providing public methods, known as getters and setters, to allow controlled access to these attributes. By using this mechanism, an object can maintain control over how its data can be changed.

Food for Thought: Is class an encapsulation or abstraction?

Encapsulation and abstraction are closely related concepts in OOPS. Encapsulation involves bundling data and methods that operate on that data within a single unit, or object. This can also be referred to as creating a class, as it involves encapsulating data and operations together. Abstraction, on the other hand, involves exposing only the necessary information and hiding the implementation details.

The main difference between these two concepts is the intent behind them. Abstraction is used to simplify and make the interface more user-friendly, while encapsulation is used to control the access and modification of data within an object.

Let's think from another perspective!

Abstraction is a technique that we use in our daily lives to simplify complex systems. It allows us to focus on the general properties of an object or concept, rather than being overwhelmed by the specifics. For example, when we use the abstraction "car," we are able to refer to the shared properties of millions of different cars, even though each car may have its own unique traits.

Encapsulation helps us to better control our systems. When a class or system has no access boundaries, it is "open for modification," which can lead to unintended side effects. Encapsulation adds boundaries to a system, allowing us to better control how data is accessed and modified. This helps to protect the integrity of the system and prevent unwanted changes.


Inheritance

Inheritance is a mechanism in object-oriented programming that allows an object to reuse or extend the functionality of another object. It allows a subclass or derived class to inherit the properties and methods of a superclass or base class, allowing the subclass to have access to the functionality of the superclass.

Rahul: "Rajesh, do you want to know why I promoted Mohan?"

Rajesh: "Yes."

Rahul: "Perhaps you should take a look at the final design that Mohan submitted. His work demonstrates a good understanding of inheritance."

abstract class Car {
    private Engine engine;
    private float horsepower;
    private float mileage;
    private float price;
    
    abstract void render();
    abstract void playSound();
    
    void setMileage(float mileage) {
        if (mileage <= 0) {
            System.err.println("Mileage cannot be negative or zero!");
        } else {
            this.mileage = mileage;
        }
    }
    
    float getMileage() {
        return this.mileage;
    }
    // ... other getters and setters
}

class MahindraBolero extends Car {
    void render() {
        // logic for rendering mahindra bolero
    }
    
    void playSound() {
        // logic for playing sound
    }
}

class HyundaiI20 extends Car {
    void render() {
        // logic for rendering Hyundai I20
    }
    
    void playSound() {
        // logic for playing I20 sound
    }
}

class Breeza extends Car {
    void render() {
        // logic for rendering Breeza
    }
    
    void playSound() {
        // logic for playing breeza specific sound
    }
}

public class Main {
    public static void main(String[] args) {
        ArrayList<Car> cars = // ... initialize car list from backend data
        for (Car car : cars) {
            car.render();
            car.playSound();
        }
    }
}

Rajesh: "How did you come up with this?"

Mohan: "I had realized during my first review that code duplicity was not going to work. So I encapsulated the common properties into a separate class called 'Car' and then used inheritance to share those common properties with all the concrete classes."

When designing with inheritance, you can place common code in a superclass and specify that other, more specific classes are subclasses of the superclass. This allows the subclass to inherit the members of the superclass and use them in a similar way to how it uses its own members. This approach makes it easier to reuse and extend code, as you can use the inherited members in the subclass without having to rewrite them.

Rajesh: "I still don't understand how you're going to render different car models using this approach. I don't see any conditional checks for rendering different cars."

Mohan: "Actually, we don't need those anymore. The Java Virtual Machine (JVM) takes care of invoking the correct render() and playSound() methods based on the instance type. This mechanism is known as polymorphism."

Polymorphism

Polymorphism is a concept in object-oriented programming that refers to the ability of an object to behave differently based on the context of its invocation. In Java, you can create multiple implementations of a method with different arguments, which allows the method to behave differently based on the number and type of arguments provided. This is known as static polymorphism, as the decision of which method to invoke is made at compile time, rather than at runtime.

For example, consider a class called "Shape" that has a method called "area." You could create multiple implementations of this method, each with a different set of arguments, to calculate the area of different shapes. When calling the "area" method on an object of the "Shape" class, the correct implementation would be invoked based on the arguments provided. This allows the same method name to be used in different contexts, making the code more flexible and reusable.

Runtime polymorphism, also known as method overriding, is a form of polymorphism in which the decision of which method to invoke is made at runtime, rather than at compile time. In method overriding, a subclass or derived class can provide its own implementation of a method that is defined in the superclass or base class. When calling the method on an object of the subclass, the subclass's implementation of the method will be invoked, rather than the implementation in the superclass.

For example, consider a class called "Shape" that has a method called "area" You could create multiple subclasses of "Shape," such as "Circle" and "Rectangle," each with its own implementation of the "area" method. When calling the "area" method on an object of the "Shape" class, the correct implementation would be invoked based on the type of object that it is working with. This allows the same method name to be used in different contexts, making the code more flexible and reusable.

Mohan's code includes several concrete classes that each provide their own implementation of the render() method. This is called "method overriding," where we are replacing the behavior of the base class's method with a new implementation. When we call render() on a parent class variable, the Java Virtual Machine (JVM) determines the correct method to call based on the object instance.

Conclusion

Rahul: "It's important to have a strong understanding of the fundamental principles of object-oriented programming as you progress in your career. Not only will it save time and help you adapt to changing business requirements, but it can also have a direct impact on business. I hope that answers your question, Rajesh."

Enjoy learning, enjoy OOPS!

More From EnjoyAlgorithms

© 2022 Code Algorithms Pvt. Ltd.

All rights reserved.