thecodex.expert  ·  The Codex Family of Knowledge
Programming Paradigms

Object-Oriented Programming

The paradigm that organises code around objects — entities that hold data and know how to act on it — and models programs as conversations between objects.

Paradigms Requires: Procedural ~24 min read Last verified:
Canonical Definition

Object-oriented programming (OOP) is a programming paradigm that organises software around objects — entities that bundle related data (attributes) and behaviour (methods) — and models programs as networks of interacting objects, structured using the four principles of encapsulation, abstraction, inheritance, and polymorphism.

One sentence that captures it

Object-oriented programming is modelling your program the same way you model the real world: as a collection of things that have properties and know how to do certain actions.

Objects: data and behaviour, together

A bank account has a balance and an owner. It can accept deposits, process withdrawals, and report its current balance. In procedural programming, the balance would be a variable, and the deposit and withdrawal logic would be separate functions. In OOP, all of that belongs together in one unit: an object.

A class is the blueprint. An object is an instance — a specific account created from that blueprint. The class BankAccount defines what every account has and can do. Each actual account (priya_account, rahul_account) is a separate object with its own data, but all use the same methods defined in the class.

Python bank_account.py
class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner       # attribute: who owns this account
        self.balance = balance   # attribute: current balance

    def deposit(self, amount):   # method: behaviour
        self.balance += amount
        return self.balance

    def withdraw(self, amount):  # method: behaviour
        if amount > self.balance:
            raise ValueError("Insufficient funds")
        self.balance -= amount
        return self.balance

# Two objects from the same class blueprint
priya = BankAccount("Priya", 1000)
rahul = BankAccount("Rahul", 500)

priya.deposit(200)   # priya.balance is now 1200
rahul.withdraw(100)  # rahul.balance is now 400
# priya and rahul are independent objects

Encapsulation: hiding what doesn't need to be seen

Encapsulation means bundling data and methods together, and controlling which parts of the code outside the object can see and modify the data. The balance of a bank account should only be changed through deposit() and withdraw() — not by any code that can directly set account.balance = 9999999.

Languages enforce this through access modifiers: private (Java, C++) hides a field from outside the class entirely. protected allows access from subclasses. Python uses a naming convention: _balance signals "don't touch this from outside" (a warning, not a hard restriction). Kotlin and Swift have internal, visible within the same module.

Inheritance: sharing and specialising

A SavingsAccount is a kind of BankAccount that also earns interest. Rather than rewriting all the account logic, SavingsAccount can inherit from BankAccount — it gets all the existing methods and attributes for free, and adds or overrides only what's different.

Python inheritance.py
class SavingsAccount(BankAccount):  # inherits from BankAccount
    def __init__(self, owner, balance=0, interest_rate=0.04):
        super().__init__(owner, balance)  # call parent's __init__
        self.interest_rate = interest_rate

    def add_interest(self):  # new method — not in BankAccount
        interest = self.balance * self.interest_rate
        self.deposit(interest)  # reuses inherited deposit()
        return interest

savings = SavingsAccount("Priya", 10000, 0.05)
savings.deposit(500)   # inherited method works directly
savings.add_interest() # new method specific to SavingsAccount

Polymorphism: one interface, many forms

Polymorphism means the same operation can behave differently depending on what object it is applied to. If you have a list of different bank account types — checking, savings, fixed deposit — and you call account.get_interest() on each, each account type can implement that method differently. The caller doesn't need to know or care which specific type it's dealing with.

This is what makes OOP powerful for large systems: you can write code that works with a general type (any BankAccount) and it will automatically do the right thing for each specific subtype, without if/else chains checking the type.

Abstraction: simplifying complexity

Abstraction means exposing only what is necessary and hiding the implementation details. When you call priya.withdraw(200), you don't need to know how the withdrawal is implemented — whether it logs to a database, checks multiple conditions, or updates several internal fields. The method is your interface; what happens inside is hidden.

Abstraction lets systems grow. A bank system can have hundreds of account types; code that works with the abstract BankAccount interface works with all of them.

