OOP Fundamentals · Core OOP Concepts

OOP Pillars & Composition

Writing code against an abstraction (Shape) instead of its concrete subtypes, so new subtypes need zero changes elsewhere.

  • 6 min read
  • 10 practice questions
  • 2 graded problems
On this page
  1. Description
  2. The four pillars
  3. Composition over inheritance
  4. How to decide, step by step
  5. Where you'll see it
  6. Which tool for which job?
  7. Common mistakes
  8. Check yourself
  9. Key takeaways
  10. Quick revise
  11. Practice

Description

Object-oriented programming is a set of tools for putting data and the rules about that data together, and for letting different kinds of objects be used through one common shape. This skill covers its four classic ideas - encapsulation, abstraction, inheritance and polymorphism - and the most important design habit built on them: preferring composition to inheritance.

Here's the kind of code these ideas fix. Imagine an online shop written without objects: a map of fields for each product, a pile of static helper functions, and if type == "BOOK" checks wherever books behave differently from electronics. It works at first, but every rule lives far from the data it protects, anyone can put an invalid price into the map, and adding a product type means hunting down every if.

We'll go from simple to more technical:

  1. The four pillars, each in one sentence, in Java and Python.
  2. Composition over inheritance, and the classic example of getting it wrong.
  3. How to decide between them, step by step.
  4. Where you'll see these choices in real libraries and frameworks.

The four pillars

PillarIn one sentenceJava tools
EncapsulationKeep an object's data private and change it only through methods that protect its rules.private fields, methods that validate
AbstractionExpose what an object does, hide how it does it.interfaces, well-named public methods
InheritanceBuild a new class on top of an existing one ("is-a").extends
PolymorphismOne call, many behaviors: the object decides what the call means.overriding, interfaces

Python uses different tools for the same pillars: encapsulation is a convention (a leading _ marks an attribute as internal, and @property gives read-only access), abstraction uses abstract base classes (abc.ABC) or typing.Protocol, inheritance is class Savings(Account), and polymorphism needs no declared interface at all - any object with the right method works (duck typing).

Java

interface Shape { double area(); }                 // abstraction

final class Circle implements Shape {
    private final double radius;                   // encapsulation: private, validated once
    Circle(double radius) {
        if (radius <= 0) throw new IllegalArgumentException("radius must be positive");
        this.radius = radius;
    }
    public double area() { return Math.PI * radius * radius; }   // polymorphism
}

double total = shapes.stream().mapToDouble(Shape::area).sum();  // no type checks

Python

import math
from abc import ABC, abstractmethod


class Shape(ABC):                                   # abstraction
    @abstractmethod
    def area(self) -> float: ...


class Circle(Shape):
    def __init__(self, radius: float):
        if radius <= 0:
            raise ValueError("radius must be positive")
        self._radius = radius                       # encapsulation: internal by convention, validated once

    def area(self) -> float:                        # polymorphism
        return math.pi * self._radius ** 2


total = sum(s.area() for s in shapes)               # no type checks

Composition over inheritance

Inheritance is the pillar most often misused. extends gives you every public method of the parent - whether it makes sense or not - and ties you to its internals.

Java

// java.util.Stack extends Vector: a "stack" that also lets anyone insert into the middle.
Stack<Integer> stack = new Stack<>();
stack.push(1); stack.push(2);
stack.add(0, 99);     // legal, because Stack IS-A Vector - and LIFO is broken

Python

class Stack(list):                                  # a "stack" that IS-A list...
    def push(self, x) -> None:
        self.append(x)


stack = Stack()
stack.push(1)
stack.push(2)
stack.insert(0, 99)                                 # legal, because Stack IS-A list - and LIFO is broken

Composition means having an object instead of being one: a class holds a field and exposes only the operations it wants.

Java

final class BoundedStack<T> {
    private final Deque<T> items = new ArrayDeque<>();   // has-a, not is-a
    private final int capacity;
    BoundedStack(int capacity) { this.capacity = capacity; }
    void push(T item) {
        if (items.size() == capacity) throw new IllegalStateException("stack is full");
        items.push(item);
    }
    T pop() { return items.pop(); }
}

Python

class BoundedStack:
    def __init__(self, capacity: int):
        self._items = []                            # has-a, not is-a
        self._capacity = capacity

    def push(self, item) -> None:
        if len(self._items) == self._capacity:
            raise RuntimeError("stack is full")
        self._items.append(item)

    def pop(self):
        return self._items.pop()

How to decide, step by step

  1. Ask "is-a or has-a?" A Car has an Engine; a SavingsAccount is an Account.
  2. Even for is-a, check substitutability: can the child be used everywhere the parent is, without surprises? (If not, inheritance will hurt - see the Liskov Substitution Principle.)
  3. If you only want to reuse code, prefer composition: hold the helper as a field and delegate to it.
  4. If you want many types to be used interchangeably, prefer an interface plus composition over a deep class hierarchy.

Where you'll see it

  • The JDK's own regrets: Stack extends Vector and Properties extends Hashtable are classic examples of inheritance exposing the wrong API. Python has its own version: subclassing the built-in list or dict is full of surprises, because their C methods don't call your overrides (extend never calls your append) - collections.UserList and UserDict exist for exactly that reason.
  • Modern frameworks favor composition: Spring beans receive collaborators through their constructors instead of inheriting behavior.
  • Game engines moved from deep Enemy -> FlyingEnemy -> FlyingShootingEnemy hierarchies to entity-component systems - composition at scale.

Which tool for which job?

