Featured image of post Demystifying Object-Oriented Programming (OOP)

Demystifying Object-Oriented Programming (OOP)

Explore the four pillars of OOP (Encapsulation, Abstraction, Inheritance, and Polymorphism) with practical examples.

Object-Oriented Programming (OOP) is a programming paradigm centered around the concept of objects, which contain data (in the form of fields or attributes) and code (in the form of procedures or methods). It is one of the most popular paradigms used in modern software engineering.

To truly master OOP, we must understand its Four Pillars: Encapsulation, Abstraction, Inheritance, and Polymorphism. Let’s demystify each of them with neat, real-world examples.


1. Encapsulation: Keeping Data Safe

Encapsulation is the bundling of data and the methods that operate on that data into a single unit (a class) while restricting direct access to some of the object’s components.

Think of encapsulation as a protective shield. Instead of letting anyone modify a variable directly, you provide controlled access through public getter and setter methods.

Why use it?

  • Data Validation: You can prevent invalid data from being assigned (e.g., setting a negative age).
  • Flexibility: You can change the internal implementation of a class without breaking external code.

Python Example

In Python, encapsulation is implemented using prefix naming conventions. While attributes are public by default, prefixing an attribute with a double underscore __ triggers name mangling, making it private and harder to access directly from outside the class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class BankAccount:
    def __init__(self, owner, initial_balance):
        self.owner = owner
        self.__balance = initial_balance  # Private attribute (name-mangled)

    # Getter method
    def get_balance(self):
        return self.__balance

    # Setter method with safety validation
    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit amount must be positive")
        self.__balance += amount

Java Example

In Java, encapsulation is strictly enforced using access modifiers. We mark fields as private and expose them only through public getter and setter methods.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class BankAccount {
    private String owner;
    private double balance; // Private field

    public BankAccount(String owner, double initialBalance) {
        this.owner = owner;
        this.balance = initialBalance;
    }

    // Getter method
    public double getBalance() {
        return this.balance;
    }

    // Setter method with validation
    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Deposit amount must be positive");
        }
        this.balance += amount;
    }
}

2. Abstraction: Hiding Complexity

Abstraction is the concept of hiding complex implementation details and showing only the essential features of an object. It reduces complexity and increases usability.

Consider a car: you only need to know how to use the steering wheel, accelerator, and brakes to drive it. You do not need to understand how the internal combustion engine or fuel injection system works.

Why use it?

  • Simplifies Interface: Users interact with a clean, simple API.
  • Isolates Changes: The underlying complex mechanism can be updated without the user needing to change how they interact with it.

Python Example

In Python, abstraction is achieved using the abc (Abstract Base Classes) module to define abstract classes and methods.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from abc import ABC, abstractmethod

class PaymentGateway(ABC):
    @abstractmethod
    def process_payment(self, amount: float) -> bool:
        pass

class StripePayment(PaymentGateway):
    def process_payment(self, amount: float) -> bool:
        # Complex API call to Stripe servers, token verification, etc.
        print(f"Processing Stripe payment of ${amount}")
        return True

Java Example

In Java, abstraction is achieved using interfaces or abstract classes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public interface PaymentGateway {
    boolean processPayment(double amount);
}

public class StripePayment implements PaymentGateway {
    @Override
    public boolean processPayment(double amount) {
        // Complex API call to Stripe servers, token verification, etc.
        System.out.println("Processing Stripe payment of $" + amount);
        return true;
    }
}

3. Inheritance: Reusing Code

Inheritance is a mechanism where a new class (subclass/child) inherits properties and behaviors (methods) from an existing class (superclass/parent).

It promotes the DRY (Don’t Repeat Yourself) principle by allowing developers to write common code once and reuse it across multiple specific entities.

Why use it?

  • Code Reusability: Avoid duplicating fields and methods.
  • Logical Hierarchy: Models relationships cleanly (e.g., a Dog is an Animal).

Python Example

Python supports both single and multiple inheritance using a comma-separated list in the class definition.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Vehicle:
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model

    def start_engine(self):
        print(f"The engine of {self.brand} {self.model} is starting...")

class ElectricCar(Vehicle):
    def __init__(self, brand, model, battery_capacity):
        super().__init__(brand, model)
        self.battery_capacity = battery_capacity

    def charge(self):
        print("Charging battery...")