Where OOP came from

OOP originated with Simula 67 (Ole-Johan Dahl and Kristen Nygaard, Norwegian Computing Centre, 1967) — designed to simulate discrete events like ships entering a harbour. The insight: model the simulation entities directly as program objects. Alan Kay extended these ideas at Xerox PARC in the early 1970s, creating Smalltalk — the first purely OOP language. Kay coined the term "object-oriented." C++ (Bjarne Stroustrup, 1979) brought OOP to systems programming. Java (Sun Microsystems, 1995) made OOP the dominant enterprise paradigm.

The four principles in depth

Encapsulation

Encapsulation serves two purposes: data hiding (preventing invalid state by controlling how data is modified) and information hiding (reducing coupling between components by hiding implementation details). These are related but distinct. Data hiding prevents bugs; information hiding enables change — if clients only access an object through its public interface, the implementation can be completely rewritten without breaking the clients.

Inheritance and the Liskov Substitution Principle

Inheritance creates an is-a relationship: a SavingsAccount is a BankAccount. The Liskov Substitution Principle (Barbara Liskov, 1987, formalised 1994) states: if S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any desirable properties of the program. In practice: a subclass must honour all the contracts of its superclass — it can extend behaviour, but must not violate it.

Violation example: if BankAccount.withdraw() guarantees the balance never goes negative, and OverdraftAccount overrides it to allow negative balances, code that assumes balances are non-negative will break when given an OverdraftAccount. This is a LSP violation.

Polymorphism: subtype vs. parametric

Subtype polymorphism (runtime polymorphism) is the classic OOP kind: a method call on a base class reference dispatches to the correct subclass method at runtime, via a vtable (virtual function table) in C++ or equivalent mechanism. This is what virtual methods in C++ and all method overriding in Java achieve.

Parametric polymorphism (generics) is a different dimension: a single piece of code works for any type. Java generics (List<T>), C++ templates, Rust generics, and TypeScript generics are all parametric polymorphism. Not specific to OOP — functional languages use it extensively.

Interfaces and abstract classes

Abstraction is concretely implemented through interfaces (in Java, Go, TypeScript) and abstract classes. An interface declares what methods a type must have, without specifying how. Any class that implements the interface can be used wherever the interface is expected. Go's interfaces are structural (implicit) — any type with the right methods satisfies the interface automatically, without declaring it.

Java polymorphism.java
interface Account {
    void deposit(double amount);
    double withdraw(double amount);
    double getBalance();
}

class SavingsAccount implements Account {
    private double balance;
    public void deposit(double amount) { balance += amount; }
    public double withdraw(double amount) {
        if (amount > balance) throw new IllegalArgumentException();
        balance -= amount; return balance;
    }
    public double getBalance() { return balance; }
}

// This method works with ANY Account — polymorphism
void processAccounts(List<Account> accounts) {
    for (Account account : accounts) {
        account.deposit(100); // correct method called per type
    }
}

Composition over inheritance

Deep inheritance hierarchies are a recognised design problem — they create tight coupling, make code hard to follow, and violate the Liskov principle when inheritance is used for code reuse rather than genuine is-a relationships. The Gang of Four design patterns book (Gamma et al., 1994) explicitly advises: "favour object composition over class inheritance." Composition uses has-a relationships: a Car has an Engine rather than inheriting from Engine. The engine can be swapped; the car delegates engine-related behaviour to its engine object. This produces more flexible, maintainable designs.

Prototype-based OOP

JavaScript does not use classes in the same way Java does. JavaScript's original OOP model is prototype-based: objects inherit directly from other objects via a prototype chain, without a separate class structure. class syntax in JavaScript (introduced in ES6 / ECMAScript 2015) is syntactic sugar over the prototype chain — the underlying mechanism is unchanged. This matters when debugging prototype chain lookups or understanding how this binding works.

