Phase 5 · Low-level design: OOP and SOLID · Topic 5.1

OOP: the four pillars

Must know

Object-oriented programming organises code into objects that bundle data and behaviour. Its four pillars are encapsulation, abstraction, inheritance and polymorphism.

Not started

Step 1 Understand it

The problem it solves. Large programs written as one long list of functions and shared variables become impossible to change safely. OOP groups data with the code that's allowed to change it.

Watch it happen

AnimationThe four pillars, on one bank account
Read the animation as text
  1. A class groups data and behaviour: a BankAccount holds a balance and has deposit() and withdraw().
  2. Encapsulation: the balance is private (–). Nobody can set it to −£30 directly. withdraw(80) checks there is enough money first, and refuses.
  3. Abstraction: callers only see deposit(), withdraw() and the balance — not how interest, fees or the ledger work inside.
  4. Inheritance: a SavingsAccount IS a BankAccount that also adds interest. It reuses everything and adds one thing.
  5. Polymorphism: code loops over all accounts calling monthly_update(). Each type does the right thing — savings add interest, current accounts take a fee — and the loop never asks which is which.

Picture it

classDiagram
  class PaymentMethod {
    <<abstract>>
    +pay(amount)
  }
  class CardPayment {
    -card_number
    +pay(amount)
  }
  class PayPalPayment {
    -email
    +pay(amount)
  }
  PaymentMethod <|-- CardPayment
  PaymentMethod <|-- PayPalPayment
The arrow with a hollow triangle means 'is a kind of'. Both payment types share the pay() method () and hide their details (the minus sign means private).

The idea

hides an object's internal data behind , so it can't be put into an invalid state. shows callers only what they need. lets one reuse and extend another. Polymorphism lets different classes be used through the same , each behaving in its own way.

How it works, step by step

  1. Group related data and behaviour into a class: a BankAccount holds a balance and has deposit() and withdraw().
  2. Make the balance private, and have withdraw() check there's enough money first. That's encapsulation.
  3. Callers only see deposit(), withdraw() and the balance. That's abstraction.
  4. A SavingsAccount is a kind of BankAccount that also adds interest. That's inheritance.
  5. Code that loops over all accounts and calls monthly_update() gets the right behaviour for each type. That's polymorphism.

Analogy

A car. You use the steering wheel and pedals (abstraction) without touching the engine (encapsulation). An electric car is a kind of car (inheritance), and 'press the accelerator' works differently in each (polymorphism).

Where the analogy breaks

Cars are physical and their family tree is shallow. In code, inheritance is easy to overuse: deep class trees become fragile, which is why is often the better choice.

Example

A payment system: an abstract PaymentMethod with pay(amount); CardPayment and PayPalPayment each implement it differently; checkout simply calls method.pay(amount) without caring which one it has.

Must-know points

  • Encapsulation: private state, with public methods that keep it valid.
  • Abstraction: interfaces and describe what, not how.
  • Inheritance: an 'is-a' relationship; reuses code but creates tight coupling.
  • Polymorphism: one interface, many implementations; replaces long if/else chains on type.

Common mistakes

  • Public fields that anyone can set to invalid values.
  • Using inheritance just to reuse a few lines of code.
  • Type checks (if isinstance …) where polymorphism would do.
python
from abc import ABC, abstractmethod

class PaymentMethod(ABC):                  # abstraction
    @abstractmethod
    def pay(self, amount: float) -> str: ...

class CardPayment(PaymentMethod):
    def __init__(self, card_number: str):
        self._card = card_number            # encapsulation: kept internal
    def pay(self, amount):
        return f"Charged £{amount} to card ending {self._card[-4:]}"

class PayPalPayment(PaymentMethod):
    def __init__(self, email: str):
        self._email = email
    def pay(self, amount):
        return f"Charged £{amount} via PayPal ({self._email})"

for method in [CardPayment("4111111111111111"), PayPalPayment("a@b.com")]:
    print(method.pay(20))                   # polymorphism

Step 2 Go further (free, optional)

Optional. The notes above are written to be enough on their own. These free resources give a second explanation and extra practice.

Step 3 Practise

