TL;DR
- Encapsulation bundles data + behavior and hides internals behind a stable interface. Protect invariants via methods/properties, not raw field access.
- Inheritance models an is‑a relationship and reuses code, but it couples hierarchies. Prefer composition > inheritance unless a true subtype fits.
- Polymorphism lets different types respond to the same message (method) differently—making code extensible without
if/elsetrees. - Apply SOLID (esp. Liskov Substitution & Open/Closed) to keep designs safe to extend.
Encapsulation (protect your invariants)
Hide implementation details and expose a minimal API. Validate and keep objects in a valid state.
Python
class BankAccount:
def __init__(self, owner, balance=0):
self._owner = owner # "protected" by convention
self._balance = balance
@property
def balance(self):
return self._balance
def deposit(self, amount):
if amount <= 0: raise ValueError("amount > 0")
self._balance += amount
def withdraw(self, amount):
if not 0 < amount <= self._balance:
raise ValueError("insufficient funds")
self._balance -= amount
TypeScript
class BankAccount {
#balance = 0; // hard-private
constructor(public owner: string, initial = 0) { this.#balance = initial; }
get balance() { return this.#balance; }
deposit(a: number) { if (a <= 0) throw new Error("amount > 0"); this.#balance += a; }
withdraw(a: number) { if (a <= 0 || a > this.#balance) throw new Error("insufficient"); this.#balance -= a; }
}
Why it matters: callers can’t break invariants by directly mutating fields.
Inheritance (reuse with care)
Use when a subtype truly is a base type and can be substituted anywhere the base is expected (LSP). Avoid deep hierarchies; don’t inherit just to “get code for free.”
Python (abstract base)
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
class Rectangle(Shape):
def __init__(self, w, h): self.w, self.h = w, h
def area(self) -> float: return self.w * self.h
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self) -> float: import math; return math.pi * self.r * self.r
Pitfall: If a subclass can’t honor the base class’s promises, don’t inherit—use composition.
Polymorphism (one interface, many forms)
Code to interfaces/abstractions, not concretions. The caller uses the base type; each subtype provides its own behavior (dynamic dispatch).
def total_area(shapes: list[Shape]) -> float:
return sum(s.area() for s in shapes) # works for any Shape
TypeScript (interface + different implementations)
interface Notifier { send(msg: string): void; }
class Email implements Notifier { send(m){ /* ... */ } }
class Slack implements Notifier { send(m){ /* ... */ } }
function alertAll(ns: Notifier[], m: string){ ns.forEach(n => n.send(m)); }
Composition > Inheritance (often)
Swap behavior by composing objects instead of subclassing.
class FlyBehavior:
def fly(self): ...
class FastFly(FlyBehavior):
def fly(self): print("zoom!")
class Duck:
def __init__(self, flyer: FlyBehavior): self.flyer = flyer
def fly(self): self.flyer.fly()
mallard = Duck(FastFly()); mallard.fly() # change behavior without new subclasses
When OOP shines vs. not
- Great for: domain models with rich invariants, plugin systems, GUIs, SDKs, game entities.
- Maybe not: pure data pipelines, simple CRUD, or math-heavy code—data classes + functions can be simpler.
Quick checklist
- [ ] Keep fields private; expose methods/properties that preserve invariants.
- [ ] Use interfaces/abstract bases and small, flat hierarchies.
- [ ] Favor composition; inherit only for true is‑a relationships (passes LSP).
- [ ] Rely on polymorphism instead of
if/elseon types. - [ ] Write small classes with single responsibility; keep dependencies injectable/testable.