OOP & SOLID

The four OOP pillars and the five SOLID principles, each with the one-sentence test an interviewer is listening for and a concrete code smell it fixes.

OOPSOLIDdesign-principles

The four pillars

  • Encapsulation — hide internal state; expose behavior. Invariants are protected because state only changes through methods.
  • Abstraction — model the essentials, hide the mechanism. Callers depend on what, not how.
  • Inheritance — share/extend behavior via an "is-a" relationship. Powerful but over-used; prefer composition ("has-a") for flexibility.
  • Polymorphism — one interface, many implementations; the caller doesn't branch on type.

SOLID

PrincipleThe test
SSingle Responsibility"One reason to change." A class does one job.
OOpen/ClosedOpen to extension, closed to modification — add a class, don't edit a switch.
LLiskov SubstitutionA subtype must be usable anywhere its base is, without surprises.
IInterface SegregationMany small interfaces beat one fat one; don't force unused methods.
DDependency InversionDepend on abstractions, not concretions.

Dependency Inversion in one example

The whole principle is one arrow flip. Before: high-level policy points down at a concrete detail. After: both point at an abstraction — the detail now depends on the interface, not the other way around.

// ❌ High-level policy welded to a concrete detail
class OrderService {
  private email = new SmtpEmailSender(); // can't swap, can't test
  confirm(o: Order) { this.email.send(o.userEmail, "Confirmed"); }
}

// ✅ Depend on an abstraction; inject the detail
interface Notifier { send(to: string, msg: string): void; }

class OrderService {
  constructor(private notifier: Notifier) {}      // inverted
  confirm(o: Order) { this.notifier.send(o.userEmail, "Confirmed"); }
}
// Now SMTP, SMS, or a fake for tests all satisfy Notifier — Open/Closed too.
SOLID is a means, not a religion

The goal is code that's easy to change and test. Quote the principle, then show the smell it removes (a giant switch, an untestable new, a subtype that throws on a base method). Interviewers want the judgment, not the acronym.

Composition over inheritance

Deep inheritance trees are rigid (a change high up ripples down) and force a single axis of variation. Composition lets you assemble behavior from small parts and swap them at runtime — it's why Strategy, Decorator and Dependency Injection all favor "has-a".

The left side pays 2^N classes for N independent choices; the right side pays one interface per axis of variation. "What varies here?" is the question that leads you to every pattern on the design patterns page.

Practice — level up

SOLID becomes muscle memory by modeling small things cleanly: one responsibility per class, behavior composed rather than inherited, an invariant held behind a tidy interface. These "design a structure" drills train exactly that.

Practice ladder: Modeling clean classes0/4 solved

Climb in order — every rung assumes the one above it. Solve on LeetCode, then tick it here; progress is saved on this device.

Warm-up — one class, one invariant

  1. Hold an extra invariant (the running min) behind push/pop — encapsulation, Single Responsibility.

Core — an interface that hides its guts

Callers see operations, never the internals.
  1. A clean put/get/remove contract over hidden buckets — abstraction and information hiding.

  2. LRU CacheMedium

    Compose a map + linked list behind get/put — composition over inheritance, done right.

Stretch — pick internals the API implies

  1. Choose internals that keep every operation cheap — Open/Closed against future operations.