Appliquez les principes d'Héritage pour la Réutilisation de Code

Félicitations ! Après avoir découvert les fondations de la Programmation Orientée Objet (POO), et maîtrisé l’Encapsulation pour sécuriser vos objets, vous avez récemment appris à les assembler pour former des structures complexes et cohérentes grâce à la Composition et à l’Agrégation. Vous avez ainsi pu modéliser la hiérarchie du tournoi de ChessMaster Pro : un Tournoi possède des Rondes, qui possèdent des Matchs.

Maintenant, nous allons aborder l’autre pilier fondamental pour la construction de systèmes évolutifs : l’Héritage. Chez ChessMaster Pro, l'équipe a désormais besoin de gérer différents types de personnes associées à l'application (joueurs, arbitres, administrateurs). 

Ce chapitre vous montrera comment lier structurellement vos classes pour optimiser la réutilisation de votre travail.

Distinguez l'Héritage des autres relations

L'un des principaux avantages stratégiques de l'adoption de la POO pour un projet comme ChessMaster Pro est la réutilisabilité. Grâce à l'Héritage et à la généralisation, vous avez la possibilité de définir des structures une seule fois et de les employer dans de multiples contextes, évitant ainsi le risque d'erreurs dues à des copies de code mal synchronisées. L'Héritage est le mécanisme clé qui rend cela possible.

L'héritage est un concept essentiel de réutilisation de code qui permet d'organiser vos objets dans une hiérarchie allant du plus général au plus spécifique :

  • Vous créez une nouvelle classe, appelée classe fille (ou sous-classe), à partir d'une classe existante, appelée classe mère (ou super-classe). 

  • La classe fille hérite automatiquement de tous les attributs et méthodes de sa classe mère. 

  • Cette approche favorise ce que l'on appelle la spécialisation progressive : vous pouvez commencer par une classe générale, telle quePersonne, et en dériver des classes plus spécifiques, commeJoueurouArbitre, qui posséderont toutes les caractéristiques dePersonneplus leurs propres spécificités.

Pour bien identifier cette relation, il est crucial de la distinguer des concepts de Composition et d'Agrégation que vous avez étudiés précédemment. 

L'héritage est décrit par la phrase "est un" (is-a). 

Par exemple : Un Joueur est une Personne. Un Arbitre est une Personne. Inversement, la Composition et l'Agrégation, qui sont des formes d'Association, utilisent la relation "possède un" (has-a). Rappelez-vous que le Tournoi possède une Ronde.

  • Dans le cas de l'Héritage, vous réutilisez le plan et le comportement de la classe mère pour créer un sous-type. 

  • Dans le cas de la Composition, vous incluez un objet existant comme attribut. 

Par exemple, si vous avez besoin que tous les utilisateurs (Joueur, Administrateur) aient des coordonnées d'enregistrement, vous placez cette fonctionnalité de base dans la classe mère Utilisateur (ou Personne), et les sous-classes l'héritent simplement. 

L'héritage vous permet ainsi de modifier partiellement le comportement ou d'ajouter de nouvelles fonctionnalités à votre code existant, rapidement et sans réécriture.

L'Héritage est réservé à la création de sous-types qui se spécialisent, fournissant une hiérarchie claire et logique pour la réutilisation des fonctionnalités de base, ce qui est parfait pour gérer les différents rôles chez ChessMaster Pro.

Maîtrisez les sous-classes et l'appel de la superclasse

Pour exploiter pleinement la puissance de l'Héritage, vous devez maîtriser la manière dont les sous-classes sont créées et, plus important encore, la manière dont elles s'initialisent. 

Le défi survient lors de la création d'une nouvelle instance de la sous-classe. Rappelez-vous que le constructeur__init__est la méthode spéciale qui garantit que l'objet est initialisé et qu'il commence son cycle de vie dans un état valide.

  • Si la classeJoueura des attributs spécifiques (commeclassementElo), mais qu'elle hérite aussi des attributs dePersonne(commenometidentifiant), vous devez vous assurer que ces attributs hérités sont également initialisés correctement.

  • Pour cela, vous devez redéfinir la méthode__init__dans votre sous-classe, puis appeler explicitement la méthode__init__du parent viasuper().

  • Le rôle fondamental de cette séquence est de déléguer la responsabilité de l'initialisation des attributs de base à la classe qui sait comment les gérer : la superclasse.

Prenons l'exemple de ChessMaster Pro. Si votre classe de base Personne nécessite un nom et un numeroID dans son constructeur, lorsque vous écrivez le constructeur de la classe fille Joueur, vous devez d'abord appelersuper().__init__(nom, numeroID)avant d'initialiser les attributs spécifiques àJoueur, commeclassementElo.

Cela garantit que toutes les informations cruciales pour qu'un objet soit valide sont définies dès le départ. Si vous ne le faites pas, les attributs hérités pourraient rester non initialisés, ce qui entraînerait des erreurs plus tard dans le programme.

