Jour 53 Day 53 · vendredi 23 octobre 2026 Friday 23 October 2026 CS Intermédiaire

OOP vs programmation fonctionnelle OOP vs functional programming

Encapsulation, fonctions pures, immutabilité : dépasser la fausse opposition OOP/FP et montrer en entretien que vous savez choisir le bon style au bon endroit. Encapsulation, pure functions, immutability: move past the fake OOP/FP rivalry and show in an interview that you can pick the right style for the right layer.

L’essentiel

L’orienté objet (OOP) et la programmation fonctionnelle (FP) sont deux façons d’organiser le code — pas deux camps ennemis. L’OOP regroupe données et comportements dans des objets qui encapsulent leur état ; la FP construit le programme en composant des fonctions pures qui transforment des données immuables.

L’OOP réelle, ce n’est pas « des classes partout et de l’héritage sur cinq niveaux » : c’est l’encapsulation (l’état privé ne se modifie que par des méthodes qui garantissent les invariants), le polymorphisme (plusieurs types derrière une même interface — on appelle shape.area() sans connaître le type concret) et la composition plutôt que l’héritage.

La FP réelle, ce n’est pas « des monades et du jargon » : c’est écrire un maximum de fonctions pures (même entrée → même sortie, zéro effet de bord), garder les données immuables (on retourne une nouvelle valeur au lieu de modifier), repousser les effets de bord (I/O, DB, réseau) aux frontières du programme, et utiliser des fonctions d’ordre supérieur (map, filter, reduce) plutôt que des boucles à accumulateur.

L’opposition est largement artificielle : le JS/TS moderne mélange les deux en permanence — des classes pour les services et l’état, map/filter et l’immutabilité pour la donnée. React lui-même est passé des classes aux fonctions sans cesser d’encapsuler l’état. Le vrai ennemi commun des deux styles porte un nom : l’état mutable partagé.

Comment ça marche

OOPFP
ÉtatEncapsulé dans l’objet, mutable mais contrôléImmuable : on crée de nouvelles valeurs
RéutilisationPolymorphisme, composition d’objetsComposition de fonctions, higher-order functions
TestsNécessite d’instancier, parfois de mockerFonction pure = entrée/sortie, trivial à tester
Effets de bordDispersés dans les méthodes (risque)Repoussés aux frontières (I/O aux bords)
Bug typiqueÉtat modifié par surprise à distanceSur-abstraction, pipelines illisibles

Le même besoin — total des commandes livrées, en centimes — dans les deux styles :

// Style impératif : une boucle, un accumulateur, des mutations
function total(orders: Order[]): number {
  let sum = 0;
  for (let i = 0; i < orders.length; i++) {
    if (orders[i].status === "delivered") {
      sum += orders[i].amount;        // mutation de l'accumulateur
    }
  }
  return sum;
}

// Style FP : un pipeline déclaratif, aucune mutation
const total = (orders: Order[]): number =>
  orders
    .filter(o => o.status === "delivered") // garder les livrées
    .map(o => o.amount)                    // en extraire le montant
    .reduce((sum, a) => sum + a, 0);       // les additionner (0 = départ)

Les deux sont correctes. La version FP se lit comme la spécification (« filtre, extrais, somme »), chaque étape se teste isolément, aucun état intermédiaire à suivre mentalement. L’impérative peut être plus rapide sur des volumes énormes (une seule passe) — mais dans 99 % du code applicatif, la lisibilité gagne.

Pourquoi l’état mutable partagé est la vraie racine des bugs : quand deux morceaux de code peuvent modifier la même structure, chacun peut casser les hypothèses de l’autre — le fameux « qui a modifié ce tableau ? » à 18h un vendredi. L’OOP répond en contrôlant la mutation (état privé, méthodes garantes des invariants) ; la FP répond en la supprimant (données immuables). Deux stratégies différentes contre le même ennemi.

En survol : le pattern matching et les types sommes viennent du monde FP et infusent partout. En TypeScript, une union discriminée type Result = { ok: true; data: User } | { ok: false; error: string } force le switch à traiter chaque cas — le compilateur signale le cas oublié. C’est la FP qui a gagné cette bataille-là dans le typage moderne.

💡 Composition > héritage — l’héritage crée un couplage fort et fragile : modifier la classe mère casse silencieusement les filles, et la hiérarchie finit toujours par mentir (le canard en plastique hérite de Canard mais ne vole pas). Composer — un objet possède des capacités plutôt qu’il n’en hérite — reste flexible : c’est le conseil du Gang of Four… dès 1994, et la raison du passage de React aux hooks et à la composition de composants.