You want toUse
Protect a rule about an object's dataprivate fields (Java) or _name attributes (Python), changed only by methods that check the rule
Let callers use many kinds of objects the same wayan interface (Java) or an ABC / Protocol (Python)
Reuse another class's codecomposition: hold it as a field and delegate
Model a genuine "is-a" with a base designed for extensioninheritance
A closed, known set of typessealed interfaces (Java 17+); typing.final and checks in Python
Stop anyone subclassing a classfinal (Java); @typing.final, checked by type checkers (Python)

Common mistakes

MistakeWhat happensDo this instead
Extending a class just to reuse a few methodsyou also expose every other method it hascompose: hold it as a field
Extending a library collection (Vector, list, dict) to add rulescallers bypass your rules through inherited methodswrap the collection
A getter and setter for every fieldpublic fields with extra steps - no rule is protectedmeaningful operations (deposit, withdraw)
Deep hierarchies (Enemy -> FlyingEnemy -> FlyingShootingEnemy)five files to understand one objectsmall interfaces plus composed parts
if type == ... checks spread through the codeevery new type means editing every ifpolymorphism: each type implements the behavior
Read more - design for inheritance, or prohibit it

A useful rule from Effective Java: design and document for inheritance, or else prohibit it. Making classes final by default (or using records and sealed interfaces) keeps you honest - inheritance becomes a deliberate, reviewed decision instead of an accident. Python has no enforced final: @typing.final is checked only by type checkers.

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. Is getters-and-setters-for-every-field encapsulation? Why or why not?

Show answer

No. If every field can be read and written freely, the object can't protect any rule about its data. Encapsulation means the object's methods keep its invariants true - often there's no setter at all, just meaningful operations like deposit().

2. Why does java.util.Stack break the idea of a stack?

Show answer

Because it extends Vector, it inherits add(index, element), remove(index) and friends, so anyone can modify the middle of the "stack" - inheritance exposed an API that contradicts what the class is supposed to be.

3. You want ReportService to reuse the formatting code in PdfFormatter. Inherit or compose?

Show answer

Compose: ReportService has a PdfFormatter field and calls it. A report service is not a kind of formatter, and composition lets you swap in an HtmlFormatter later.

Key takeaways

  • Encapsulation protects rules; abstraction hides details; inheritance models is-a; polymorphism lets one call mean different things.
  • Ask "is-a or has-a?" - and even for is-a, check the child can stand in for the parent everywhere.
  • Prefer composition for code reuse: hold a field, expose only what you mean to.
  • Make classes final unless they're designed for inheritance.
  • Composition costs a few forwarding lines and buys control of your API and freedom to swap parts.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

Pick the tool

You want toUse
Protect a rule about dataprivate state + methods that check it
Use many types the same wayan interface / ABC / Protocol
Reuse codecomposition: a field you delegate to
A genuine is-a, base designed for itinheritance
A closed set of typessealed ... permits (Java); a union + assert_never (Python)
No subclassesfinal (Java); @typing.final or __init_subclass__ (Python)

Snippets worth knowing by heart

Java

final class BoundedStack<T> {
    private final Deque<T> items = new ArrayDeque<>();
    private final int capacity;
    BoundedStack(int capacity) { this.capacity = capacity; }
    void push(T item) {
        if (items.size() == capacity) throw new IllegalStateException("stack is full");
        items.push(item);
    }
    T pop() { return items.pop(); }
}

Python

class BoundedStack:
    def __init__(self, capacity: int):
        self._items = []
        self._capacity = capacity

    def push(self, item) -> None:
        if len(self._items) == self._capacity:
            raise RuntimeError("stack is full")
        self._items.append(item)

    def pop(self):
        return self._items.pop()

The traps

  • Stack extends Vector, class Stack(list) - inherited methods break the rules.
  • Getters and setters for everything - no invariant is protected.
  • Deep hierarchies - one object spread over five files.
  • Type checks instead of polymorphism - every new type edits every if.

Staff-level points, one line each

  • Fragile base class = self-use: HashSet.addAll calls add (counted 6 for 3); dict.update skips __setitem__; list.extend skips append.
  • UserDict/UserList route everything through overridable methods - or wrap instead.
  • Python __getattr__ never forwards special methods: len(wrapper) raised TypeError.
  • New default methods on an interface aren't forwarded by old wrappers (synchronizedCollection had to override removeIf).
  • The self problem: the inner object hands out this/self and bypasses the wrapper.
  • HotSpot inlines monomorphic and bimorphic call sites; only megamorphic ones pay a table lookup.
  • Python call sites are specialized per type since 3.11; an isinstance chain isn't faster.
  • Sealed hierarchies give polymorphism plus compiler-checked exhaustiveness.
  • GoF 1994: "Favor object composition over class inheritance"; Effective Java Items 18-19.

Say it in an interview

  • "A report service isn't a kind of formatter, so it holds one and delegates."
  • "I'd wrap the collection - subclassing depends on how its methods call each other."
  • "Those types are a closed set, so I'd make the interface sealed and switch exhaustively."
  • "The wrapper overrides the new default method too, so it keeps forwarding."

Practice

This skill comes with 10 multiple-choice questions that tell you which misconception a wrong answer reveals and 2 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice OOP Pillars & Composition free

Total Area via Polymorphism

EasyFreeCoding problemJava · Python

You're given a mix of shapes, each one of CIRCLE <radius>, RECTANGLE <width> <height>, or TRIANGLE <base> <height>. Implement each shape's area() so that summing over a List<Shape> - without ever checking which concrete type each one is - gives the correct total area.

A Bounded Stack Built by Composition

EasyFreeCoding problemJava · Python

java.util.Stack extends Vector, so every stack also exposes add(index, element) and other list methods that break last-in-first-out. Build a better one by composition: BoundedStack has a Deque and exposes only these operations:

Practice OOP Pillars & Composition free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime