There are four fundamental principles of OOP: Abstraction, Encapsulation, Inheritance, and Polymorphism. These principles provide advantages like modularity, code reusability, extensibility, data hiding, etc.
In this blog, we will learn how these OOP principles work together to make a well-designed application.
Once upon a time in the world of object-oriented programming, Mr Rahul (product manager) was given the task to create a website for CarsInfo (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 OOP 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 {
private String model;
private Engine engine;
private float horsepower;
private float mileage;
private float price;
public Car(String model) {
this.model = model;
}
public void renderCar() {
if (model.equals("Hyundai i20"))
renderHyundaiI20();
else if (model.equals("Mahindra Bolero"))
renderMahindraBolero();
}
private void renderMahindraBolero() {
// Logic for rendering Mahindra Bolero
}
private void renderHyundaiI20() {
// Logic for rendering Hyundai i20
}
// Getters and setters for the private fields
// ...
}
class Main {
public static void main(String[] args) {
Car mahindraBolero = new Car("Mahindra Bolero");
Car hyundaiI20 = new Car("Hyundai i20");
mahindraBolero.renderCar();
hyundaiI20.renderCar();
}
}
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 {
private Engine engine;
private float horsePower;
private float mileage;
private float price;
public void render() {
// Logic for rendering Mahindra Bolero
}
}
class HyundaiI20 {
private Engine engine;
private float horsePower;
private float mileage;
private float price;
public void render() {
// Logic for rendering Hyundai i20
}
}
.....
// sample usage
class Main {
public static void main(String[] args) {
MahindraBolero mahindraBolero = new MahindraBolero();
HyundaiI20 hyundaiI20 = new HyundaiI20();
mahindraBolero.render();
hyundaiI20.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."
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() {
if(model.equals("Mahindra Bolero")) {
// Call method to play Mahindra Bolero sound
}
else if(model.equals("Hyundai i20")) {
// Call method to play Hyundai i20 sound
}
}
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 OOP" and his final submitted code. Note: We have provided the final sample of Mohan's code in the inheritance section below.
Abstraction helps 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.
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 use the power of abstract classes, think about extension rather than initialization." - The wise developer replied.
Sample example of Java abstract class:
abstract class Car {
....
public abstract void start(String key);
public abstract void accelerate();
public abstract void decelerate();
public abstract int getTopSpeed();
public void applyBrakes() {
// Default implementation for applying brakes
}
public void stop() {
// Default implementation for stopping the car
}
....
}
Sample example of Java interface:
public interface Car {
void start(String key);
void stop();
void accelerate();
void decelerate();
int getTopSpeed();
void applyBrakes();
}
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:
Explore the blog: Abstraction in OOPS.
Encapsulation 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 car example, if we directly expose 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?"
The wise developer: "We can use access modifiers to achieve encapsulation. Access modifiers specify the accessibility or scope of a field, method, constructor, or class."
The concept of getters and setters is a common 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. By using this mechanism, an object can maintain control over how its data can be changed.
In OOP, encapsulation and abstraction are closely related concepts. Encapsulation bundle data and methods that operate on that data within a single unit, or object. Abstraction, on the other hand, exposes only the necessary information and hides 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 helps 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 refer to the shared properties of millions of different cars, even though each car may have its own unique traits.
When a class or system has no access boundaries, it is "open for modification," which can lead to unintended side effects. So, encapsulation adds boundaries to a system and provides better control. This helps us to protect the integrity of the system and prevent unwanted changes.
Explore the blog: Encapsulation in OOPS.
In object-oriented programming, Inheritance is a mechanism 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. In this way, the subclass can 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;
// ... other common attributes
public abstract void render();
public abstract void playSound();
public void setMileage(float mileage) {
if (mileage <= 0) {
System.err.println("Mileage cannot be negative or zero!");
} else {
this.mileage = mileage;
}
}
// ... other common methods
public float getMileage() {
return this.mileage;
}
// ... other getters and setters
}
class MahindraBolero extends Car {
public void render() {
// logic for rendering Mahindra Bolero
}
public void playSound() {
// logic for playing sound
}
}
class HyundaiI20 extends Car {
public void render() {
// logic for rendering Hyundai I20
}
public void playSound() {
// logic for playing I20 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 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 design more specific classes that are subclasses of the superclass. These subclasses will now inherit the members of the superclass and use them as if they were its own members. This approach makes it easier to reuse and extend code since you can use the inherited members in the subclass without the need 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."
Explore the blog: Inheritance in OOPS.
In object-oriented programming, Polymorphism is 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. Due to this, the same method will behave differently based on the number and type of arguments provided.
This is known as Compile-time Polymorphism (method overloading) because the decision of which method to invoke is made at compile time, rather than at runtime.
Suppose there is a "Shape" class with a method area(). You can create multiple implementations of this method to calculate the area of different shapes (each with a different set of arguments). When we call the "area" method on an object of the "Shape" class, the correct implementation will be invoked based on the arguments provided. This will help us to use the same method name in different contexts and make our code more flexible and reusable.
Runtime Polymorphism (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 provide its own implementation of a method that is defined in the superclass or base class. When you call a method on an object of the subclass, the subclass's implementation of the method will be invoked, rather than the implementation in the superclass.
Mohan's code includes several concrete classes that each provide their own implementation of the render() method. This is "method overriding," where we are replacing the behaviour 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 type of object instance.
Explore the blog: Compile and Runtime Polymorphism in Java.
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!