Concepts clés à maîtriser

  • Fonction pure : même entrée → même sortie, et aucun effet de bord (pas d’I/O, pas de mutation externe, pas de Date.now() ni de Math.random() cachés). Conséquence directe : testable sans mock, mémoïsable, parallélisable, déplaçable sans risque.
  • Immutabilité : const next = [...items, newItem] plutôt que items.push(newItem) — on retourne une nouvelle valeur au lieu de modifier en place.
  • Effets de bord aux frontières : le pattern « functional core, imperative shell » — un cœur de logique pure (calculs, décisions) entouré d’une coquille fine qui parle au monde (DB, HTTP, fichiers). On teste le cœur sans mock, la coquille en intégration.
  • Fonctions d’ordre supérieur : fonctions qui prennent ou retournent des fonctions. map/filter/reduce en sont, mais aussi les middlewares Express, les hooks React, les décorateurs.
  • Encapsulation & invariants : la valeur de l’OOP n’est pas la syntaxe class, c’est de rendre les états invalides impossibles — un BankAccount dont le solde ne se modifie que par deposit/withdraw qui vérifient les règles.
  • Le style « FP light » pragmatique en équipe : immutabilité par défaut, fonctions pures pour la logique métier, map/filter/reduce pour les transformations, effets aux bords — sans dogme, sans pipe(curry(flip(...))) illisible. C’est le style dominant des codebases TS modernes.
        functional core, imperative shell
  ┌──────────────────────────────────────────┐
  │  Shell impérative (effets de bord)       │
  │   HTTP ─ DB ─ fichiers ─ horloge         │
  │   ┌──────────────────────────────────┐   │
  │   │   Cœur pur (logique métier)      │   │
  │   │   calculs, validation, décisions │   │
  │   │   → testable sans aucun mock     │   │
  │   └──────────────────────────────────┘   │
  └──────────────────────────────────────────┘

En entretien

« C’est quoi une fonction pure et pourquoi c’est testable ? » — Une fonction dont la sortie ne dépend que de ses arguments, et qui ne produit aucun effet de bord observable. Testable parce que le test est trivial : on donne une entrée, on vérifie la sortie — pas de mock, pas de setup de DB, pas d’ordre d’exécution. Bonus : elle est aussi mémoïsable et sûre à paralléliser, précisément parce que rien d’extérieur n’entre en jeu.

« Pourquoi dit-on composition plutôt qu’héritage ? » — L’héritage couple fortement la fille à l’implémentation de la mère : tout changement de la mère se propage silencieusement, et les hiérarchies profondes deviennent fausses avec le temps. La composition assemble des capacités (class Car { engine: Engine }) : couplage faible, testable pièce par pièce, recombinable. L’héritage garde un usage légitime : de vraies relations « est-un » stables, peu profondes.

« OOP ou FP : tu choisis quoi ? » — Les deux, selon la couche. Logique métier et transformations de données : style fonctionnel (pur, immuable, map/filter). Services avec cycle de vie, état encapsulé, polymorphisme (plusieurs providers derrière une interface) : style objet. Le JS/TS moderne mélange les deux — la vraie compétence est de garder l’état mutable partagé au minimum, quel que soit le style.

« Quel est le problème de l’état mutable partagé ? » — Deux morceaux de code qui modifient la même structure cassent mutuellement leurs hypothèses : bugs d’action à distance, impossibles à reproduire, pires en concurrence (race conditions). L’OOP le contrôle par l’encapsulation, la FP le supprime par l’immutabilité — les deux réponses valent mieux qu’une variable globale modifiée partout.

« map/filter/reduce : tu peux me les expliquer ? » — map transforme chaque élément (n → n), filter garde ceux qui passent un prédicat (n → ≤n), reduce replie la liste en une seule valeur via un accumulateur. Ensemble ils remplacent la boucle à accumulateur par un pipeline déclaratif où chaque étape est nommée et testable. Piège à mentionner : reduce sans valeur initiale sur un tableau vide lance une exception.

Pièges & idées reçues

