Aller au contenu principal
sfiParadigmes de programmation

Paradigmes de programmation

Ce que ce chapitre apporte

  • Définir ce qu'est un paradigme de programmation et le distinguer d'un langage.
  • Nommer et caractériser les quatre paradigmes principaux.
  • Implémenter un même problème en procédural, en orienté objet et en fonctionnel.
  • Reconnaître le rôle de l'état mutable dans les défauts d'un programme.
  • Comparer les approches sur des critères explicites et justifier une recommandation.
Où on va
Deux programmes qui rendent exactement le même service peuvent être structurés de façons irréconciliables. Ce n'est pas une question de langage ni de style : c'est une question de paradigme, c'est-à-dire de la manière dont on se représente ce qu'est un programme. Ce chapitre prend un seul problème (un gestionnaire de stock) et l'écrit quatre fois. L'objectif n'est pas de désigner un vainqueur, c'est de savoir justifier un choix devant une contrainte donnée.

Un paradigme n'est pas un langage

Définition

Un paradigme de programmation est une façon de concevoir et de structurer un programme : ce qu'on considère comme l'unité de base, comment on la compose, et où vit l'information.

Trois confusions à écarter d'emblée.

Un paradigme n'est pas un langage. Python permet d'écrire du procédural, de l'objet et du fonctionnel dans le même fichier. Java, longtemps purement objet, a intégré des fonctions de première classe. Les langages sont majoritairement multi-paradigmes ; ce sont les programmeurs qui choisissent.

Un paradigme n'est pas un niveau de compétence. Écrire en objet n'est pas « plus avancé » qu'écrire en procédural. Un script de 40 lignes enveloppé dans trois classes est plus difficile à lire, pas plus professionnel.

Ce n'est pas non plus une religion. Le vrai code mélange : un service web déclare ses routes de façon déclarative, traite ses données de façon fonctionnelle et gère ses connexions par des objets.

La carte des quatre paradigmes

