Sitemap
Tazbinur

Tazbinur

AI-First Full-Stack Engineer · Shipping AI Agents to 20K+ Users · LLM Apps, RAG & Voice Agents · Backend & AI Infrastructure

Object Oriented Programming & Its Four Pillars

8 min readApr 15, 2024

--

As a software engineer or a software engineering student studying in college or university or whoever is related to software engineering and coding must have heard the very familiar word OOP which stands for Object Oriented Programming. This is indeed a very important concept that we software engineers use to build software. OOP is a set of instructions or principles that design & organise your codebase with instances of objects made from classes. Let’s discuss the four pillars of OOP.

Press enter or click to view image in full size
Photo by Safar Safarov on Unsplash

1. Abstraction:

Abstraction is the concept of hiding the detailed implementation of actions or data but providing only some specific interface so that the user can interact with the object easily. Let’s understand with some examples:

public class BankAccount {

private double balance;

public void checkBalance() {
// verify account holder information
// get the balance information
// show the balance
}

public void deposit(double amount) {
// verify account holder information
// verify deposite amount
// increase balance
}

public void withdraw(double amount) {
// verify account holder information
// verify deposite amount
// decrease balance
}
}

Here we have a BankAccount class and it will have three methods to interact with such as checkBalance, deposit and withdraw to check balance, deposit and withdraw money from the account. Now for each action, there are multiple steps including verification and account operations, from bank to bank these steps may vary. Now as a user, he doesn’t need to know what is going on behind each action, he does not need to know how information is being verified, how the bank is keeping track of my account information and balance or how they are making the transactions. Such complex implementations are hidden from the user only providing him with three methods to call for each action and other business logic is taken care of inside those methods. This information or implementation hiding is known as abstraction in OOP.

2. Encapsulation:

Encapsulation is the process of combining data & methods in a single unit providing limited access of data and methods to the outside world. It restricts or exposes the data & methods using access modifiers so that only limited data or methods can be accessed from the outside world. This is done to make a proper flow of control over the whole object. Let’s understand with an example:

class BankAccount {
private double balance;

public BankAccount() {
this.balance = 0;
}

public double checkBalance() {
return balance;
}

public void deposit(double amount) {
if (amount < 0) {
throw new IllegalArgumentException("Deposit amount can't be negative");
}
balance += amount;
}

public void withdraw(double amount) {
if (amount < 0) {
throw new IllegalArgumentException("Withdraw amount can't be negative");
}
if (amount > balance) {
throw new IllegalArgumentException("Insufficient amount");
}
balance -= amount;
}
}

public class Main {
public static void main(String[] args) {
BankAccount myBankAccount = new BankAccount();

System.out.println("My account balance is " + myBankAccount.checkBalance() + " tk");
// output: My account balance is 0.0 tk

myBankAccount.deposit(100);

myBankAccount.withdraw(50);

System.out.println("My account's new balance is " + myBankAccount.checkBalance() + " tk");
// output: My account's new balance is 50.0 tk
}
}

Here we can see that the BankAccount class has three methods to expose to the outside world which are checkBalance, deposit and withdraw amount. Internally the class maintains a balance to store the updated account balance after each deposit or withdrawal. But this balance is not exposed to the outside world restricting it only within the object of this class so that no one can directly change the balance from outside breaking the business logic of deposit or withdraw resulting in unrealistic information. Thus encapsulation binds the data & methods together and ensures that information is passed and changed in a controlled way.

3. Inheritance:

This is a mechanism in OOP that allows one class (child class) to be created from another class (parent class) and as a result child class automatically gets some (public and protected) data & method of the parent class. Along with these, the child class can also have its property and methods defined in itself as what will be available via the object of that child class. This helps reduce code duplicacy removing the same business logic and making a cleaner codebase. With this, a hierarchical relationship is created between the parent & child class. Let’s look into the example from the previous bank account one: suppose we want some other type of bank account called personal bank account and business bank account which will include respectively personal information and business information. We can achieve these using inheritance like this:

