Aller au contenu principal

Paradigmes & patterns · L2 · Section 1/4

Impératif et objet

Progression

Points d’expérience : XPSérie de jours consécutifs : · —Progression du module : — / —compris

#Impératif et objet

Le style impératif enchaîne des ordres qui transforment un état : affectations, boucles, conditions. Le style objet regroupe cet état et les opérations qui le respectent dans une entité qui garantit ses invariants. Ce chapitre montre les deux sur le même exemple, puis les transformations qui rendent un code objet plus testable : encapsulation stricte, injection de dépendances, composition à la place de l'héritage.

Prérequis : Python de base (classes, exceptions), notions de test unitaire. Le module prog-python couvre le langage ; ici on raisonne sur la conception.

Objectifs d'apprentissage :

  • Distinguer encapsulation, héritage, composition, et préférer la composition pour limiter le couplage.
  • Écrire des invariants de classe explicites et les faire respecter par toutes les méthodes.
  • Appliquer SRP et OCP à petite échelle (fonctions et classes focalisées, extensions sans modification risquée).
  • Expliquer l'impact des états partagés et des effets de bord sur la testabilité.
  • Transformer un code procédural en objets, et inversement, en conservant le comportement observable.

#Le même calcul en impératif puis en objet

Un même traitement, total d'un panier avec remise, écrit deux fois. Version impérative : l'état vit dans des variables locales.

Chargement de l’éditeur...

Version objet : l'état vit dans un objet dont les méthodes maintiennent un invariant (sous-total recalculé, remise décidée par une règle unique).

Chargement de l’éditeur...

Les deux versions affichent exactement la même chose (sous-total 295.00, remise 10%, total 265.50). La différence n'est pas dans le résultat mais dans les garanties : la version impérative laisse chacun recalculer et réinterpréter la règle de remise ; la version objet la concentre dans taux_remise, interdit les lignes invalides à l'entrée, et offre une surface testable (Panier se construit, s'alimente, s'interroge). C'est le critère de choix : dès qu'une règle métier risque d'être dupliquée ou contournée, l'objet rapporte plus que son coût en cérémonie.

#Exemple OO simple avec invariant

Chargement de l’éditeur...

L'invariant est : solde ≥ 0 à tout instant observable. Chaque méthode qui touche au solde le rétablit ou refuse : le constructeur rejette un solde négatif, deposer rejette les montants nuls ou négatifs (sinon deposer(-30) contournerait retirer), retirer vérifie les fonds. C'est la définition opérationnelle de l'encapsulation : non pas « un underscore devant l'attribut », mais « aucune séquence d'appels publics ne peut produire un état invalide ».

#Transformation concrète : de l'héritage à la composition

Le cas d'école de la fragilité de l'héritage : une hiérarchie de comptes rémunérés. Version héritage, où la sous-classe casse la substitution :

Chargement de l’éditeur...

