Abstraction and Interfaces
Learning Objectives
Distinguish abstraction from encapsulation.
Define interfaces and abstract classes in C++, Java, C#, Python and TypeScript.
Describe a contract with preconditions, postconditions and invariants.
Explain why a polymorphic C++ base class needs a virtual destructor.
Prerequisites: Modules 2–3.
Intuition and Definition
Abstraction is choosing *what* to expose: the essential operations of a concept, with the details hidden. A car's abstraction is steering wheel, pedals and gear selector — not fuel injection timing. Encapsulation is the mechanism that keeps the hidden details from being touched.
An abstract data type (ADT) is defined only by its operations and their behaviour: a *stack* is "push, pop, top, empty" whether it is implemented with an array or a linked list.
An interface is a named set of operations with no implementation. In C++ it is a class with only pure virtual functions (= 0); a class with at least one pure virtual function is abstract and cannot be instantiated.
A contract describes what an operation needs and promises:
Precondition: what must be true before the call (amount > 0).
Postcondition: what is true afterwards (balance increased by amount).
Invariant: what is always true for the object (balance >= 0).
Why It Matters
Code written against an abstraction (Shape, Logger, Storage) works with every implementation — today's and next year's. Small, focused interfaces are easy to implement and easy to fake in tests (interface segregation, Module 19).
Syntax and Language Notes
class Shape { // C++: abstract class used as an interface
public:
virtual double area() const = 0; // pure virtual
virtual ~Shape() = default; // virtual destructor
};// Java (not run)
public interface Shape { double area(); }public interface IShape { double Area(); }
public abstract class ShapeBase : IShape { public abstract double Area(); }from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...interface Shape { area(): number; } // erased at run time: structural check only
abstract class ShapeBase implements Shape { abstract area(): number; }Complete Example
Trying to instantiate the abstract class is a compile-time error in C++:
…and a run-time error in Python:
#include <iostream>
#include <memory>
#include <numbers>
#include <vector>
class Shape {
public:
virtual double area() const = 0;
virtual const char* name() const = 0;
virtual ~Shape() { std::cout << " ~Shape\n"; }
};
class Circle : public Shape {
double r_;
public:
explicit Circle(double r) : r_(r) {}
double area() const override { return std::numbers::pi * r_ * r_; }
const char* name() const override { return "circle"; }
~Circle() override { std::cout << " ~Circle\n"; }
};
class Rect : public Shape {
double w_, h_;
public:
Rect(double w, double h) : w_(w), h_(h) {}
double area() const override { return w_ * h_; }
const char* name() const override { return "rect"; }
~Rect() override { std::cout << " ~Rect\n"; }
};
double totalArea(const std::vector<std::unique_ptr<Shape>>& shapes) {
double t = 0;
for (const auto& s : shapes) t += s->area(); // works for any Shape
return t;
}
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(1.0));
shapes.push_back(std::make_unique<Rect>(2.0, 3.0));
for (const auto& s : shapes) std::cout << s->name() << " area "
…circle area 3.14159
rect area 6
total 9.14159
destroying through Shape pointers:
~Rect
~Shape
~Circle
~Shapeclass Shape {
public:
virtual double area() const = 0;
virtual ~Shape() = default;
};
int main() {
Shape s;
return 0;
}m04_abstract_fail.cpp:7:11: error: variable type 'Shape' is an abstract classfrom abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
class Square(Shape):
def __init__(self, s): self.s = s
def area(self): return self.s * self.s
print(Square(3).area())
try:
Shape()
except TypeError as e:
print("TypeError:", e)9
TypeError: Can't instantiate abstract class Shape with abstract method areaStep-by-Step Walkthrough
Shape declares *what* every shape can do; it has no data and cannot be instantiated.
Circle and Rect supply the *how*. override asks the compiler to check that a base function with the same signature exists.
totalArea depends only on the abstraction, so new shapes need no change to it.
When the vector is destroyed, each unique_ptr<Shape> deletes its object through a base pointer. Because ~Shape is virtual, the derived destructor runs first, then the base destructor.
classDiagram
class Shape {
<<abstract>>
+area() double
+name() string
}
Shape <|-- Circle
Shape <|-- Rect
Client ..> Shape : usesCommon Mistakes
Missing virtual destructor. Deleting a Circle through a Shape* whose destructor is not virtual is undefined behaviour — typically only ~Shape runs and the Circle part leaks.
Fat interfaces. An IDevice with 25 methods forces every implementation (and every test fake) to implement all of them. Split by client need.
Leaking implementation details into the interface (getInternalBuffer()), which freezes the implementation.
Confusing abstraction with encapsulation. Abstraction decides the public surface; encapsulation protects what is behind it.
Trade-offs and Alternatives
Abstract base class / interface Clear contract, run-time substitution
Virtual call cost, heap allocation usually needed C++ templates / concepts (static polymorphism) Zero-overhead, inlining
Implementation chosen at compile time; longer compile times Function objects (std::function, lambdas)
Lightweight for single-operation interfaces No grouping of related operations
Duck typing (Python) / structural typing (TS) No declaration needed
Errors appear only at run time (Python) or are purely static (TS)
Real-World Use
Storage drivers (BlockDevice), payment gateways (PaymentProvider), logging (ILogger), and hardware abstraction layers in embedded systems (CanController with STM32 and simulator implementations) all hide implementations behind small interfaces.
Practice
Q1. (Easy) What makes a C++ class abstract?
Answer: At least one pure virtual function (= 0), declared or inherited and not overridden.
Q2. (Medium) Write the contract (pre/postconditions) for Stack::pop().
Answer: Precondition: the stack is not empty. Postcondition: the size decreases by one and the previous top element is removed (and returned, if the API returns it). Invariant: size ≥ 0.
Q3. (Advanced) Design a TemperatureSource interface used by a cluster display, with one hardware implementation and one fake for tests. Show that the display code works with both.
#include <iostream>
class TemperatureSource {
public:
virtual double celsius() const = 0;
virtual ~TemperatureSource() = default;
};
class AdcTemperatureSource : public TemperatureSource { // pretend hardware
int raw_;
public:
explicit AdcTemperatureSource(int raw) : raw_(raw) {}
double celsius() const override { return raw_ * 0.1 - 40.0; } // scaling from the sensor datasheet
};
class FakeTemperatureSource : public TemperatureSource {
double value_;
public:
explicit FakeTemperatureSource(double v) : value_(v) {}
double celsius() const override { return value_; }
};
void showCoolant(const TemperatureSource& src) {
double t = src.celsius();
std::cout << "coolant " << t << " C" << (t > 110 ? " OVERHEAT" : "") << "\n";
}
int main() {
showCoolant(AdcTemperatureSource(1290));
showCoolant(FakeTemperatureSource(115.0));
}coolant 89 C
coolant 115 C OVERHEATInterview Question
Q1. "When should a C++ base class have a virtual destructor?"
Answer: Whenever objects of derived classes may be deleted through a pointer to the base (including via std::unique_ptr<Base>). Without it, delete basePtr is undefined behaviour. Rule of thumb: a base class's destructor should be either public and virtual (polymorphic deletion allowed) or protected and non-virtual (deletion through the base forbidden). Note that std::shared_ptr<Base> created by make_shared<Derived> happens to call the right destructor even without virtual, because it captures the deleter — but relying on that is fragile.
Quick Quiz
Q1. Can you create an object of an abstract class?
Answer: No — only of concrete derived classes that implement every pure virtual function.
Q2. What does override add?
Answer: A compile-time check that the function really overrides a virtual base function (catches signature typos).
Q3. Abstraction vs encapsulation in one sentence each?
Answer: Abstraction chooses which operations to expose; encapsulation hides and protects the state behind them.
Summary
Abstraction exposes essentials; interfaces and abstract classes express it in code.
Contracts = preconditions, postconditions, invariants.
Polymorphic C++ bases need a virtual (or protected) destructor.
Related: Module 8 (polymorphism), Module 19 (ISP, LSP), Module 65 (test doubles).