The Four Pillars of OOP

Learning Objectives

State each pillar precisely and what it protects against.

Show each pillar in isolation and in one integrated design.

Recognise misuse of each pillar.

Prerequisites: Modules 2–5.

Intuition and Definition

Question it answers Keep state private and change it only through operations that preserve invariants "Who may change this?" Public setters for everything

Expose the essential operations, hide the details "What does a caller need to know?" Leaky interfaces exposing internals

Define a subtype as a specialisation of a base type "Is it substitutable?" Inheriting just to reuse code

Use one interface with many implementations, chosen at run time (or compile time) "Which behaviour runs?" Long if (type == …) chains instead

Why It Matters

The pillars are not goals in themselves; each solves a problem. Encapsulation prevents invalid states, abstraction reduces what callers depend on, inheritance expresses genuine "is-a" substitutability, and polymorphism lets new behaviour plug in without editing existing code.

Syntax and Language Notes

All four in one sentence of C++: a private field (encapsulation) behind a pure virtual interface (abstraction), implemented by a derived class (inheritance), used through a base reference (polymorphism).

class Sensor {                        // abstraction
public:
    virtual double read() = 0;
    virtual ~Sensor() = default;
};
class SpeedSensor : public Sensor {   // inheritance
    double offset_ = 0.5;             // encapsulation
public:
    double read() override { return 87.0 + offset_; }
};
void show(Sensor& s) { s.read(); }    // polymorphism

Complete Example

#include <iostream>
#include <memory>
#include <string>
#include <vector>

// Abstraction: what the cluster needs from any sensor
class Sensor {
public:
    virtual std::string label() const = 0;
    virtual double value() const = 0;
    virtual bool warning() const { return false; }   // default behaviour
    virtual ~Sensor() = default;
};

// Encapsulation: calibration is private, the reading is validated
class CoolantSensor : public Sensor {                 // Inheritance
    double raw_;
    double offset_;
public:
    CoolantSensor(double raw, double offset) : raw_(raw), offset_(offset) {}
    std::string label() const override { return "Coolant (C)"; }
    double value() const override { return raw_ + offset_; }
    bool warning() const override { return value() > 110.0; }
};

class SpeedSensor : public Sensor {
    double kmh_;
public:
    explicit SpeedSensor(double kmh) : kmh_(kmh < 0 ? 0 : kmh) {}   // never negative
    std::string label() const override { return "Speed (km/h)"; }
    double value() const override { return kmh_; }
};

class FuelSensor : public Sensor {
    double percent_;
public:
    explicit FuelSensor(double p) : percent_(p) {}
    std::string label(
…
Speed (km/h): 0
Coolant (C): 110.5  [WARNING]
Fuel (%): 7.5  [WARNING]

Step-by-Step Walkthrough

Abstraction: Cluster knows only label(), value() and warning().

Encapsulation: CoolantSensor hides its raw value and calibration offset; SpeedSensor clamps negative input in its constructor.

Inheritance: each sensor *is a* Sensor and can be used wherever one is expected; SpeedSensor inherits the default warning().

Polymorphism: render() calls s->warning(); the right override runs for each object.

Adding an oil-pressure sensor means writing one new class — Cluster is unchanged (Open/Closed principle, Module 19).

classDiagram
  class Sensor {
    <<abstract>>
    +label() string
    +value() double
    +warning() bool
  }
  Sensor <|-- CoolantSensor
  Sensor <|-- SpeedSensor
  Sensor <|-- FuelSensor
  Cluster o-- "many" Sensor

Common Mistakes

Treating the pillars as a checklist ("we must use inheritance somewhere").

Using inheritance for code reuse only — prefer composition (Module 7).

Type switches (if (dynamic_cast<FuelSensor*>(s))) inside the client defeat polymorphism.

Abstractions shaped by the first implementation, which then fit nobody else.

Trade-offs and Alternatives

Polymorphism through virtual functions costs an indirect call and usually a heap allocation per object. When the set of types is closed and known (e.g. exactly three sensor kinds), std::variant + std::visit gives value semantics and exhaustive checking (Module 17, 49).

Real-World Use

Instrument clusters, plug-in audio effects, payment providers, file-system drivers and UI widgets all follow this shape: an abstract interface, several encapsulated implementations, and a client that iterates polymorphically.

Practice

Q1. (Easy) Which pillar is violated by a public double raw field in CoolantSensor?

Answer: Encapsulation — callers could change the reading without calibration or validation.

Q2. (Medium) Add an OilPressureSensor that warns below 1.0 bar. Which existing classes change?

Answer: None. You write class OilPressureSensor : public Sensor with label, value and warning, and add an instance to the cluster.

Q3. (Advanced) Rewrite render() without polymorphism using an enum kind field. What breaks when a fourth sensor type appears?

Answer: render (and every other function that switches on kind) needs a new case; forgetting one silently misbehaves. With virtual functions the compiler forces each new class to implement the pure virtual functions, and existing code is untouched.

Interview Question

Q1. "Explain the four pillars using one example, and say which one you would sacrifice first under pressure."

Answer: Use a payment system: PaymentProvider interface (abstraction), each provider keeps its API keys private (encapsulation), CardProvider/WalletProvider derive from it (inheritance), checkout calls provider.charge() (polymorphism). The pillar most often worth giving up is inheritance — composition and interfaces deliver polymorphism without coupling to a base class implementation. Encapsulation is the last thing to give up because it protects correctness.

Quick Quiz

Q1. Which pillar lets new types plug into existing code without changing it?

Answer: Polymorphism (enabled by abstraction).

Q2. Which pillar answers "who may change this state?" Answer: Encapsulation.

Q3. Inheritance should model which relationship?

Answer: A substitutable "is-a" relationship (Liskov substitution).

Summary

Encapsulation protects invariants; abstraction limits what callers know; inheritance models substitutable specialisation; polymorphism selects behaviour at run time.

Use them to solve concrete problems, not as a checklist.

Related: Module 7 (inheritance), Module 8 (polymorphism), Module 19 (SOLID).