⚠️ « FP = pas de classes » / « OOP = pas de fonctions » — faux dans les deux sens. Une classe TS avec des méthodes pures et un état immuable est parfaitement fonctionnelle dans l’esprit ; un module de fonctions qui mutent un objet global partagé n’a de fonctionnel que la syntaxe. Le style se juge à la gestion de l’état, pas aux mots-clés.

  • « L’immutabilité est trop lente » — copier a un coût, mais dans le code applicatif il est presque toujours négligeable (et les moteurs JS optimisent). On profile avant d’optimiser ; au pire, on mute localement dans une fonction qui reste pure vue de l’extérieur.
  • « Plus il y a de classes, plus c’est de l’OOP » — l’OOP se juge aux invariants protégés, pas au nombre de classes. Une classe anémique (getters/setters sans logique) n’encapsule rien : c’est un struct avec des cérémonies.
  • const en JS n’immobilise que la référence — const arr = []; arr.push(1) est légal. L’immutabilité de la valeur est une discipline (readonly, Object.freeze, Immer), pas un mot-clé.
  • Le dogmatisme dans les deux sens : tout réécrire en pipe/curry illisible est aussi nuisible qu’une hiérarchie d’héritage de six niveaux. En équipe, le style « FP light » gagne : pur par défaut, pragmatique aux bords.

🎤 En entretien — ne choisissez jamais un camp. La réponse qui marque : « les deux outillent le même problème — l’état mutable partagé — l’un en l’encadrant, l’autre en le supprimant ; je prends le style qui rend chaque couche la plus simple à tester ». Vous venez de montrer du recul, pas une religion.

Pour aller plus loin

The essentials

Object-oriented programming (OOP) and functional programming (FP) are two ways of organizing code — not two enemy camps. OOP groups data and behavior into objects that encapsulate their state; FP builds the program by composing pure functions that transform immutable data.

Real OOP is not “classes everywhere and five levels of inheritance”: it’s encapsulation (private state only changes through methods that guarantee invariants), polymorphism (several types behind one interface — you call shape.area() without knowing the concrete type) and composition over inheritance.

Real FP is not “monads and jargon”: it’s writing as many pure functions as possible (same input → same output, zero side effects), keeping data immutable (return a new value instead of modifying), pushing side effects (I/O, DB, network) to the program’s boundaries, and using higher-order functions (map, filter, reduce) instead of accumulator loops.

The rivalry is largely artificial: modern JS/TS mixes both constantly — classes for services and state, map/filter and immutability for data. React itself moved from classes to functions without ever ceasing to encapsulate state. The real common enemy of both styles has a name: shared mutable state.

How it works

OOPFP
StateEncapsulated in the object, mutable but controlledImmutable: you create new values
ReusePolymorphism, object compositionFunction composition, higher-order functions
TestsRequires instantiating, sometimes mockingPure function = input/output, trivial to test
Side effectsScattered across methods (risk)Pushed to the boundaries (I/O at the edges)
Typical bugState changed by surprise at a distanceOver-abstraction, unreadable pipelines

The same need — total of delivered orders, in cents — in both styles:

// Imperative style: a loop, an accumulator, mutations
function total(orders: Order[]): number {
  let sum = 0;
  for (let i = 0; i < orders.length; i++) {
    if (orders[i].status === "delivered") {
      sum += orders[i].amount;        // mutating the accumulator
    }
  }
  return sum;
}

// FP style: a declarative pipeline, no mutation
const total = (orders: Order[]): number =>
  orders
    .filter(o => o.status === "delivered") // keep delivered ones
    .map(o => o.amount)                    // extract the amount
    .reduce((sum, a) => sum + a, 0);       // add them up (0 = seed)

Both are correct. The FP version reads like the specification (“filter, extract, sum”), each step can be tested in isolation, no intermediate state to track mentally. The imperative one can be faster on huge volumes (a single pass) — but in 99% of application code, readability wins.

Why shared mutable state is the real root of bugs: when two pieces of code can modify the same structure, each can break the other’s assumptions — the infamous “who changed this array?” at 6pm on a Friday. OOP answers by controlling mutation (private state, methods guarding invariants); FP answers by removing it (immutable data). Two different strategies against the same enemy.

A quick look ahead: pattern matching and sum types come from the FP world and are spreading everywhere. In TypeScript, a discriminated union type Result = { ok: true; data: User } | { ok: false; error: string } forces the switch to handle every case — the compiler flags the forgotten one. That’s a battle FP won in modern type systems.

💡 Composition > inheritance — inheritance creates a strong, brittle coupling: changing the parent class silently breaks the children, and the hierarchy always ends up lying (the rubber duck inherits from Duck but doesn’t fly). Composing — an object has capabilities rather than inheriting them — stays flexible: it’s the Gang of Four’s advice… from 1994, and the reason React moved to hooks and component composition.