De plus, l'utilisation de l'Héritage est souvent combinée au Polymorphisme.

Par exemple, la classe mère Personne pourrait avoir une méthode génériquesePresenter(). La classe filleJoueurredéfinira cette méthode pour inclure son classement Elo. L'appel dejoueur.sePresenter()fonctionnera, quel que soit le type exact de la personne, démontrant la flexibilité du code.

Pour simplifier la maintenance future, une convention de bonne pratique consiste à utiliser *args et **kwargs dans le constructeur__init__de la sous-classe.

Ces arguments génériques permettent de transmettre facilement les paramètres non spécifiques à la classe parente, minimisant l'impact si les paramètres du parent venaient à changer dans le futur.

Maîtriser l'appel àsuper()est la clé pour concevoir des hiérarchies d'objets robustes et maintenables.

Gérez l'héritage multiple avec les Mix-ins

Alors que l'héritage simple (une classe fille, une classe mère) est la norme dans la POO, certains langages, comme Python (utilisé pour nos exemples conceptuels), permettent l'héritage multiple : une classe peut hériter de plusieurs autres classes.

L'héritage multiple est un outil puissant pour regrouper des fonctionnalités, mais il peut introduire une complexité significative, notamment le célèbre « problème du diamant »

Ce problème survient lorsque deux classes parentes différentes implémentent une méthode portant le même nom : 

  • Lorsque la classe enfant appelle cette méthode, le système rencontre une ambiguïté : quelle méthode doit-il exécuter ?

  • Bien que la fonctionsuper()puisse gérer ce scénario en suivant l'ordre de résolution de la méthode Method Resolution Order ou MRO du langage, les hiérarchies d'héritage multiples profondes deviennent rapidement difficiles à lire et à comprendre, rendant complexe l'identification de la sous-classe responsable d'un comportement spécifique.

Pour tirer parti de l'héritage multiple sans en subir la complexité, une approche fortement recommandée est l'utilisation de Mix-ins

La caractéristique fondamentale d'un Mix-in est qu'il n'est pas destiné à être instancié seul. Il s'agit uniquement d'un paquet de code réutilisable que vous "mélangez" avec votre classe de base principale.

Dans le contexte de ChessMaster Pro, imaginons que vous ayez besoin d'un nouveau rôle : un utilisateur qui est à la fois un Joueur et un Arbitre temporaire.

  1. Vous définissez une classe de basePersonne(votre pilier central).

  2. Vous définissez unJoueurMixinqui inclut les fonctionnalités liées au jeu (commecalculerElo()).

  3. Vous définissez unArbitreMixinqui inclut les fonctionnalités liées à l'arbitrage (commeverifierRegle()).

  4. Votre nouvelle classeArbitreJoueurhérite dePersonneet des deux Mix-ins.

