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.
Un paradigme n'est pas un langage
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
| Paradigme | Question centrale | Unité de base | Langages typiques |
|---|---|---|---|
| Impératif / procédural | Comment faire ? | l'instruction, la procédure | C, Python, Pascal |
| Orienté objet | Qui fait quoi ? | la classe, l'objet | Java, C++, Python |
| Fonctionnel | Qu'est-ce que c'est ? | la fonction, la valeur | Haskell, OCaml, Lisp |
| Déclaratif | Quoi obtenir ? | la règle, la requête | SQL, Prolog, HTML |
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.
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.
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.
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.
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.
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.
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.
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.
[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.
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.
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ère | Procédural | Objet | Fonctionnel | Déclaratif |
|---|---|---|---|---|
| État mutable | global, partout | encapsulé dans l'objet | absent | géré par le moteur |
| Testabilité | moyenne (état global) | bonne (objet isolable) | excellente (fonctions pures) | bonne (requête vérifiable) |
| Concurrence | difficile (verrous) | difficile (verrous) | naturelle | déléguée au moteur |
| Lisibilité à petite échelle | excellente | lourde | déroutante au début | excellente |
| Passage à l'échelle | mauvais | bon | bon | excellent dans son domaine |
| Coût mémoire | faible | moyen | plus élevé (copies) | opaque |
| Débogage | facile (pas à pas) | moyen | facile (pas d'effet caché) | difficile (boîte noire) |
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.
- 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.
- 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.
- 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 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
- Nomme le paradigme avant de décrire une solution : « approche fonctionnelle » situe tout ce qui suit.
- Identifie où vit l'état. C'est la question qui distingue les quatre approches, et le point de départ de toute comparaison.
- Écris la version la plus simple d'abord. Le procédural sert de référence pour mesurer ce que les autres apportent.
- Compare sur des critères explicites (état, testabilité, concurrence, lisibilité) et pas sur une préférence.
- Cite le coût de l'approche que tu recommandes. Une recommandation sans inconvénient n'est pas une recommandation.
- 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 à . Cette notation, et ce qu'elle change réellement sur de gros volumes, c'est tout le chapitre suivant : la complexité algorithmique.