Key concepts to master

  • Pure function: same input → same output, and no side effects (no I/O, no external mutation, no hidden Date.now() or Math.random()). Direct consequence: testable without mocks, memoizable, parallelizable, safe to move around.
  • Immutability: const next = [...items, newItem] rather than items.push(newItem) — return a new value instead of modifying in place.
  • Side effects at the boundaries: the “functional core, imperative shell” pattern — a core of pure logic (computations, decisions) surrounded by a thin shell that talks to the world (DB, HTTP, files). You test the core without mocks, the shell in integration.
  • Higher-order functions: functions that take or return functions. map/filter/reduce qualify, but so do Express middlewares, React hooks, decorators.
  • Encapsulation & invariants: OOP’s value is not the class syntax, it’s making invalid states impossible — a BankAccount whose balance only changes through deposit/withdraw methods that enforce the rules.
  • The pragmatic “FP light” style for teams: immutability by default, pure functions for business logic, map/filter/reduce for transformations, effects at the edges — no dogma, no unreadable pipe(curry(flip(...))). It’s the dominant style of modern TS codebases.
        functional core, imperative shell
  ┌──────────────────────────────────────────┐
  │  Imperative shell (side effects)         │
  │   HTTP ─ DB ─ files ─ clock              │
  │   ┌──────────────────────────────────┐   │
  │   │   Pure core (business logic)     │   │
  │   │   computation, validation,       │   │
  │   │   decisions → testable w/o mocks │   │
  │   └──────────────────────────────────┘   │
  └──────────────────────────────────────────┘

In an interview

“What is a pure function and why is it testable?” — A function whose output depends only on its arguments, and which produces no observable side effect. Testable because the test is trivial: feed an input, check the output — no mocks, no DB setup, no execution order. Bonus: it’s also memoizable and safe to parallelize, precisely because nothing external is involved.

“Why do we say composition over inheritance?” — Inheritance tightly couples the child to the parent’s implementation: any change to the parent propagates silently, and deep hierarchies become wrong over time. Composition assembles capabilities (class Car { engine: Engine }): loose coupling, testable piece by piece, recombinable. Inheritance keeps one legitimate use: genuine, stable, shallow “is-a” relationships.

“OOP or FP: which do you pick?” — Both, depending on the layer. Business logic and data transformations: functional style (pure, immutable, map/filter). Services with a lifecycle, encapsulated state, polymorphism (several providers behind one interface): object style. Modern JS/TS mixes both — the real skill is keeping shared mutable state to a minimum, whatever the style.

“What’s the problem with shared mutable state?” — Two pieces of code modifying the same structure break each other’s assumptions: action-at-a-distance bugs, impossible to reproduce, worse under concurrency (race conditions). OOP controls it through encapsulation, FP removes it through immutability — either answer beats a global variable modified everywhere.

“Can you explain map/filter/reduce?” — map transforms each element (n → n), filter keeps those passing a predicate (n → ≤n), reduce folds the list into a single value via an accumulator. Together they replace the accumulator loop with a declarative pipeline where each step is named and testable. Trap worth mentioning: reduce without an initial value throws on an empty array.

Pitfalls & misconceptions

⚠️ “FP = no classes” / “OOP = no functions” — wrong both ways. A TS class with pure methods and immutable state is perfectly functional in spirit; a module of functions mutating a shared global object is functional in syntax only. Judge the style by how state is handled, not by the keywords.

  • “Immutability is too slow” — copying has a cost, but in application code it’s almost always negligible (and JS engines optimize it). Profile before optimizing; at worst, mutate locally inside a function that stays pure from the outside.
  • “More classes = more OOP” — OOP is judged by the invariants it protects, not by class count. An anemic class (getters/setters with no logic) encapsulates nothing: it’s a struct with ceremonies.
  • const in JS only freezes the reference — const arr = []; arr.push(1) is legal. Value immutability is a discipline (readonly, Object.freeze, Immer), not a keyword.
  • Dogmatism in either direction: rewriting everything into unreadable pipe/curry chains is as harmful as a six-level inheritance hierarchy. In a team, “FP light” wins: pure by default, pragmatic at the edges.

🎤 In an interview — never pick a camp. The answer that lands: “both tool the same problem — shared mutable state — one by fencing it in, the other by removing it; I take the style that makes each layer simplest to test”. You’ve just shown perspective, not religion.

Going further

S'entraîner sur ce sujet → Practice this topic →