Grâce à cette technique, vous atteignez deux objectifs :

  1. Vous maintenez une hiérarchie principale claire (l'objet est unePersonne).

  2. Vous composez de manière modulaire les fonctionnalités optionnelles spécifiques (JoueuretArbitre).

Les Mix-ins renforcent la modularité et vous permettent d'assembler des fonctionnalités de manière précise et flexible, offrant une solution élégante pour gérer les entités aux rôles multiples complexes sans alourdir l'héritage simple.

Utilisez des classes abstraites pour définir des Interfaces

L'Héritage ne sert pas uniquement à réutiliser des implémentations concrètes ; il est aussi fondamental pour la mise en œuvre de l'Abstraction, l'un des quatre piliers de la POO.

Ce contrat est souvent implémenté via des classes abstraites ou des interfaces

  • Ces structures définissent ce qu'une classe doit faire sans spécifier comment elle le fait.

  • Leur objectif est de servir de modèle en spécifiant un ensemble de propriétés qu'un objet doit impérativement posséder pour être considéré comme valide dans le système.

L'abstraction, lorsqu'elle est combinée à l'héritage, permet de gérer la complexité en masquant les informations non pertinentes pour l'utilisateur de la classe :

  • L'équipe de ChessMaster Pro pourrait définir une interfaceIJouable(via une classe abstraite) qui force toute classe l'implémentant (commeJoueurou uneIA) à posséder une méthodejouerCoup(), sans se soucier des détails de l'implémentation derrière cette méthode. 

  • Si le langage utilisé (comme Python) ne permet pas d'empêcher directement l'instanciation, l'effet de classe abstraite peut être créé en définissant des méthodes qui lèvent une erreur (NotImplementedError), forçant ainsi les développeurs à les redéfinir dans les sous-classes.

Au lieu d'utiliser des erreurs génériques, vous créez une classe d'exception de base (par exemple,ErreurTournoi) et en dérivez des sous-classes spécialisées (ErreurInscription,  ErreurMatchmaking).

L'avantage est que vous pouvez ensuite capturer toutes ces erreurs connexes dans un seul bloc except, simplifiant grandement la gestion des erreurs et rendant le code plus lisible et robuste. 

En utilisant l'héritage pour définir des interfaces et des contrats, vous construisez non seulement une application qui fonctionne, mais une application qui peut grandir et s'adapter facilement aux changements futurs.

Adoptez la Composition plutôt que l'Héritage (Délégation)

L'Héritage, bien que fondamental pour la réutilisabilité et la spécialisation, n'est pas toujours la meilleure solution pour toutes les formes de réutilisation de code.

La principale raison d'être de ce principe est d'éviter les hiérarchies d'héritage profondes, c'est-à-dire celles qui comportent de nombreuses couches successives de classes mères et filles.

Ces hiérarchies peuvent devenir extrêmement difficiles à lire, à comprendre et à maintenir, car il devient complexe de déterminer quelle classe, dans la chaîne, est responsable de quel comportement. 

La Composition offre une alternative robuste pour atteindre un résultat similaire sans les complications liées au couplage fort de l'héritage. 

Revenons à l'application ChessMaster Pro. Si vous souhaitez que certains joueurs puissent enregistrer des logs d'activité, au lieu de faire hériter la classe Joueur d'une classeLogger, vous feriez de la classeLoggerun attribut duJoueur. Ainsi, quand Joueur a besoin d'enregistrer une activité, il appelle simplementself.logger.enregistrer(...).

Cette délégation de responsabilité présente plusieurs avantages :

  1. Flexibilité : Vous pouvez changer le type de logger à tout moment (passer d'un logger de fichier à un logger de base de données) sans modifier la structure d'héritage deJoueur.

  2. Couplage faible :Joueurdépend de l'interface duLogger, mais pas de son implémentation interne.

  3. Clarté : La responsabilité de chaque classe est claire. LeJoueurest responsable de son état (classement Elo), et leLoggerest responsable de l'enregistrement.

Savoir quand utiliser le lien rigide is-a (Héritage) pour la spécialisation fondamentale et quand privilégier le lien flexible has-a (Composition/Délégation) pour l'assemblage de fonctionnalités est la marque d'une conception POO mature. Dans le développement de logiciels modernes et scalables comme ChessMaster Pro, la Composition est souvent le choix par défaut pour la réutilisation de code, ne recourant à l'Héritage que pour établir de véritables hiérarchies de types.

À vous de jouer !

Contexte

La start-up ChessMaster Pro a besoin d'intégrer des rôles d'utilisateur plus complexes et évolutifs. Certains utilisateurs peuvent jouer, d'autres peuvent arbitrer, et nous devons prévoir la possibilité qu'un utilisateur cumule ces deux rôles (par exemple, un joueur qui arbitre un match d'un tournoi secondaire). L'Héritage direct (un seul parent) pourrait rendre la gestion des rôles multiples compliquée. Nous allons utiliser des Mix-ins pour assembler les fonctionnalités de manière modulaire.

Consignes

  1. Définissez une classe de base Personne (incluant le nom et le numéro d'identification number).

  2. Créez deux classes de comportement sous forme de mix-ins : JoueurMixin (ajoutant la méthodejouer()) et ArbitreMixin (ajoutant la méthodearbitrer()).

  3. Créez une classe ArbitreJoueur qui hérite dePersonneet des deux mix-ins.

  4. Implémentez les constructeurs (__init__) en vous assurant que l'initialisation de la classe principalePersonneest gérée viasuper().

En résumé

  • L'Héritage est le pilier de la POO qui favorise la réutilisation du code en permettant à une classe fille de spécialiser les fonctionnalités de sa classe mère, établissant une relation de type "est un" (is-a).

  • Pour garantir la validité de l'objet, le constructeur de la sous-classe doit obligatoirement appeler le constructeur de la superclasse via la fonctionsuper()afin d'initialiser les attributs hérités.

  • L'Héritage multiple peut être géré efficacement grâce aux Mix-ins, qui sont des classes non instanciables conçues pour injecter des ensembles de fonctionnalités modulaires et optionnelles dans une classe de base.

  • L'Héritage est utilisé pour l'Abstraction en définissant des classes abstraites ou des interfaces qui agissent comme des "contrats" que les sous-classes concrètes doivent respecter, masquant ainsi les détails d'implémentation.

  • Le principe "Composition plutôt que l'Héritage" est souvent préféré pour les fonctionnalités complexes ou profondes, car il permet une réutilisation des comportements plus flexible (délégation) sans les complications et le couplage des hiérarchies d'héritage profondes.

Bravo pour votre progression tout au long de ce cours ! Vous maîtrisez désormais les bases essentielles de la programmation orientée objet : continuez à pratiquer pour renforcer vos compétences, et n’oubliez pas de tester vos acquis en réalisant le quiz de fin de cours.

Et si vous obteniez un diplôme OpenClassrooms ?
  • Formations jusqu’à 100 % financées
  • Date de début flexible
  • Projets professionnalisants
  • Mentorat individuel
Trouvez la formation et le financement faits pour vous