Skip to main content
Clean Architecture 70 XP · 7 min

OOP: The Reality of Encapsulation

OOP didn't invent encapsulation — C had perfect encapsulation before C++ and Java actually weakened it with header file requirements.

Showing
Ad (728×90)

Why this matters

Encapsulation is often cited as one of OOP's defining contributions, but Robert C. Martin challenges this. C programs, compiled from a .c file with a .h header, gave users access only to function signatures — the implementation was completely hidden. That is perfect encapsulation. When C++ introduced header files that had to declare private member variables, and Java required all class source to be visible, the language-enforced encapsulation Martin calls "perfect" was actually degraded.

The lesson is that encapsulation is not a language feature — it is a discipline. The language can make it easier or harder to break, but it cannot enforce it. A developer writing Python or TypeScript must choose to protect internal state through naming conventions, properties, and private keywords. Without that discipline, objects become transparent data buckets that anyone can corrupt.

The problem

When all attributes are public, callers can assign any value to any field. Invariants — like "balance must never be negative" — cannot be enforced.

Bad

class BankAccount:
    def __init__(self, owner: str, balance: float):
        self.owner   = owner
        self.balance = balance  # nothing stops: acc.balance = -999999

account = BankAccount("Alice", 500.0)
account.balance = -99999  # silent corruption — no validation possible
class BankAccount {
  owner:   string;
  balance: number; // any caller can write: acc.balance = -Infinity

  constructor(owner: string, balance: number) {
    this.owner   = owner;
    this.balance = balance;
  }
}

const acc = new BankAccount("Alice", 500);
acc.balance = -99999; // no guard, no error — invariant broken silently

The solution

Private attributes and controlled methods enforce invariants at the boundary. State changes only happen through operations that validate the input first.

Good

class BankAccount:
    def __init__(self, owner: str, initial: float):
        self._owner   = owner
        self._balance = initial

    @property
    def balance(self) -> float: return self._balance

    def deposit(self, amount: float) -> None:
        if amount <= 0: raise ValueError("Must be positive")
        self._balance += amount

    def withdraw(self, amount: float) -> None:
        if amount <= 0:             raise ValueError("Must be positive")
        if amount > self._balance:  raise ValueError("Insufficient funds")
        self._balance -= amount

acc = BankAccount("Alice", 500.0)
acc.deposit(100.0)
print(acc.balance)  # 600.0
class BankAccount {
  private _balance: number;
  private _owner:   string;

  constructor(owner: string, initialBalance: number) {
    this._owner   = owner;
    this._balance = initialBalance;
  }

  get balance(): number { return this._balance; }

  deposit(amount: number): void {
    if (amount <= 0) throw new Error("Must be positive");
    this._balance += amount;
  }

  withdraw(amount: number): void {
    if (amount <= 0)            throw new Error("Must be positive");
    if (amount > this._balance) throw new Error("Insufficient funds");
    this._balance -= amount;
  }
}

const acc = new BankAccount("Alice", 500);
acc.deposit(100);
console.log(acc.balance); // 600

Key takeaway

Encapsulation is a discipline enforced by the programmer, not a guarantee provided by the language — the language merely makes it easier or harder to break.

Done with this lesson?

Mark it complete to earn XP and track your progress.