Java Example

Java supports single class inheritance using the extends keyword. Multiple inheritance is achieved using interfaces.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Vehicle {
    protected String brand;
    protected String model;

    public Vehicle(String brand, String model) {
        this.brand = brand;
        this.model = model;
    }

    public void startEngine() {
        System.out.println("The engine of " + brand + " " + model + " is starting...");
    }
}

public class ElectricCar extends Vehicle {
    private double batteryCapacity;

    public ElectricCar(String brand, String model, double batteryCapacity) {
        super(brand, model);
        this.batteryCapacity = batteryCapacity;
    }

    public void charge() {
        System.out.println("Charging battery...");
    }
}

4. Polymorphism: Many Forms

Polymorphism means “many forms.” In OOP, it allows objects of different classes to be treated as objects of a common superclass, and behave differently based on their actual type.

For example, a draw() method would behave differently depending on whether it is called on a Circle, a Square, or a Triangle.

Why use it?

  • Extensibility: You can add new classes that conform to the interface without changing the calling code.
  • Clean Code: Replaces verbose conditional blocks (if-else / switch) with clean, dynamic method dispatch.

Python Example

Python’s dynamic nature implements polymorphism natively. We can pass any object that implements a specific method (often called duck typing).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Animal:
    def make_sound(self):
        pass

class Dog(Animal):
    def make_sound(self):
        return "Woof!"

class Cat(Animal):
    def make_sound(self):
        return "Meow!"

# Polymorphism in action
def play_sound(animal: Animal):
    print(animal.make_sound())

play_sound(Dog())  # Outputs: Woof!
play_sound(Cat())  # Outputs: Meow!

Java Example

In Java, polymorphism is checked at compile-time and resolved dynamically at runtime through method overriding.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public abstract class Animal {
    public abstract String makeSound();
}

public class Dog extends Animal {
    @Override
    public String makeSound() {
        return "Woof!";
    }
}

public class Cat extends Animal {
    @Override
    public String makeSound() {
        return "Meow!";
    }
}

// Polymorphism in action
public class Test {
    public static void playSound(Animal animal) {
        System.out.println(animal.makeSound());
    }

    public static void main(String[] args) {
        playSound(new Dog()); // Outputs: Woof!
        playSound(new Cat()); // Outputs: Meow!
    }
}

Limitations of OOP

While Object-Oriented Programming is incredibly powerful, it has certain limitations depending on the programming language:

Limitations in Java

  1. Verbosity and Boilerplate: Java is known for requiring substantial boilerplate code (getters, setters, constructors, class structures) for simple operations.
  2. Single Inheritance Restrictions: Java does not allow multiple class inheritance to prevent the “Diamond Problem.” You must use interfaces or composition, which can occasionally complicate design.
  3. No Top-Level Functions: Everything in Java must reside inside a class. Static utility scripts (like math helpers) must be wrapped in classes, which goes against purely procedural or functional styles.
  4. Memory/Execution Overhead: Everything being an object leads to high memory utilization (metadata headers for objects) and increased load on the Garbage Collector (GC).

Limitations in Python

  1. Lack of Strict Encapsulation: Python doesn’t support true private access modifiers. The double-underscore notation only delays direct modifications; name mangling is easily bypassed. This relies on the “we are all consenting adults here” convention.
  2. Performance Penalties: Dynamic dispatch and dynamic type-checking add runtime overhead. Attribute resolution checks the object’s dictionary at runtime, which is slower than Java’s vtable lookup.
  3. Complex Multiple Inheritance: Managing Method Resolution Order (MRO) and super() sequences can lead to subtle diamond-inheritance errors if not carefully designed.
  4. Runtime Mutability Risks: Python classes and objects can be dynamically updated at runtime (monkey-patching). This makes debugging difficult and introduces instability if third-party modules modify base classes unpredictably.

Conclusion

Understanding the four pillars of OOP is essential for designing scalable, maintainable, and robust systems:

  1. Encapsulation protects data integrity.
  2. Abstraction simplifies developer interactions.
  3. Inheritance removes redundancy.
  4. Polymorphism introduces flexibility and custom execution flow.

By knowing both the advantages and limitations of OOP in languages like Java and Python, you can make informed decisions about when to use object-oriented patterns or shift to other paradigms like functional programming. Happy coding!

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy