POO vs Fonctionnel — 03 — Comparaison directe : le même problème, deux solutions

POO vs fonctionnel sur les mêmes problèmes concrets : gestion d'état, gestion d'erreurs, testabilité, concurrence. Code TypeScript et Python côte à côte.

03 — Comparaison directe : le même problème, deux solutions

Ce que tu vas apprendre

  • Le même problème résolu en POO et en fonctionnel côte à côte
  • Gestion d'état, gestion d'erreurs, testabilité, concurrence
  • Quand chaque approche est plus adaptée
  • Ce que les benchmarks et études disent sur la maintenabilité

Prérequis


Problème 1 : traiter une liste de commandes

Contexte : calculer le revenu total des commandes livrées d'un mois donné, avec une remise de 10% sur les commandes de plus de 500€.

Solution POO

typescriptclass OrderProcessor {
  private orders: Order[];

  constructor(orders: Order[]) {
    this.orders = orders;
  }

  getMonthlyRevenue(year: number, month: number): number {
    let total = 0;

    for (const order of this.orders) {
      if (order.status !== "delivered") continue;
      if (order.date.getFullYear() !== year) continue;
      if (order.date.getMonth() + 1 !== month) continue;

      const amount = order.total > 500
        ? order.total * 0.9
        : order.total;

      total += amount;
    }

    return total;
  }
}

const processor = new OrderProcessor(orders);
const revenue = processor.getMonthlyRevenue(2026, 6);

Solution fonctionnelle

typescript// Fonctions pures — chacune fait une seule chose
const isDelivered = (order: Order): boolean =>
  order.status === "delivered";

const isFromMonth = (year: number, month: number) => (order: Order): boolean =>
  order.date.getFullYear() === year && order.date.getMonth() + 1 === month;

const applyBulkDiscount = (order: Order): number =>
  order.total > 500 ? order.total * 0.9 : order.total;

const sum = (numbers: number[]): number =>
  numbers.reduce((acc, n) => acc + n, 0);

// Composition — lisible comme une phrase
const getMonthlyRevenue = (orders: Order[], year: number, month: number): number =>
  sum(
    orders
      .filter(isDelivered)
      .filter(isFromMonth(year, month))
      .map(applyBulkDiscount)
  );

const revenue = getMonthlyRevenue(orders, 2026, 6);

En Python :

pythonfrom typing import List
from functools import reduce

# POO
class OrderProcessor:
    def __init__(self, orders: List[dict]) -> None:
        self._orders = orders

    def get_monthly_revenue(self, year: int, month: int) -> float:
        total = 0.0
        for order in self._orders:
            if order["status"] != "delivered":
                continue
            if order["date"].year != year or order["date"].month != month:
                continue
            amount = order["total"] * 0.9 if order["total"] > 500 else order["total"]
            total += amount
        return total

# Fonctionnel
def is_delivered(order: dict) -> bool:
    return order["status"] == "delivered"

def is_from_month(year: int, month: int):
    return lambda order: order["date"].year == year and order["date"].month == month

def apply_bulk_discount(order: dict) -> float:
    return order["total"] * 0.9 if order["total"] > 500 else order["total"]

def get_monthly_revenue(orders: List[dict], year: int, month: int) -> float:
    filtered = filter(is_delivered, orders)
    filtered = filter(is_from_month(year, month), filtered)
    amounts = map(apply_bulk_discount, filtered)
    return sum(amounts)

La version fonctionnelle est plus facile à tester : chaque fonction peut être testée isolément sans instancier un objet. isDelivered, applyBulkDiscount et sum sont des fonctions pures — un input, un output.


Problème 2 : gestion d'erreurs

Contexte : lire un fichier de config JSON, valider les champs obligatoires, retourner une config ou une erreur.

Solution POO (exceptions)

typescriptclass ConfigLoader {
  load(path: string): Config {
    let raw: string;
    try {
      raw = fs.readFileSync(path, "utf-8");
    } catch (e) {
      throw new Error(`Fichier introuvable : ${path}`);
    }

    let data: unknown;
    try {
      data = JSON.parse(raw);
    } catch (e) {
      throw new Error(`JSON invalide dans ${path}`);
    }

    if (!data || typeof data !== "object") {
      throw new Error("Config vide");
    }

    const obj = data as Record<string, unknown>;

    if (typeof obj.apiUrl !== "string" || !obj.apiUrl) {
      throw new Error("apiUrl manquant ou invalide");
    }
    if (typeof obj.timeout !== "number" || obj.timeout <= 0) {
      throw new Error("timeout manquant ou invalide");
    }

    return { apiUrl: obj.apiUrl, timeout: obj.timeout };
  }
}

// Usage
try {
  const config = new ConfigLoader().load("./config.json");
  startApp(config);
} catch (e) {
  console.error("Erreur de config :", e.message);
  process.exit(1);
}

Solution fonctionnelle (Result type)

typescripttype Result<T, E = string> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function readFile(path: string): Result<string> {
  try {
    return { ok: true, value: fs.readFileSync(path, "utf-8") };
  } catch {
    return { ok: false, error: `Fichier introuvable : ${path}` };
  }
}

function parseJson(raw: string): Result<unknown> {
  try {
    return { ok: true, value: JSON.parse(raw) };
  } catch {
    return { ok: false, error: "JSON invalide" };
  }
}