class BankAccount {
private double balance;

public BankAccount() {
this.balance = 0.0;
}

public double checkBalance() {
return balance;
}

public void deposit(double amount) {
if (amount < 0) {
throw new IllegalArgumentException("Deposit amount can't be negative");
}
balance += amount;
}

public void withdraw(double amount) {
if (amount < 0) {
throw new IllegalArgumentException("Withdraw amount can't be negative");
}
if (amount > balance) {
throw new IllegalArgumentException("Insufficient amount for withdrawal");
}
balance -= amount;
}
}

class PersonalBankAccount extends BankAccount {
private String firstName;
private String lastName;

public PersonalBankAccount(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

public String getPersonFullName() {
return firstName + " " + lastName;
}
}

class BusinessBankAccount extends BankAccount {
private String businessName;

public BusinessBankAccount(String businessName) {
this.businessName = businessName;
}

public String getBusinessName() {
return businessName;
}
}

public class Main {
public static void main(String[] args) {

PersonalBankAccount myPersonalBankAccount = new PersonalBankAccount("Md", "Tazbinur");
myPersonalBankAccount.deposit(200.0);
System.out.println("My personal bank account balance is " + myPersonalBankAccount.checkBalance() + " tk");
// output: My personal bank account balance is 200.0 tk

BusinessBankAccount myBusinessBankAccount = new BusinessBankAccount("Tazbinur Academy");
myBusinessBankAccount.deposit(50.0);
myBusinessBankAccount.withdraw(30.0);
System.out.println("My business bank account balance is " + myBusinessBankAccount.checkBalance() + " tk");
// output: My business bank account balance is 20.0 tk
}
}

As you can see we’ve inherited the BankAccount class using the extend keyword. We didn’t define the account’s checkBalance, deposit and withdraw method but we can access those using the child class.

4. Polymorphism:

The word polymorphism means something that can have many forms or types. In OOP the polymorphism refers to any interface which can have multiple behaviour depending upon different situations. For example, let’s take the previous personal & business bank account example:

class BankAccount {
private double balance;
protected int dailyWithdrawCount;

public BankAccount() {
this.balance = 0.0;
this.dailyWithdrawCount = 0;
}

public double checkBalance() {
return balance;
}

public void deposit(double amount) {
if (amount < 0) {
throw new IllegalArgumentException("Deposit amount can't be negative");
}
balance += amount;
}

public void withdraw(double amount) {
if (hasReachedDailyWithdrawLimit()) {
throw new IllegalArgumentException("You reached maximum number of withdraw today");
}
if (amount < 0) {
throw new IllegalArgumentException("Withdraw amount can't be negative");
}
if (amount > balance) {
throw new IllegalArgumentException("Insufficient amount");
}
balance -= amount;
dailyWithdrawCount++;
}

public boolean hasReachedDailyWithdrawLimit() {
if (dailyWithdrawCount >= 1) {
return true;
}
return false;
}
}

class PersonalBankAccount extends BankAccount {
public boolean hasReachedDailyWithdrawLimit() {
if (dailyWithdrawCount >= 2) {
return true;
}
return false;
}
}

class BusinessBankAccount extends BankAccount {
public boolean hasReachedDailyWithdrawLimit() {
if (dailyWithdrawCount >= 3) {
return true;
}
return false;
}
}

public class Main {
public static void main(String[] args) {
PersonalBankAccount myPersonalBankAccount = new PersonalBankAccount();
myPersonalBankAccount.deposit(200);
myPersonalBankAccount.withdraw(30); // no exception
myPersonalBankAccount.withdraw(30); // no exception
myPersonalBankAccount.withdraw(30); // throws withdraw limit exception

BusinessBankAccount myBusinessBankAccount = new BusinessBankAccount();
myBusinessBankAccount.deposit(200);
myBusinessBankAccount.withdraw(30); // no exception
myBusinessBankAccount.withdraw(30); // no exception
myBusinessBankAccount.withdraw(30); // no exception
myBusinessBankAccount.withdraw(30); // throws withdraw limit exception
}
}

Here we’ve introduced a hasReachedDailyWithDrawLimit method for both personal & business accounts having different limits. We can see the name & signature of this method is the same but its behaviour is different based on the object of the class they belong to. Here this method has different forms for different classes.

Compile time & Run time polymorphism:

There are two types of polymorphism such as Compile-time and Run-time polymorphism. The compile time polymorphism occurs when the correct binding occurs which compiles the code and this is achieved through method overloading. On the other hand run time polymorphism occurs when the correct binding is determined while running the code determining which method to call and it is achieved through method overriding.

Get Tazbinur’s stories in your inbox

Join Medium for free to get updates from this writer.

Method overloading is a process when the same class has multiple methods with the same name but have different parameters signatures or return types. Here is an example of this:

public class BankAccount {
private double balance;
private double withdrawCharge = 2.0;

public BankAccount() {
balance = 0.0;
}

public void deposit(double amount) {
balance += amount;
System.out.println("The new balance is: " + balance + " tk");
}

public void withdraw(double amount) {
balance -= amount;
System.out.println("The new balance is: " + balance + " tk");
}

public void withdraw(double amount, boolean applyCharge) {
balance -= amount;
if (applyCharge) {
balance -= withdrawCharge;
}
System.out.println("The new balance is: " + balance + " tk");
}

public static void main(String[] args) {
BankAccount myBankAccount = new BankAccount();

myBankAccount.deposit(100.0);
// output: The new balance is: 100 tk

myBankAccount.withdraw(30.50, false);
// output: The new balance is: 69.5 tk

myBankAccount.withdraw(20.25, true);
// output: The new balance is: 47.25 tk
}
}

Here we can see we have two withdraw methods but they have different parameter numbers and they have different logic as well. Instead of this, we can call with the same name and which one should be called is determined at the compile time.

Method overriding is the process where the child class can have the same method name which exists in the parent class but its implementation can be different in the child class. The condition here is that the method’s name, return type, and parameters must be the same. We can look at the example here:

class BankAccount {
protected double balance;

public BankAccount() {
balance = 0.0;
}

public double checkBalance() {
return balance;
}

public void deposit(double amount) {
if (amount < 0) {
throw new IllegalArgumentException("Deposit amount can't be negative");
}
balance += amount;
}

public void withdraw(double amount) {
if (amount < 0) {
throw new IllegalArgumentException("Withdrawal amount can't be negative");
}
if (amount > balance) {
throw new IllegalArgumentException("Insufficient amount");
}
balance -= amount;
}
}

class PersonalBankAccount extends BankAccount {
@Override
public void withdraw(double amount) {
if (amount < 0) {
throw new IllegalArgumentException("Withdrawal amount can't be negative");
}
if (amount > 1000) {
throw new IllegalArgumentException("More than 1000 tk can't be withdrawn at once from personal bank account");
}
if (amount > balance) {
throw new IllegalArgumentException("Insufficient amount");
}
balance -= amount;
}
}

public class Main {
public static void main(String[] args) {
BankAccount myBankAccount = new BankAccount();
myBankAccount.deposit(2000);
myBankAccount.withdraw(1500);

PersonalBankAccount myPersonalBankAccount = new PersonalBankAccount();
myPersonalBankAccount.deposit(2000);
myPersonalBankAccount.withdraw(1500); // throws limit exception
}
}

Here we can see that the parent class has a withdraw method but the child class redefined the same methods with the same signature but with different logic implementations. Despite having the same method name & signature the correct method to be called is determined at the run time.

These are the four pillars of object-oriented programming in a very short note. A good understanding of these concepts will help with writing and maintaining well-structured code for scalable software.

Thanks for reading till the end, if you liked this article give some claps 👏 to it & feel free to connect with me to stay updated on future articles!

Email: mdtazbinur@gmail.com
LinkedIn: https://www.linkedin.com/in/tazbinur/
Github: https://github.com/tazbin

--

--

Tazbinur
Tazbinur

AI-First Full-Stack Engineer · Shipping AI Agents to 20K+ Users · LLM Apps, RAG & Voice Agents · Backend & AI Infrastructure