Last updated: 2026-09-23
OOP Fundamentals
What is Object-Oriented Programming?
OOP is a programming paradigm based on objects — data structures containing data (attributes) and code (methods). The term itself dates to the mid-1960s: Alan Kay, developing Smalltalk, later recalled coining "object-oriented" around 1967 to describe a design built from small, message-passing units modelled loosely on biological cells, each hiding its own internal state and communicating only by sending and receiving messages1. The mechanics most programmers associate with the term today — classes, subclasses, and objects as runtime instances of them — predate Kay's naming and come from Simula, developed by Ole-Johan Dahl and Kristen Nygaard for discrete-event simulation; Simula 67 specifically added classes, subclasses, and virtual (overridable) methods to the earlier Simula I2. The "four pillars" below is a widely used teaching simplification, not a single formal definition — Booch's influential account of the object model, for comparison, names a different set of four elements (abstraction, encapsulation, modularity, and hierarchy), folding what the pillars below call "inheritance" and "polymorphism" together under hierarchy3. Both framings are legitimate; neither is uniquely "the" definition, which is worth knowing before treating either as gospel.
| Pillar | Description |
|---|---|
| Encapsulation | Bundle data + methods; hide internal state |
| Inheritance | Derive new classes from existing ones |
| Polymorphism | Same interface, different implementations |
| Abstraction | Hide complexity, expose only essentials |
Classes and Objects
Class = Blueprint (definition)
Object = Instance (concrete realization)
class Vehicle:
def __init__(self, make: str, model: str, year: int):
self.make = make
self.model = model
self.year = year
self._speed = 0 # protected by convention
def accelerate(self, amount: int) -> int:
self._speed += amount
return self._speed
# Usage
car = Vehicle("Toyota", "Camry", 2024)
car.accelerate(30)
#include <string>
class Vehicle {
private:
std::string make_;
std::string model_;
int year_;
int speed_ = 0;
public:
Vehicle(std::string make, std::string model, int year)
: make_(std::move(make)), model_(std::move(model)), year_(year) {}
int accelerate(int amount) {
speed_ += amount;
return speed_;
}
};
// Usage
Vehicle car("Toyota", "Camry", 2024);
car.accelerate(30);
public final class Vehicle {
private final String make;
private final String model;
private final int year;
private int speed = 0;
public Vehicle(String make, String model, int year) {
this.make = make;
this.model = model;
this.year = year;
}
public int accelerate(int amount) {
speed += amount;
return speed;
}
}
// Usage
Vehicle car = new Vehicle("Toyota", "Camry", 2024);
car.accelerate(30);
public sealed class Vehicle {
private readonly string Make;
private readonly string Model;
private readonly int Year;
private int Speed = 0;
public Vehicle(string make, string model, int year) {
Make = make;
Model = model;
Year = year;
}
public int Accelerate(int amount) {
Speed += amount;
return Speed;
}
}
// Usage
var car = new Vehicle("Toyota", "Camry", 2024);
car.Accelerate(30);
class Vehicle
attr_reader :make, :model, :year
def initialize(make, model, year)
@make = make
@model = model
@year = year
@speed = 0 # instance variables are private by default
end
def accelerate(amount)
@speed += amount
end
end
# Usage
car = Vehicle.new("Toyota", "Camry", 2024)
car.accelerate(30)
Visibility
| Prefix | Convention | Access |
|---|---|---|
name |
Public | Anywhere |
_name |
Protected | Class + subclasses |
__name |
Private | Class only (name mangling) |
The Four Pillars in Depth
1. Encapsulation
Bundle data and methods that operate on that data."
- Control access via getters/setters/properties
- Invariants maintained internally
- Reduces coupling
class BankAccount:
def __init__(self, initial: float = 0.0):
self._balance = float(initial)
@property
def balance(self) -> float:
return self._balance
def deposit(self, amount: float) -> None:
if amount > 0:
self._balance += amount
def withdraw(self, amount: float) -> bool:
if 0 < amount <= self._balance:
self._balance -= amount
return True
return False
#include <stdexcept>
class BankAccount {
double balance_ = 0.0;
public:
explicit BankAccount(double initial = 0.0) : balance_(initial) {}
double balance() const { return balance_; }
void deposit(double amount) {
if (amount > 0) balance_ += amount;
else throw std::invalid_argument("Amount must be positive");
}
bool withdraw(double amount) {
if (amount > 0 && amount <= balance_) {
balance_ -= amount;
return true;
}
return false;
}
};
public final class BankAccount {
private double balance = 0.0;
public BankAccount() {}
public BankAccount(double initial) { this.balance = initial; }
public double getBalance() { return balance; }
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
balance += amount;
}
public boolean withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return true;
}
return false;
}
}
public sealed class BankAccount {
private decimal _balance = 0m;
public BankAccount() {}
public BankAccount(decimal initial) => _balance = initial;
public decimal Balance => _balance;
public void Deposit(decimal amount) {
if (amount <= 0) throw new ArgumentException("Amount must be positive");
_balance += amount;
}
public bool Withdraw(decimal amount) {
if (amount > 0 && amount <= _balance) {
_balance -= amount;
return true;
}
return false;
}
}
class BankAccount
attr_reader :balance
def initialize(initial = 0.0)
@balance = initial.to_f
end
def deposit(amount)
@balance += amount if amount > 0
end
def withdraw(amount)
return false unless amount > 0 && amount <= @balance
@balance -= amount
true
end
end
2. Inheritance
"Derive specialised classes from general ones."
from abc import ABC, abstractmethod
class Animal(ABC):
def __init__(self, name: str):
self.name = name
@abstractmethod
def speak(self) -> str:
pass
class Dog(Animal):
def speak(self) -> str:
return "Woof!"
class Cat(Animal):
def speak(self) -> str:
return "Meow!"
# Usage
animals = [Dog("Rex"), Cat("Whiskers")]
for animal in animals:
print(f"{animal.name}: {animal.speak()}")
#include <string>
#include <memory>
#include <iostream>
class Animal {
protected:
std::string name_;
public:
explicit Animal(std::string name) : name_(std::move(name)) {}
virtual ~Animal() = default;
virtual std::string speak() const = 0;
const std::string& name() const { return name_; }
};
class Dog : public Animal {
public:
explicit Dog(std::string name) : Animal(std::move(name)) {}
std::string speak() const override { return "Woof!"; }
};
class Cat : public Animal {
public:
explicit Cat(std::string name) : Animal(std::move(name)) {}
std::string speak() const override { return "Meow!"; }
};
// Usage
int main() {
std::vector<std::unique_ptr<Animal>> animals;
animals.push_back(std::make_unique<Dog>("Rex"));
animals.push_back(std::make_unique<Cat>("Whiskers"));
for (const auto& a : animals) {
std::cout << a->speak() << '\n';
}
}
public abstract class Animal {
private final String name;
protected Animal(String name) { this.name = name; }
public String getName() { return name; }
public abstract String speak();
}
public final class Dog extends Animal {
public Dog(String name) { super(name); }
@Override public String speak() { return "Woof!"; }
}
public final class Cat extends Animal {
public Cat(String name) { super(name); }
@Override public String speak() { return "Meow!"; }
}
// Usage
List<Animal> animals = List.of(new Dog("Rex"), new Cat("Whiskers"));
for (Animal a : animals) System.out.println(a.speak());
public abstract class Animal {
public string Name { get; }
protected Animal(string name) => Name = name;
public abstract string Speak();
}
public sealed class Dog : Animal {
public Dog(string name) : base(name) {}
public override string Speak() => "Woof!";
}
public sealed class Cat : Animal {
public Cat(string name) : base(name) {}
public override string Speak() => "Meow!";
}
// Usage
var animals = new List<Animal> { new Dog("Rex"), new Cat("Whiskers") };
foreach (var a in animals) Console.WriteLine(a.Speak());
class Animal
attr_reader :name
def initialize(name)
@name = name
end
def speak
raise NotImplementedError, "#{self.class} must implement speak"
end
end
class Dog < Animal
def speak
"Woof!"
end
end
class Cat < Animal
def speak
"Meow!"
end
end
# Usage
animals = [Dog.new("Rex"), Cat.new("Whiskers")]
animals.each { |animal| puts "#{animal.name}: #{animal.speak}" }
Key concepts:
- super() / base() / super() — call parent method
- Method overriding — same name, different behaviour
- Abstract base classes (abc module / abstract / virtual)
Seidl et al.'s introductory UML text frames this precisely: inheritance is a class hierarchy in which a subclass extends or overrides a superclass's attributes and operations, and polymorphism is an attribute or operation binding dynamically to a subtype at runtime6 — matching the Animal/Dog/Cat pattern used above.
3. Polymorphism
"One interface, many implementations."
def make_sound(animal: Animal) -> None:
print(animal.speak())
# Usage
animals = [Dog("Rex"), Cat("Whiskers")]
for animal in animals:
make_sound(animal) # Woof! / Meow!
void make_sound(const Animal& animal) {
std::cout << animal.speak() << '\n';
}
int main() {
Dog dog("Rex");
Cat cat("Whiskers");
make_sound(dog); // Woof!
make_sound(cat); // Meow!
}
public static void makeSound(Animal animal) {
System.out.println(animal.speak());
}
public static void main(String[] args) {
makeSound(new Dog("Rex")); // Woof!
makeSound(new Cat("Whiskers")); // Meow!
}
static void MakeSound(Animal animal) {
Console.WriteLine(animal.Speak());
}
static void Main() {
MakeSound(new Dog("Rex")); // Woof!
MakeSound(new Cat("Whiskers")); // Meow!
}
def make_sound(animal)
puts animal.speak
end
# Usage — duck typing: anything that responds to #speak works
animals = [Dog.new("Rex"), Cat.new("Whiskers")]
animals.each { |animal| make_sound(animal) } # Woof! / Meow!
Key concepts:
- Dynamic dispatch — the call animal.speak() is resolved to Dog.speak or Cat.speak at runtime based on the object's actual type, not the variable's declared type
- Duck typing (Python, Ruby) — the Ruby example needs no shared base class at all; anything responding to speak works, which is polymorphism decided structurally rather than by declared inheritance
- Parametric vs. subtype polymorphism — the examples above are all subtype polymorphism (many types share one interface); generics/templates are a different kind, letting one implementation work across many types instead
Classes Aren't the Only Way to Build Objects
Every example on this page is class-based: an object's shape is fixed by a separate class definition, and the object is created by instantiating that class. This is by far the dominant model — Simula, Smalltalk, C++, Java, C#, Python, and Ruby all use it — but it is not the only way to do object orientation, and treating it as though it were leaves a gap in understanding what "object-oriented" actually means. Prototype-based programming builds objects directly from other objects, with no separate class construct at all: a new object is created by cloning (or delegating to) an existing one, and its "type" is simply whichever object it was built from. Self, developed at Xerox PARC in the mid-1980s, is the language that worked this model out in full4, and it reached everyday programmers through ECMAScript — JavaScript's every object still carries an internal link to a prototype object it delegates to for any property it doesn't have itself, with no class required, even though modern JavaScript's class keyword now provides familiar syntax over this same underlying mechanism5. See Prototype-Based Programming for the full picture: what a prototype chain actually is, why it collapses the class/instance distinction this page has been treating as fundamental, and where it does — and doesn't — matter in practice.
Related Topics
- Prototype-Based Programming: Objects Without Classes — the class-free alternative this page's closing section introduces, where objects are built directly from other objects instead of instantiated from a class.
- Inheritance & Composition — a full treatment of the second pillar introduced here, including when inheritance is the wrong tool and the Liskov Substitution Principle it can silently violate.
- Polymorphism & Interfaces — a full treatment of the third pillar, including the more precise academic taxonomy behind the "same interface, different implementations" one-liner above.
- Paradigms & Polyglot Programming — places object orientation as one point among several in a wider paradigm landscape, useful once these four pillars feel solid.
References
Kay, A. C. (1993). The early history of Smalltalk. ACM SIGPLAN Notices, 28(3), 69–96. https://doi.org/10.1145/155360.155364 ↩
Dahl, O.-J., & Nygaard, K. (1966). SIMULA: An ALGOL-based simulation language. Communications of the ACM, 9(9), 671–678. https://doi.org/10.1145/365813.365819; Simula 67's addition of classes, subclasses, and virtual methods is covered in Dahl, O.-J. (2001). The Birth of Object Orientation: The Simula Languages. ↩
Booch, G. (1994). Object-Oriented Analysis and Design with Applications (2nd ed.). Addison-Wesley. Names abstraction, encapsulation, modularity, and hierarchy as the major elements of the object model. ↩
Ungar, D., & Smith, R. B. (1987). Self: The power of simplicity. OOPSLA '87 Conference Proceedings, published as ACM SIGPLAN Notices, 22(12), 227–241. https://doi.org/10.1145/38807.38828 ↩
Ecma International. ECMAScript® Language Specification (ECMA-262). https://262.ecma-international.org/ — see the "Ordinary Object Internal Methods and Internal Slots" section for the
[[Prototype]]internal slot and prototype-chain property lookup. ↩Seidl, M., Scholz, M., Huemer, C., & Kappel, G. (2015). UML @ Classroom: An Introduction to Object-Oriented Modeling. Springer. Section 1.3, "Basic Principles of Object Orientation." ↩