function validateConfig(data: unknown): Result<Config> {
  if (!data || typeof data !== "object") {
    return { ok: false, error: "Config vide" };
  }
  const obj = data as Record<string, unknown>;

  if (typeof obj.apiUrl !== "string" || !obj.apiUrl) {
    return { ok: false, error: "apiUrl manquant ou invalide" };
  }
  if (typeof obj.timeout !== "number" || obj.timeout <= 0) {
    return { ok: false, error: "timeout manquant ou invalide" };
  }

  return { ok: true, value: { apiUrl: obj.apiUrl, timeout: obj.timeout } };
}

// Chaînage explicite — pas de try/catch imbriqués
function loadConfig(path: string): Result<Config> {
  const file = readFile(path);
  if (!file.ok) return file;

  const json = parseJson(file.value);
  if (!json.ok) return json;

  return validateConfig(json.value);
}

// Usage
const result = loadConfig("./config.json");
if (!result.ok) {
  console.error("Erreur de config :", result.error);
  process.exit(1);
}
startApp(result.value);

La version fonctionnelle rend le chemin d'erreur explicite dans le type. Il est impossible d'oublier de gérer une erreur — le compilateur TypeScript le détecte.


Problème 3 : gestion d'état partagé

Contexte : un compteur de visites partagé entre plusieurs handlers HTTP.

Solution POO (état mutable partagé)

typescriptclass VisitCounter {
  private count: number = 0;

  increment(): void {
    this.count++;
  }

  getCount(): number {
    return this.count;
  }
}

const counter = new VisitCounter();

// Problème en concurrence :
// Requête A lit count = 5
// Requête B lit count = 5
// Requête A écrit count = 6
// Requête B écrit count = 6  ← perd l'incrémentation de A

Solution fonctionnelle (état explicite et immutable)

typescript// L'état est passé explicitement — pas de surprise
type State = { visitCount: number };

function incrementVisit(state: State): State {
  return { ...state, visitCount: state.visitCount + 1 };
}

// Avec un gestionnaire d'état centralisé (Redux-like)
let appState: State = { visitCount: 0 };

function dispatch(action: (s: State) => State): State {
  appState = action(appState); // Mise à jour centralisée
  return appState;
}

// Usage
dispatch(incrementVisit);
dispatch(incrementVisit);
console.log(appState.visitCount); // 2

En environnement concurrent (Node.js, Python asyncio), la version fonctionnelle avec état immutable évite les race conditions — si l'état ne mute pas, il n'y a rien à synchroniser.


Comparaison sur 5 dimensions

Tableau comparatif POO vs Fonctionnel

Les 5 dimensions

Testabilité : le fonctionnel gagne sur les transformations de données (fonctions pures, pas de mock). La POO gagne sur les domaines complexes avec cycle de vie.

Lisibilité : la POO est plus intuitive pour les développeurs débutants. Le fonctionnel est plus lisible pour des pipelines de transformation.

Concurrence : le fonctionnel gagne — l'immutabilité élimine les race conditions.

Modélisation du domaine : la POO gagne — les objets avec état et comportement modélisent mieux les entités métier.

Maintenabilité : les deux. Les études mesurent des résultats mitigés selon le contexte.


Ce que dit la recherche

Raymond Hettinger (core developer Python) a montré que les comprehensions et les générateurs Python, inspirés du FP, produisent du code plus lisible et moins sujet aux bugs d'index que les boucles impératives.

Une étude de Nanz & Furia (2015) sur la productivité des paradigmes (A Comparative Study of Programming Languages in Rosetta Code) montre que les langages fonctionnels produisent en moyenne des solutions plus courtes — mais la corrélation avec la maintenabilité à long terme dépend du contexte.

Robert C. Martin, dans Clean Architecture (2017), note que le paradigme fonctionnel contraint l'assignation (pas de mutation), ce qui le rend naturellement adapté aux systèmes distribués et concurrents.


Tableau de décision

Situation Approche recommandée Pourquoi
Transformation de données (ETL, parsing) Fonctionnel Pipelines clairs, testabilité maximale
Domaine avec entités et cycle de vie POO Encapsulation des invariants
Traitement parallèle / concurrent Fonctionnel Immutabilité = pas de race condition
UI avec état complexe Hybride (Redux/FP pour état, composants OOP) Clarté + réactivité
APIs REST simples Fonctionnel ou hybrid Handlers purs = tests simples
Simulation de systèmes physiques POO Entités avec état — le modèle mental colle

Résumé

Aucun paradigme n'est supérieur à l'autre sur tous les cas. La vraie compétence est de savoir les distinguer :

  • Transformations de données → fonctionnel
  • Entités à cycle de vie → POO
  • Systèmes concurrents → fonctionnel (immutabilité)
  • Cas mixtes → hybride (TypeScript et Python le permettent nativement)

Étape suivante : 04 — L'approche hybride en TypeScript et Python


Sources

  • Martin, R. C. (2017). Clean Architecture: A Craftsman's Guide to Software Structure and Design. Prentice Hall.
  • Nanz, S., & Furia, C. A. (2015). A Comparative Study of Programming Languages in Rosetta Code. Proceedings of ICSE 2015.
  • Wlaschin, S. (2018). Domain Modeling Made Functional. Pragmatic Bookshelf.
  • Meijer, E. (2012). Your Mouse is a Database. ACM Queue, 10(3).
  • Haskell Wiki. (2023). Why Haskell matters. haskell.org/wiki.

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.