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(); } // polymorphismComplete 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" SensorCommon 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).