ParadigmeQuestion centraleUnité de baseLangages typiques
Impératif / procéduralComment faire ?l'instruction, la procédureC, Python, Pascal
Orienté objetQui fait quoi ?la classe, l'objetJava, C++, Python
FonctionnelQu'est-ce que c'est ?la fonction, la valeurHaskell, OCaml, Lisp
DéclaratifQuoi obtenir ?la règle, la requêteSQL, Prolog, HTML
La ligne de partage la plus utile
Elle passe entre impératif (je décris la suite d'étapes à exécuter) et déclaratif (je décris le résultat voulu, un moteur se débrouille). Le fonctionnel penche vers le déclaratif, l'objet reste fondamentalement impératif. Retiens cette question : est-ce que j'écris comment, ou quoi ?

Le même problème, quatre fois

Le cahier des charges, identique pour les quatre versions : gérer une liste de produits, chacun ayant un nom, une quantité en stock et un prix unitaire. Le système doit permettre d'ajouter un produit, de calculer la valeur totale du stock et de rechercher un produit par son nom.

Version impérative / procédurale

On décrit la suite des opérations. Les données sont dans une structure globale, les procédures la modifient.

main.py
Sortie
>_ Prêt à exécuter…

Direct, lisible, minimal. Et une faiblesse structurelle : stock est accessible et modifiable de partout. Rien n'empêche un autre bout de programme d'écrire stock[0]["quantite"] = -5.

Version orientée objet

On regroupe les données et les opérations qui les concernent. L'état devient interne, protégé derrière une interface.

main.py
Sortie
>_ Prêt à exécuter…

La structure que le code installe se lit mieux dessinée : deux classes, une relation de composition, et une constante qui appartient au Stock et non au programme.

Diagramme de classes
Stock
- _produits : dict
+ CAPACITE = 50
+ ajouter(nom, quantite, prix)
+ valeur_totale() : float
+ rechercher(nom) : Produit
Produit
- nom : str
- quantite : int
- prix : float
+ valeur() : float

Le losange plein dit composition : un produit n'a pas d'existence hors de son stock, il disparaît avec lui. C'est bien ce que fait le code, puisque les produits ne vivent que dans le dictionnaire privé.

Deux gains concrets. La contrainte « quantité positive » est vérifiée à un seul endroit, impossible à contourner en passant par l'interface. Et la limite de 50 références appartient au Stock, là où on la cherchera.

Le dictionnaire n'est pas un détail d'implémentation
La version objet indexe par nom : la recherche passe de O(n) à O(1). Ce changement est invisible de l'extérieur, c'est précisément l'intérêt de l'encapsulation. Sur 50 références l'écart ne se mesure pas ; sur 50 000, il décide de tout. C'est le sujet du chapitre suivant.

Version fonctionnelle

On bannit la modification. Chaque opération renvoie un nouvel état au lieu de changer l'ancien, et les fonctions sont pures : même entrée, même sortie, aucun effet de bord.

main.py
Sortie
>_ Prêt à exécuter…

Le résultat remarquable est la ligne s1 est intact. Chaque version du stock existe toujours. Annuler une opération ne demande aucun code : il suffit de reprendre l'état précédent. Rejouer un historique, comparer deux états, auditer une modification deviennent triviaux.

Pourquoi le fonctionnel est le paradigme de la concurrence
Une valeur qui ne change jamais peut être lue par mille processus simultanément sans le moindre verrou. Toute la difficulté du parallélisme (accès concurrents, conditions de course, blocages) vient de l'état mutable partagé. Le supprimer supprime la classe de problèmes entière, et c'est la raison pour laquelle les paradigmes fonctionnels ont resurgi avec les processeurs multi-cœurs.

Version déclarative

On ne décrit ni les étapes, ni les objets : on décrit le résultat voulu. Un moteur choisit comment l'obtenir. C'est exactement ce que fait SQL, et ce bloc s'exécute vraiment.

requete.sql
Résultat
>_ Prêt à exécuter…

Nulle part il n'est écrit comment trier, ni dans quel ordre parcourir la table. La contrainte quantite >= 0 est déclarée une fois, et le moteur la fait respecter : c'est le pendant de la validation écrite dans le constructeur de la version objet, sauf qu'ici aucun code ne l'applique.

D'autres formes de déclaratif
Une expression régulière décrit la forme d'un texte, pas l'automate qui le reconnaît. Le HTML décrit une structure, pas les instructions de rendu. Prolog énonce des faits et des règles, et le moteur cherche les solutions. Une liste en compréhension Python, [p.nom for p in stock if p.quantite < 5], penche du même côté : on décrit l'ensemble voulu, pas la boucle.
Vérification rapidesans note, on peut se reprendre

1.Un paradigme de programmation, c'est…

2.Le principal risque de l'état mutable partagé, c'est…

3.L'encapsulation sert d'abord à…

Ce que change l'état mutable

C'est le vrai clivage entre les paradigmes, et il se voit mieux sur un bug que sur un discours.

main.py
Sortie
>_ Prêt à exécuter…
L'affectation ne copie pas
sauvegarde = stock_initial ne duplique rien : les deux noms désignent le même objet en mémoire. Le bug est silencieux, il se manifeste loin de sa cause, et c'est l'un des plus coûteux à diagnostiquer en Python comme en Java.
Le paradigme fonctionnel ne le corrige pas : il le rend impossible à écrire. C'est une différence de nature entre « faire attention » et « ne pas pouvoir se tromper ».

Comparer sur des critères

CritèreProcéduralObjetFonctionnelDéclaratif
État mutableglobal, partoutencapsulé dans l'objetabsentgéré par le moteur
Testabilitémoyenne (état global)bonne (objet isolable)excellente (fonctions pures)bonne (requête vérifiable)
Concurrencedifficile (verrous)difficile (verrous)naturelledéléguée au moteur
Lisibilité à petite échelleexcellentelourdedéroutante au débutexcellente
Passage à l'échellemauvaisbonbonexcellent dans son domaine
Coût mémoirefaiblemoyenplus élevé (copies)opaque
Débogagefacile (pas à pas)moyenfacile (pas d'effet caché)difficile (boîte noire)
Le fonctionnel n'est pas gratuit
Recréer une structure à chaque modification coûte de la mémoire et des copies. Les langages fonctionnels le compensent par des structures persistantes qui partagent la part inchangée, mais en Python, écrire naïvement du fonctionnel sur de gros volumes est réellement plus lent. Un argument « le fonctionnel est meilleur » sans mention de ce coût est un argument incomplet.

Choisir, et le justifier

La bonne question n'est jamais « quel est le meilleur paradigme », mais « lequel pour ce problème, sous ces contraintes ». La méthode tient en trois questions.

  1. Le programme a-t-il un état qui évolue ? Un script de transformation de fichier n'en a pas : fonctionnel ou déclaratif. Une simulation, un jeu, une session utilisateur en ont un : objet.
  2. Cet état est-il partagé entre plusieurs fils d'exécution ? Si oui, l'immuabilité cesse d'être une élégance pour devenir une garantie de correction.
  3. Le domaine a-t-il déjà un langage déclaratif ? Requêtes de données, validation de format, mise en page : ne réécris pas en impératif ce que SQL, une expression régulière ou un moteur de règles fait mieux.
La recommandation pour un système multi-utilisateurs
Un gestionnaire de stock consulté et modifié par plusieurs utilisateurs simultanément touche le point 2 de plein fouet : deux commandes concurrentes sur la même référence, et la quantité devient fausse sans qu'aucune exception ne soit levée.
La réponse en pratique est mixte, et c'est cette réponse-là qu'on attend : la persistance déclarative confiée à une base de données, qui gère les accès concurrents et les contraintes d'intégrité ; un cœur métier fonctionnel, fait de fonctions pures qui calculent le nouvel état sans le modifier, donc testable et parallélisable ; une façade objet mince pour l'interface, là où la notion de session a un sens.
Ce qu'il faut écarter explicitement, c'est le procédural à état global : il n'offre aucune barrière contre les accès concurrents.

Exercices type

Le même calcul de valeur totale, en procédural et en fonctionnel. Qu'est-ce qui change vraiment ?

En procédural : total = 0 puis une boucle qui accumule en modifiant total. On décrit comment accumuler.

En fonctionnel : reduce(lambda t, p: t + p[1] * p[2], stock, 0), ou plus simplement sum(q * pr for _, q, pr in stock). On décrit ce qu'est la valeur totale : la somme des produits quantité × prix.

Ce qui change réellement : dans la première version, il existe un moment où total contient une valeur intermédiaire fausse. Dans la seconde, non, l'expression n'a qu'une valeur, la bonne.

Sur trois lignes c'est anecdotique. Sur un calcul de cent lignes avec dix variables intermédiaires, c'est la différence entre un code qu'on peut raisonner et un code qu'on doit exécuter mentalement.

Pourquoi dit-on que les fonctions pures sont plus faciles à tester ?

Une fonction pure ne dépend que de ses arguments et ne modifie rien d'extérieur. Son test s'écrit donc en une ligne : assert valeur_totale(stock) == 787,5.

Une fonction impure suppose un état préalable : il faut le construire avant l'appel, puis vérifier après coup qu'il a été correctement modifié. Le test devient un scénario, sensible à l'ordre d'exécution des autres tests.

C'est aussi ce qui rend les fonctions pures mémoïsables, on peut mettre leur résultat en cache sans risque, puisque la même entrée donnera toujours la même sortie.

Java est-il un langage orienté objet ?

Historiquement oui, et exclusivement : tout devait vivre dans une classe.

Aujourd'hui, non, plus exclusivement. Depuis Java 8, il a des expressions lambda, des références de méthodes, des Stream et des Optional : de quoi écrire dans un style franchement fonctionnel.

La bonne formulation est donc : Java encourage l'objet par sa structure, mais permet le fonctionnel. C'est le cas de presque tous les langages courants, et c'est pourquoi « le langage X est objet » est une phrase à nuancer.

Dans la version objet, pourquoi `_produits` porte-t-il un souligné ?

C'est la convention Python pour signaler un attribut interne : « ceci n'appartient pas à l'interface publique, n'y touche pas depuis l'extérieur ».

Python ne l'empêche pas techniquement : stock._produits reste accessible. Contrairement à Java ou C++, l'encapsulation y est une convention entre développeurs, pas une contrainte du compilateur.

Ce qu'elle protège n'est pas la donnée mais la liberté de la changer : tant que personne n'y accède directement, remplacer le dictionnaire par une base de données ne casse aucun code appelant.

Un traitement doit s'exécuter sur 16 cœurs. Quel paradigme, et pourquoi ?

Fonctionnel, et la raison est précise : les données immuables se partagent entre cœurs sans verrou. Aucun cœur ne peut modifier ce qu'un autre est en train de lire, donc ni condition de course, ni interblocage, la classe de bugs disparaît par construction.

En procédural ou en objet avec état mutable partagé, il faudrait protéger chaque accès par un verrou : code plus complexe, plus lent, et affecté de bugs qui ne se reproduisent pas.

Nuance honnête : cela suppose que le traitement se décompose en parties indépendantes. Un calcul intrinsèquement séquentiel, où chaque étape a besoin du résultat de la précédente, ne se parallélise pas mieux en fonctionnel qu'ailleurs.

Pourquoi ne pas tout écrire en déclaratif, puisque c'est le plus concis ?

Parce que le déclaratif n'existe que dans un domaine où un moteur sait résoudre le problème. SQL sait interroger des tables ; une expression régulière sait reconnaître un motif. Hors de leur domaine, il n'y a pas de moteur.

Deux limites supplémentaires. Le débogage : quand une requête SQL est lente, on ne lit pas le code du moteur, on interroge un plan d'exécution, l'abstraction devient opaque au pire moment. Et l'expressivité : dès que la logique métier a des cas particuliers, la traduire en règles déclaratives produit quelque chose de moins lisible que dix lignes impératives.

Le bon usage est celui d'un outil spécialisé : déclaratif là où le domaine s'y prête, impératif ou fonctionnel pour le reste.

La méthode

  1. Nomme le paradigme avant de décrire une solution : « approche fonctionnelle » situe tout ce qui suit.
  2. Identifie où vit l'état. C'est la question qui distingue les quatre approches, et le point de départ de toute comparaison.
  3. Écris la version la plus simple d'abord. Le procédural sert de référence pour mesurer ce que les autres apportent.
  4. Compare sur des critères explicites (état, testabilité, concurrence, lisibilité) et pas sur une préférence.
  5. Cite le coût de l'approche que tu recommandes. Une recommandation sans inconvénient n'est pas une recommandation.
  6. Réponds à la contrainte de l'énoncé. « Multi-utilisateurs » veut dire concurrence, et la concurrence appelle l'immuabilité.

En résumé

  • Un paradigme est une façon de structurer un programme : ni un langage, ni un niveau, ni une religion.
  • Impératif / procédural : comment faire. Instructions et procédures, état global. Simple et direct, difficile à faire grandir.
  • Orienté objet : qui fait quoi. Données et opérations regroupées, état encapsulé, contraintes vérifiées en un seul point.
  • Fonctionnel : qu'est-ce que c'est. Valeurs immuables, fonctions pures, aucun effet de bord. Testable et parallélisable, plus coûteux en mémoire.
  • Déclaratif : quoi obtenir. On décrit le résultat, un moteur trouve le chemin. Excellent dans son domaine, opaque au débogage.
  • La vraie ligne de partage est l'état mutable partagé, d'où viennent la plupart des bugs de concurrence.
  • L'affectation ne copie pas : deux noms peuvent désigner le même objet.
  • Les langages sont multi-paradigmes ; le choix appartient au programmeur.
  • Une architecture réelle mélange : persistance déclarative, cœur fonctionnel, façade objet.

Et ensuite ? La version objet remplaçait une liste par un dictionnaire, et faisait passer la recherche de O(n)O(n) à O(1)O(1). Cette notation, et ce qu'elle change réellement sur de gros volumes, c'est tout le chapitre suivant : la complexité algorithmique.

Paradigmes de programmation | Plateforme ETS