caduh

Object-Oriented Programming (OOP) Concepts — encapsulation, inheritance, polymorphism

2 min read

A brief, practical overview of the core OOP ideas with tiny examples: how encapsulation protects invariants, when inheritance fits, and how polymorphism keeps code open to extension.

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/else trees.
  • 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/else on types.
  • [ ] Write small classes with single responsibility; keep dependencies injectable/testable.