Commonly confused
OOP is not about classes. OOP is about the four principles — encapsulation, abstraction, inheritance, and polymorphism. These can be implemented without classes (JavaScript's prototype model, Go's interfaces). A class is the most common implementation mechanism, not the definition of the paradigm. Alan Kay's original Smalltalk OOP was centred on message-passing between objects, not class hierarchies.
Inheritance is not the most important OOP feature. In practice, deep inheritance hierarchies cause more problems than they solve. Composition is preferred for code reuse; inheritance is appropriate only for true is-a relationships. The Gang of Four's primary advice was: favour composition over inheritance.
Python classes are not enforcing encapsulation at the language level. Python has no private keyword. The convention _name signals "don't use this outside the class" but cannot prevent it. __name (name mangling) makes external access harder but not impossible. Python relies on convention and programmer discipline for encapsulation, not language enforcement.
How this connects
Enables

The origin of OOP: Simula and Smalltalk

Simula 67 (Dahl & Nygaard, 1967) introduced classes, objects, inheritance, and virtual procedures. It was designed as a simulation language — entities in a simulation (ships, queues, processes) mapped directly to objects in the program. This physical modelling intuition remains the best way to identify good OOP boundaries: if an entity in the problem domain has both state and behaviour, it is a candidate for an object.

Alan Kay's Smalltalk (Xerox PARC, 1972–1980) pursued a different vision: extreme uniformity and message passing. In Smalltalk, everything — including integers and booleans — is an object. Sending a message to an object (5 + 3 sends the + message to the object 5) is the only way to cause any effect. Late binding — method dispatch resolved at runtime, never at compile time — was fundamental. Kay later said the most important idea in Smalltalk was message passing, not the object system itself.

Vtables and dynamic dispatch

In C++, virtual method calls are implemented via a vtable (virtual function table). Each class with virtual methods has a vtable — an array of function pointers, one per virtual method. Each object has a hidden pointer to its class's vtable. A virtual method call dereferences the vtable pointer and calls through the appropriate function pointer — two pointer dereferences and an indirect call, compared to a direct call for non-virtual methods. This is the cost of runtime polymorphism in C++: ~1–3 ns on modern hardware, often negligible, but a concern in inner loops of high-performance code.

Java uses a similar mechanism via the Method Table (mtable) in the JVM. Python's method dispatch goes through the __mro__ (Method Resolution Order) — a linearisation of the class hierarchy computed via the C3 algorithm (Barrett et al., 1996) — which handles multiple inheritance in a consistent, predictable way.

Specification reference

Liskov, B. & Wing, J. (1994). "A behavioral notion of subtyping." ACM TOPLAS, 16(6), 1811–1841. — The formal statement of the Liskov Substitution Principle. Gamma, E., Helm, R., Johnson, R. & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. — The definitive OOP design patterns reference. Kay, A. C. (1993). "The Early History of Smalltalk." ACM SIGPLAN Notices, 28(3), 69–95. — Primary source for the origin and intent of OOP.

How this connects
Formal basis
Source confidence: High Last verified: Primary source: Kay, SIGPLAN 1993 · Liskov & Wing, TOPLAS 1994

Sources

1
Kay, A. C. (1993). "The Early History of Smalltalk." ACM SIGPLAN Notices, 28(3), 69–95. — Primary source for the origin, intent, and principles of OOP as conceived by its originator.
2
Liskov, B. & Wing, J. (1994). "A behavioral notion of subtyping." ACM Transactions on Programming Languages and Systems, 16(6), 1811–1841. — Formal statement of the Liskov Substitution Principle.
3
Gamma, E., Helm, R., Johnson, R. & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. — The canonical reference for OOP design patterns; source of "favour composition over inheritance."
4
Dahl, O.-J. & Nygaard, K. (1966). "SIMULA: An ALGOL-based simulation language." Communications of the ACM, 9(9), 671–678. — The paper introducing the class concept and object-oriented simulation.
5
Stroustrup, B. (1994). The Design and Evolution of C++. Addison-Wesley. — Primary reference for C++'s OOP model, vtables, and the design decisions behind virtual dispatch.