Introduction to Programming Paradigms
Prerequisites: basic programming in any language.
Learning Objectives
Explain what a programming paradigm is and name the major ones.
Define object-oriented programming precisely.
Compare procedural and object-oriented code for the same task.
Recognise situations where OOP is *not* the best tool.
Prerequisites: basic programming in any language.
Intuition and Definition
A paradigm is a way of organising a program — a set of ideas about where data lives, how behaviour is expressed and how parts communicate. It is not a language: C++, Python, C# and TypeScript all support several paradigms, and real programs mix them.
A sequence of steps acting on data passed between functions functions, structs, loops
Objects that combine state with the behaviour that keeps that state valid classes, interfaces, virtual dispatch
Pure functions that transform immutable values higher-order functions, map/filter/reduce
Describe *what* result you want, not *how* to compute it SQL, HTML, regex, build files
Facts and rules; the engine searches for answers Prolog, Datalog
Code reacts to events (clicks, messages, interrupts) callbacks, observers, event loops
Several activities make progress at the same time threads, actors, async/await
Lay data out for how it is processed (often arrays of fields) structure-of-arrays, ECS in games
Object-oriented programming (OOP) organises software around objects — units that combine state (data) with behaviour (operations) — and relies on four ideas: abstraction (expose what matters, hide the rest), encapsulation (protect state behind operations that keep it valid), inheritance (define a type as a specialisation of another) and polymorphism (one interface, many implementations).
Why It Matters
Modularity: each object owns a piece of state; bugs are easier to localise.
Controlled state: only an object's own methods may change its data, so invariants ("balance never negative") are enforced in one place.
Extensibility: new behaviour can be added as a new class implementing an existing interface, without editing callers.
Testability: objects with explicit dependencies can be tested in isolation.
Domain modelling: classes like Invoice, Sensor or Account mirror the vocabulary of the problem.
The costs are real too: extra indirection, more types to read, memory overhead per object, and the temptation to over-engineer.
Syntax and Language Notes
The same task — compute an order total with a 10% discount over 100 — in three styles. Procedural code passes data to functions; OOP code puts the rule next to the data; functional code builds the result from expressions without mutation.
// procedural: data and functions are separate
struct Line { double price; int qty; };
double total(const Line* lines, int n) {
double t = 0;
for (int i = 0; i < n; ++i) t += lines[i].price * lines[i].qty;
return t > 100 ? t * 0.9 : t;
}# functional style in Python: no mutation, one expression
def total(lines):
t = sum(price * qty for price, qty in lines)
return t * 0.9 if t > 100 else t// object-oriented in TypeScript: the rule lives with the data
class Order {
private lines: { price: number; qty: number }[] = [];
add(price: number, qty: number) { this.lines.push({ price, qty }); }
total(): number {
const t = this.lines.reduce((s, l) => s + l.price * l.qty, 0);
return t > 100 ? t * 0.9 : t;
}
}Complete Example
#include <iostream>
#include <numeric>
#include <vector>
// ---- procedural ----
struct Line { double price; int qty; };
double proceduralTotal(const std::vector<Line>& lines) {
double t = 0;
for (const Line& l : lines) t += l.price * l.qty;
return t > 100 ? t * 0.9 : t;
}
// ---- object-oriented ----
class Order {
std::vector<Line> lines; // state is private
public:
void add(double price, int qty) { // behaviour guards the state
if (price < 0 || qty <= 0) throw std::invalid_argument("bad line");
lines.push_back({price, qty});
}
double total() const {
double t = 0;
for (const Line& l : lines) t += l.price * l.qty;
return t > 100 ? t * 0.9 : t;
}
};
// ---- functional ----
double functionalTotal(const std::vector<Line>& lines) {
double t = std::accumulate(lines.begin(), lines.end(), 0.0,
[](double acc, const Line& l) { return acc + l.price * l.qty; });
return t > 100 ? t * 0.9 : t;
}
int main() {
std::vector<Line> lines{{40.0, 2}, {25.0, 1}};
std::cout << "procedural: " << proceduralTotal(lines) << "\n";
Order order;
order.add(40.0, 2);
order.add(25.0, 1);
…procedural: 94.5
oop: 94.5
functional: 94.5
rejected: bad lineStep-by-Step Walkthrough
All three versions compute 40×2 + 25×1 = 105, which is over 100, so the 10% discount gives 94.5.
In the procedural version any code can build an invalid Line (negative price) and pass it in — nothing protects the data.
In the OOP version lines is private: the only way to add a line is add, which rejects invalid input. The rule "a line has a non-negative price and a positive quantity" is enforced in exactly one place.
The functional version has no mutable variables at all; it folds the lines into a total with std::accumulate.
flowchart LR
P[Procedural<br/>data + functions] --> R1[anyone can modify data]
O[Object-oriented<br/>data inside Order] --> R2[only Order's methods change it]
F[Functional<br/>pure expressions] --> R3[no mutation at all]Common Mistakes
Treating OOP as "use classes everywhere". A class with only getters and setters adds ceremony without protecting anything.
**Thinking a language *is* a paradigm.** C++ and Python are multi-paradigm; good code chooses per problem.
Ignoring functional techniques inside OOP. Immutable value objects and pure helper functions make classes simpler.
Modelling nouns instead of responsibilities. A class should exist because it owns state and rules, not because a noun appears in the requirements.
Trade-offs and Alternatives
Prefer something else when… State must stay consistent across many operations
A short script transforms input to output once Several implementations share one interface (drivers, payment providers)
A data pipeline is a chain of transformations (functional style)
The domain has long-lived entities with rules A tight numeric loop must be cache-friendly (data-oriented layout) Teams need clear module boundaries
The problem is naturally declarative (queries, configuration)
Real-World Use
GUI toolkits, game engines, device drivers, banking systems and automotive ECU software (AUTOSAR software components) are organised around objects or object-like components. Data science notebooks and build scripts are mostly procedural/functional; databases are declarative.
Practice
Q1. (Easy) Name the paradigm: "SELECT name FROM users WHERE age > 30".
Answer: Declarative — it states the desired result; the database decides how to compute it.
Q2. (Medium) Give two reasons the OOP Order above is safer than the procedural version.
Answer: (1) The lines are private, so no outside code can insert an invalid line. (2) The validation rule lives in one method (add), so changing it later is a single edit rather than a hunt through every caller.
Q3. (Advanced) A game updates the position of 1,000,000 particles every frame. Why might a Particle class with virtual update() be slower than separate arrays x[], y[], vx[], vy[]?
Answer: Each object is allocated separately (pointer chasing, poor cache locality), each call goes through a virtual dispatch the compiler can't inline, and loading a whole object brings unused fields into cache. Structure-of-arrays lets the CPU stream contiguous memory and vectorise the loop — the data-oriented paradigm.
Interview Question
Q1. "Is OOP always better than procedural programming?" Answer: No. OOP pays off when you have long-lived state with invariants, several interchangeable implementations, or large teams that need stable module boundaries. For small scripts, stateless transformations and performance-critical numeric loops, procedural, functional or data-oriented code is usually simpler and faster. A strong answer names the trade-off (indirection and ceremony versus encapsulation and extensibility) and gives an example of each.
Quick Quiz
Q1. Which four ideas characterise OOP?
Answer: Abstraction, encapsulation, inheritance and polymorphism.
Q2. True or false: Python is an object-oriented language, so it cannot be used functionally.
Answer: False — Python, like most modern languages, is multi-paradigm.
Q3. What does "controlled state" mean?
Answer: Only the object's own methods may change its data, so its rules are enforced in one place.
Summary
A paradigm is a way of organising code; languages support several.
OOP combines state and behaviour in objects and relies on abstraction, encapsulation, inheritance and polymorphism.
Benefits: modularity, controlled state, extensibility, testability. Costs: indirection, complexity, overhead.
Choose the paradigm per problem. Related: Module 2 (classes and objects), Module 59 (functional OOP), Module 70 (performance).