InterviewPrepKit

Home / Cheat Sheet / Object-Oriented Design

Cheat sheet

OOP Fundamentals

Read the full lesson →

Every principle here reduces to one number: how many files a requirement change forces you to edit. Each one makes that count small on one axis by charging a price on another.

Vocabulary

  • Class: the template. Object/instance: one filled-in copy. Account vs Account(500).
  • Field/attribute: one piece of an object’s data. Method: a function on the class taking the object as self.
  • Invariant: a statement that must hold before and after every method (“balance is never negative”).
  • Interface: a named set of method signatures with no code behind them.
  • Input to a design: a requirement change (English sentence). Output: files opened + tests re-run. Lower is better.

The four pillars (defined by the edit they prevent)

PillarMakes cheapThe tell it is missing
EncapsulationChange internal storage without touching callersCallers reach into fields; a rename breaks a dozen files
AbstractionSwap how without renegotiating whatEvery caller knows which database you use
InheritanceAdd a subtype existing callers already handleA subclass overrides a method to raise NotImplementedError
PolymorphismAdd a case without editing the dispatcherA growing chain of isinstance checks
  • Encapsulation is not “make fields private”. It is “exactly one named place may violate the invariant”. Python has no real private: leading _ is a convention, __ triggers name mangling (still not privacy).
  • Use type(x) is int, not isinstance(x, int): bool is a subclass of int, so isinstance(True, int) is True.
  • Read-only field = @property getter with no setter. Never write get_x()/set_x() in Python.
  • Abstraction test: would this promise still hold if I rewrote the internals tomorrow? If not, it was implementation with a docstring.
  • Polymorphism math: 4 operations switching on type × 1 new type = 4 edits; polymorphic = one new class, zero edits. mypy can’t catch a missed switch (every branch is valid code).

SOLID (violation → fix)

PrincipleTellFix
SOne reason (one actor) to changeFinance, design, DBA in one fileSplit by actor into separate classes
OOpen to extension, closed to modificationif kind == ... chain you edit per caseStrategy: one class per rule, dispatch via dict
LSubtype usable by code that never heard of itstretch(Square(3)) fails; Rectangle passesImmutable value objects sharing a Protocol role
INo client depends on methods it doesn’t usescan() raising NotImplementedErrorOne interface per capability
DPolicy owns the abstraction, detail implements itTest needs a real server runningInject the dependency; test with a fake
  • LSP rules: a subtype may not strengthen a precondition, weaken a postcondition, break a base invariant, or throw a new exception type. Removing a capability the base promised means it isn’t really a subclass.
  • OCP cost: dict dispatch turns a bad key into a runtime KeyError instead of a visible missing branch.
  • ISP: interfaces belong to the caller. Protocol (structural, just have the methods) vs ABC+@abstractmethod (nominal, must declare and checked at construction).
  • DIP payoff: the test needs nothing installed. Use field(default_factory=list), never sent: list = [] (shared across all instances).

Composition over inheritance

  • Composition = has-a (hold a part, delegate); inheritance = is-a (substitutable).
  • Inheritance multiplies, composition adds: 3 powertrains × 2 bodies = 6 subclasses (× a third axis = 12); composed = 3 + 2 + 2 = 7 parts.
  • Rule: inherit to be substitutable, compose to reuse. If your reason is “B needs A’s save()”, it’s composition.
  • Composition assumes axes vary independently. When the arithmetic says addition, you’ve assumed independence — the assumption most likely to be wrong.

Coupling & cohesion (both are counts)

  • Coupling: how many modules must exist for this to run, and how many break when it changes.
    • Efferent Ce = modules this imports; afferent Ca = modules that import this.
    • Instability = Ce / (Ce + Ca), from 0.0 to 1.0. Near 1.0 = a leaf (fine); near 0.0 = a stable core (must not change). Danger = high Ce and high Ca.
    • Law of Demeter: talk only to immediate friends. Each dot past the first is a class you depend on (a.b.c.d knows 2 structures).
  • Cohesion: what fraction of methods touch the same fields. Two disjoint field groups = two classes (no test exercises both halves).

Singleton: don’t

  • A Singleton allows one instance via a global access point. It is a global variable with a constructor. Three costs:
    1. Hides the dependency (signature takes nothing, calls Config() inside).
    2. Makes tests order-dependent (shared memory; test 2 fails only after test 1 ran). No fix but a reset(), which admits the design is wrong.
    3. Lazy init is a race (if _inst is None is a check-then-store gap; the GIL doesn’t save you).
  • Instead: make “one of these” a lifetime decision, not an access one. Construct one in main, inject it. In Python, a module-level object is the idiomatic process-wide form (import is cached, thread-safe under the import lock).
  • Defensible only for a stateless, immutable, process-wide resource where injection is impractical (logger, connection pool) — and keep the constructor injectable.

The transferable skill

Every class structure is a bet about what varies. Ask: what did I assume varies (params, strategies, subtypes)? What is fixed (signatures, return shapes, enum lists)? What would the opposite bet have built? The failure always takes the same form: the part you froze is the part that needed to change. You can’t freeze less (an interface that promises nothing isn’t one) — so name the bet and know what the losing case costs.

Want the full picture? The lesson has the derivations, worked examples, and diagrams this card compresses into bullets. Read the full lesson →
Report a bug