Le code client qui itère sur des Compte et appelle retirer plante sur CompteBloque : la sous-classe viole le contrat de la classe de base (toute opération valide sur la base doit rester valide sur la sous-classe, c'est le principe de substitution de Liskov). Version composition, où la politique de retrait est une pièce injectée :

Chargement de l’éditeur...

Ce qui a changé, ligne à ligne : CompteBloque disparaît (plus aucune sous-classe), la classe Compte gagne un paramètre politique avec un défaut qui préserve l'ancien comportement, et le test de la politique devient un simple appel verifier(100, 10) sans construire de compte. Le client itère sans connaître les variantes : ajouter « retrait plafonné à 500 » se fait en écrivant une nouvelle politique de trois lignes, sans toucher à Compte ni aux autres politiques. C'est OCP en pratique : ouvert à l'extension, fermé à la modification.

#Pièges fréquents

  • Constructeurs lourds avec I/O (ouvrir un fichier, interroger un service) : le test unitaire devient un test d'intégration malgré lui. Préférer l'injection de dépendances : le constructeur reçoit un port (objet avec la méthode attendue), l'adaptateur concret est fourni au point d'assemblage.
  • États globaux cachés (singletons mal maîtrisés) : deux tests qui semblent indépendants partagent en réalité l'état du singleton, et leur ordre d'exécution change le verdict.
  • Méthodes trop longues : extraire des fonctions privées, chacune avec un invariant documenté en une phrase.
  • Getters/setters systématiques : exposer tous les attributs « proprement » n'est pas de l'encapsulation ; ne publier que les opérations qui ont un sens métier.

#Mini-quiz

Laquelle favorise le faible couplage ?
Laquelle favorise le faible couplage ?
Un invariant de classe, c'est :
Un invariant de classe, c'est :
CompteBloque.retirer qui lève toujours une exception viole :
CompteBloque.retirer qui lève toujours une exception viole :

#Exercice : Hiérarchie de formes géométriques

Implémentez une hiérarchie de classes pour des formes géométriques en utilisant l'héritage et le polymorphisme.

#Instructions

  1. Créez une classe de base abstraite Forme avec une méthode abstraite aire().
  2. Créez des classes dérivées Rectangle, Cercle, et Triangle qui implémentent la méthode aire().
  3. Ajoutez une méthode description() dans la classe de base qui retourne une description de la forme.
  4. Utilisez le polymorphisme pour stocker différentes formes dans une liste et calculer leurs aires.

#Exemple de code

pythonpython

1from abc import ABC, abstractmethod2import math3 4class Forme(ABC):5    @abstractmethod6    def aire(self):7        pass8 9    def description(self):10        return f"Cette forme a une aire de {self.aire():.2f} unités carrées."11 12class Rectangle(Forme):13    def __init__(self, largeur, hauteur):14        self.largeur = largeur

Pourquoi cette hiérarchie est légitime, contrairement à CompteBloque : chaque sous-classe renforce le contrat plutôt que de le restreindre. Forme promet « toute forme sait répondre à aire() », et chaque dérivée le fait sans jamais lever d'exception là où la base réussissait. La relation est un is-a stable (un Rectangle est une Forme, pour toujours), et le client polymorphe (for forme in formes) ne dépend d'aucune sous-classe. C'est le cas d'usage où l'héritage reste le bon outil.

Chargement de l’éditeur...

Sorties attendues : 12.00 pour le Rectangle (3×4), 78.54 pour le Cercle (π×25), 24.00 pour le Triangle (0,5×6×8). La méthode description de la base appelle self.aire() sans savoir laquelle s'exécutera : c'est le polymorphisme, la base programme vers une abstraction que chaque dérivée concrétise.

#Exercice avec correction : injection de dépendances

Le FileLogger ci-dessous ouvre un fichier à chaque message, ce qui rend le code impossible à tester sans toucher au disque. Transformez-le pour que la destination devienne un collaborateur injecté.

pythonpython

1class Service:2    def __init__(self):3        self.logger = FileLogger()   # dépendance créée à l'intérieur4 5    def traiter(self, donnees):6        self.logger.log(f"traitement de {donnees}")7        return len(donnees)

#Correction

pythonpython

1class Service:2    def __init__(self, logger):      # dépendance reçue de l'extérieur3        self.logger = logger4 5    def traiter(self, donnees):6        self.logger.log(f"traitement de {donnees}")7        return len(donnees)8 9class LoggerMemoire:                 # double de test: trois lignes10    def __init__(self):11        self.messages = []12    def log(self, msg):13        self.messages.append(msg)

Trois changements : le constructeur ne crée plus rien (il reçoit), le test peut injecter LoggerMemoire et inspecter messages sans fichier ni mock lourd, et Service ne connaît que le contrat « un objet avec log(str) », pas la classe FileLogger. Le point d'assemblage (le main) choisit le vrai logger ; le cœur du programme reste testable en mémoire. C'est l'inversion de dépendances : les modules de haut niveau ne dépendent plus des détails de bas niveau, les deux dépendent d'une abstraction.

Méthode de vérification : écrire le test de Service.traiter avec LoggerMemoire, vérifier qu'il s'exécute sans accès disque et sans réseau, et que le message journalisé contient bien la donnée traitée. Si le test exige un environnement, l'injection n'est pas terminée.