Try each exercise yourself first: on paper, out loud or in code. Then open the answer and compare it with yours.

  1. ExplainSay it in plain words in under a minute, with no notes.

    Explain the four pillars with the car picture.

    Show answer

    • Abstraction: to drive, you use the steering wheel, the pedals and the gear stick. You don't need to know how fuel injection works. A class shows callers only the few methods they need.
    • Encapsulation: the engine is under the bonnet, out of reach. You can't set the engine speed to an impossible value by hand; you can only press the accelerator, which keeps it within safe limits. A class keeps its data private and only lets it change through methods that keep it valid.
    • Inheritance: an electric car is a kind of car. It has everything a car has (wheels, seats, a steering wheel) and adds a battery. A reuses its parent and extends it.
    • Polymorphism: "press the accelerator" works on any car, but a petrol car burns more fuel while an electric car draws more current. Code calls the same method on different classes, and each does its own version.

  2. DrawSketch the diagram (or class diagram) from memory.

    Draw a for Shape with Circle and Rectangle subclasses and an area() method.

    Show answer

    Shape is abstract: it only promises area(). Each subclass keeps its own measurements private (the minus sign) and implements area() its own way. The hollow triangle points from each subclass up to the parent: "is a kind of".

    text
               «abstract»
                 Shape
            ───────────────
            + area(): float
                   △
          ┌────────┴────────┐
       Circle           Rectangle
    ────────────     ──────────────
    - radius         - width
                     - height
    + area()         + area()
      = π × r²         = width × height
  3. ApplyUse it in a real design question and say exactly where it fits.

    Code the Shape example in Python, then add a Triangle without changing any code that uses shapes.

    Show answer

    total_area only knows about Shape and its area() method. Adding Triangle is one new class; total_area, and any other code that uses shapes, is not touched. That is polymorphism (and the open/closed principle): the caller says "give me your area", and each shape answers in its own way.

    python
    import math
    from abc import ABC, abstractmethod
    
    
    class Shape(ABC):
        @abstractmethod
        def area(self) -> float: ...
    
    
    class Circle(Shape):
        def __init__(self, radius):
            self._radius = radius
    
        def area(self):
            return math.pi * self._radius ** 2
    
    
    class Rectangle(Shape):
        def __init__(self, width, height):
            self._width, self._height = width, height
    
        def area(self):
            return self._width * self._height
    
    
    def total_area(shapes):                  # the code that uses shapes
        return sum(shape.area() for shape in shapes)
    
    
    print(round(total_area([Circle(1), Rectangle(2, 3)]), 2))                   # 9.14
    
    
    class Triangle(Shape):                   # added later: total_area is untouched
        def __init__(self, base, height):
            self._base, self._height = base, height
    
        def area(self):
            return 0.5 * self._base * self._height
    
    
    print(round(total_area([Circle(1), Rectangle(2, 3), Triangle(4, 5)]), 2))   # 19.14
  4. Trade-offsSay what it costs, when not to use it, and the alternative.

    Give one example where inheritance makes a design worse.

    Show answer

    A stack built by inheriting from a list: class Stack(list).

    It looks like neat reuse: a list can already add to the end and pop from the end. But inheritance hands over everything a list can do, and the stack can't take any of it back. Callers can now insert(0, x), sort it, delete from the middle or slice it, all of which break the one promise a stack makes: last in, first out. (Java made exactly this mistake: its Stack class extends Vector.)

    Composition fixes it: the Stack holds a list privately and exposes only push(), pop() and peek(). Users can't break it, and the list could later be swapped for something else without anyone noticing.

    The general lesson: inherit only for a true "is a kind of", where the child can honour every promise the parent makes. "Uses one internally" is composition.

Step 4 Interview-ready, then prove it

Your interview answer

About 20 to 40 seconds, said out loud. Practise it until it sounds like you, not like notes.

Encapsulation protects an object's state, abstraction exposes only what callers need, inheritance models an 'is-a' relationship to reuse behaviour, and polymorphism lets code work with an interface while each class supplies its own behaviour — like checkout calling pay() on any payment method.

Follow-up questions

Abstract class or interface?

An interface only declares methods. An abstract class can also hold shared code and state. In Python both are usually written with ABC.

Overloading or overriding?

Overloading: the same method name with different parameters, chosen at compile time (for example in Java). Overriding: a subclass replaces a parent's method, chosen at runtime — that's polymorphism.

Tick each check when you can do it without notes

A topic is finished when you pass all four checks, not when you finish a video. Be strict: if you needed your notes, it is not ticked yet.

  • Explain Say it in plain words in under a minute, with no notes.
  • Draw Sketch the diagram (or class diagram) from memory.
  • Apply Use it in a real design question and say exactly where it fits.
  • Trade-offs Say what it costs, when not to use it, and the alternative.

0 of 4 checks ticked.

Saved in this browser. Back it up or move it to another device.