Les premiers pas
La programmation orientée objet (POO) constitue l'un des paradigmes de programmation les plus importants et les plus largement utilisés dans le développement logiciel moderne. Elle est adoptée par de nombreux langages de programmation majeurs, tels que Java, C++ et bien d'autres, pour structurer et organiser le code de manière claire, modulaire et réutilisable.
Dans le présent article, nous allons présenter et expliquer les notions fondamentales qui composent ce modèle. Plus précisément, nous allons détailler trois concepts clefs : les classes et les instances, servant à définir des modèles et à créer des objets concrets ; l'héritage, qui permet de créer de nouvelles classes à partir de classes existantes ; et l'encapsulation, consistant à protéger et organiser les données ainsi que les comportements associés.
À ce stade, notre présentation restera volontairement générale : nous ne ferons aucune référence directe ou spécifique au langage JavaScript. Tous les exemples illustrant les explications seront donc exprimés en pseudo-code, afin que les principes soient compréhensibles quelle que soit la technologie utilisée.
Remarque importante : les mécanismes que nous allons décrire appartiennent à une forme particulière de programmation orientée objet, souvent qualifiée de POO basée sur les classes ou encore de POO classique. C'est ce style, historiquement très répandu, qui est le plus souvent désigné lorsqu'on parle simplement de programmation orientée objet, sans préciser de variante.
Par la suite, lorsque nous aborderons le cas spécifique de JavaScript, nous mettrons en lumière le rôle et le fonctionnement des constructeurs ainsi que celui de la chaîne de prototypes, en les comparant aux concepts traditionnels de la POO. Nous analyserons également les principales différences entre la mise en ouvre orientée objet de JavaScript et celle des langages dits «classiques».
| Catégorie | Description |
|---|---|
| Prérequis | Connaissance des bases de JavaScript (notamment des bases Objet) et des concepts JavaScript orientés objet. |
| Objectifs d'apprentissage | Concepts de programmation orientée objet (POO) : classes, instances, héritage et encapsulation. Comment ces concepts de POO s'appliquent à JavaScript et quelles sont les différences avec un langage comme Java ou C++. |
La programmation orientée objet est un paradigme reposant sur l'idée de représenter un système informatique sous la forme d'un ensemble structuré d'objets, chacun de ces objets étant conçu pour incarner ou modéliser un élément précis, une fonction ou un aspect particulier de ce système global. Chaque objet regroupe en son sein à la fois des données - que l'on appelle aussi attributs ou propriétés - et des fonctions - également nommées méthodes - permettant d'agir sur ces données ou de les exploiter pour réaliser certaines opérations.
Un objet expose, vers l'extérieur, une interface publique définissant les actions que les autres composantes ou modules du programme peuvent exécuter sur lui. Cette interface constitue le seul point d'accès prévu pour interagir avec l'objet, garantissant ainsi une utilisation contrôlée et cohérente de ses fonctionnalités. Parallèlement, l'objet conserve et gère un état interne privé, inaccessible directement depuis le reste du code. Ce mécanisme permet de protéger l'intégrité des données internes et d'éviter qu'elles soient modifiées de manière imprévue ou non contrôlée.
Grâce à cette séparation claire entre interface publique et état interne, les autres parties du système n'ont pas besoin de connaître les détails techniques ou la logique interne de l'objet pour pouvoir l'utiliser efficacement. Elles peuvent se concentrer sur ce que l'objet fait, plutôt que sur la manière dont il le fait, ce qui favorise la modularité, la réutilisabilité et la maintenance du code.
Classes et instances
Lorsque nous abordons la modélisation d'un problème en programmation orientée objet (POO), nous commençons par définir des représentations abstraites appelées classes. Ces classes servent de modèles ou de plans pour décrire les types d'objets que nous souhaitons intégrer et manipuler au sein de notre système logiciel. Une classe ne correspond pas encore à un objet concret ; elle définit plutôt la structure générale et le comportement attendu pour un ensemble d'objets similaires.
Prenons un exemple concret : si notre objectif est de représenter informatiquement une école, nous pourrions avoir besoin d'objets qui incarnent les professeurs. Dans un tel contexte, chaque professeur possède un ensemble de propriétés communes : par exemple, tous disposent d'un nom et sont associés à une matière spécifique qu'ils enseignent. Par ailleurs, tous les professeurs sont capables de réaliser certaines actions récurrentes : ils peuvent corriger des devoirs, préparer des leçons, ou encore se présenter à leurs étudiants au début de l'année scolaire.
Dans ce modèle, Professeur pourrait donc constituer une classe à part entière au sein de notre système. Cette définition de classe décrirait précisément, d'une part, les données (ou attributs) que chaque professeur détient, comme le nom ou la matière enseignée, et, d'autre part, les méthodes (ou fonctions membres) que tout professeur est capable d'exécuter, comme corrigerDevoir() ou sePresenter().
Une fois cette classe définie, il devient possible de créer des instances de cette classe, c'est-à-dire des objets concrets représentant chaque professeur réel de l'école. Chaque instance dispose de ses propres valeurs pour les propriétés définies dans la classe (par exemple : Nom = "Mme Dupont", Matière = "Mathématiques"), tout en partageant la même structure et les mêmes comportements que les autres instances issues de la même classe.
En pseudo-code, la définition d'une classe Professeur pourrait être exprimée ainsi :
|
class Professeur properties nom enseignent methods corrigerDevoir(papier) sePresenter() |
Ceci définit une classe Professeur avec :
- deux propriétés de données : nom et enseignent
- deux méthodes : corrigerDevoir() pour noter un devoir et sePresenter() pour se présenter.
En elle-même, une classe ne réalise aucune action concrète et ne produit aucun résultat observable lors de l'exécution du programme. On peut la considérer comme un modèle, un plan ou encore un gabarit servant à définir la structure et le comportement d'objets qui seront créés ultérieurement. La classe décrit les propriétés et les méthodes que possédera tout objet issu de ce modèle, mais tant qu'aucune instance n'a été fabriquée, rien de tangible n'existe dans la mémoire du programme.
Lorsque nous créons un professeur concret à partir de la classe Professeur, ce nouvel objet prend le nom d'instance de la classe Professeur. Autrement dit, une instance est une manifestation concrète et individuelle d'une classe, dotée de ses propres données internes tout en respectant la structure et les comportements définis dans le modèle. Chaque instance est indépendante des autres, même si elles partagent toutes les mêmes méthodes et attributs définis dans la classe d'origine.
La création d'une instance passe par l'utilisation d'une fonction spéciale appelée constructeur. Ce constructeur est chargé de préparer l'objet au moment de sa naissance en lui attribuant des valeurs initiales pour son état interne (les propriétés qui devront être connues ou fixées dès la création). Lors de l'appel au constructeur, nous pouvons lui transmettre des arguments correspondant à ces valeurs initiales, ce qui permet de personnaliser chaque objet dès son instanciation.
Dans la plupart des langages orientés objet, ce constructeur est intégré directement à la définition de la classe elle-même. Par convention ou par exigence du langage, il porte généralement le même nom que la classe à laquelle il appartient, ce qui permet d'identifier immédiatement sa fonction et d'établir clairement le lien entre la structure définie et les objets créés à partir de celle-ci :
|
class Professeur properties nom enseignent constructor Professeur(nom, enseignent) methods corrigerDevoir(papier) sePresenter() |
Ce constructeur prend deux paramètres, ce qui nous permet d'initialiser les propriétés nom et enseignent lors de la création d'un nouveau professeur concret.
Maintenant que nous disposons d'un constructeur, nous pouvons créer des professeurs. Les langages de programmation utilisent souvent le mot clef new pour signaler l'appel d'un constructeur comme dans le code JavaScript suivant :
- tremblay = new Professeur("Tremblay", "Psychologie");
- angelique = new Professeur("Angélique", "Poésie");
-
- tremblay.enseignent; // 'Psychologie'
- tremblay.sePresenter(); // 'Je m'appelle professeur Tremblay et je serai votre professeur de psychologie.'
-
- angelique.enseignent; // 'Poésie'
- angelique.sePresenter(); // 'Je m'appelle professeur Angélique et je serai votre professeur de poésie.'
Cela crée deux objets, tous deux des instances de la classe Professeur.
Héritage
Imaginons maintenant que, dans notre modèle d'établissement scolaire, nous souhaitions aussi représenter un autre type d'acteur important : les étudiants. Contrairement aux professeurs, les étudiants n'exercent pas les mêmes rôles ni ne possèdent les mêmes capacités au sein du système. Par exemple, un étudiant ne peut pas corriger les copies ou évaluer des devoirs, puisqu'il n'a pas de fonction d'enseignement. De la même manière, il n'est pas associé à une matière précise qu'il dispenserait à d'autres, mais il est, au contraire, rattaché à une classe spécifique dans laquelle il suit un programme d'apprentissage.
Cependant, tout comme les professeurs, chaque étudiant possède certaines caractéristiques communes devant également être prises en compte dans notre modélisation. Ainsi, chaque étudiant a un nom, qui permet de l'identifier, et peut également effectuer certaines actions simples, comme se présenter aux autres membres de la classe ou aux enseignants, par exemple en début d'année ou lors d'une activité d'intégration.
Pour représenter ces éléments dans notre système, nous pouvons définir une classe Etudiant. Cette classe comportera des propriétés qui stockent les informations essentielles à propos de chaque étudiant, comme son nom et son année d'études (ou niveau scolaire). Elle comprendra également un constructeur, c'est-à-dire une fonction spéciale appelée au moment de la création d'une nouvelle instance, qui prendra en paramètres le nom et l'année de l'étudiant afin d'initialiser son état interne.
Enfin, cette classe définira aussi une méthode appelée sePresenter(), permettant à un étudiant de se présenter lorsqu'elle sera exécutée. Ainsi, la structure de cette classe en pseudo-code pourrait se décrire de la manière suivante :
|
class Etudiant properties nom annee constructor Etudiant(nom, annee) methods sePresenter() |
Il serait utile de pouvoir représenter le fait que les étudiants et les professeurs partagent certaines propriétés, ou plus précisément, le fait qu'à un certain niveau, ils sont identiques. L'héritage nous le permet.
Commençons par observer que les étudiants et les professeurs sont tous deux des personnes, et que ces personnes ont un nom et souhaitent se présenter. Nous pouvons modéliser cela en définissant une nouvelle classe « Personne », dans laquelle nous définissons toutes les propriétés communes des personnes. Ensuite, Professeur et Étudiant peuvent dériver de «Personne», en ajoutant leurs propriétés supplémentaires :
|
class Personne properties nom constructor Personne(nom) methods sePresenter() class Professeur : extends Personne properties enseignent constructor Professeur(nom, enseignent) methods corrigerDevoir(papier) sePresenter() class Etudiant : extends Personne properties annee constructor Etudiant(nom, annee) methods sePresenter() |
Dans ce cas, nous dirions que Personne est la superclasse, ou classe parente, de Professeur et Etudiant. Inversement, Professeur et Etudiant sont des sous-classes, ou classes enfants, de Personne.
Vous remarquerez peut-être que la fonction sePresenter() est définie dans les trois classes. En effet, si chacun souhaite se présenter, sa façon de le faire est différente en JavaScript :
- tremblay = new Professor("Tremblay", "Psychologie");
- tremblay.sePresenter(); // 'Je m'appelle professeur Tremblay et je serai votre professeur de psychologie.'
-
- sebastien = new Student("Sébastien", 1);
- sebastien.sePresenter(); // 'Je m'appelle Sébastien et je suis en première année.'
Nous pourrions avoir une implémentation par défaut de sePresenter() pour les personnes n'étant ni étudiants ni professeurs :
- pierre = new Personne("Pierre");
- pierre.sePresenter(); // 'Je m'appelle Pierre.'
Cette caractéristique - lorsqu'une méthode porte le même nom mais a une implémentation différente dans différentes classes - est appelée polymorphisme. Lorsqu'une méthode d'une sous-classe remplace l'implémentation de la superclasse, on dit que la sous-classe remplace la version de la superclasse.
Encapsulation
Les objets fournissent une interface au code souhaitant les utiliser, mais conservent leur propre état interne. Cet état interne est privé, ce qui signifie qu'il n'est accessible que par ses propres méthodes, et non par d'autres objets. Garder l'état interne d'un objet privé, et généralement établir une distinction claire entre son interface publique et son état interne privé, est appelé encapsulation.
Cette fonctionnalité est utile car elle permet au programmeur de modifier l'implémentation interne d'un objet sans avoir à rechercher et mettre à jour tout le code qui l'utilise : elle crée une sorte de pare-feu entre cet objet et le reste du système.
Par exemple, supposons que des étudiants soient autorisés à étudier le tir à l'arc s'ils sont en deuxième année ou plus. Nous pourrions implémenter cela simplement en exposant la propriété «annee» de l'étudiant, et un autre code pourrait l'examiner pour déterminer si l'étudiant peut suivre le cours en JavaScript :
- if (etudiant.annee > 1) {
- // permettre à l'élève d'entrer dans la classe
- }
Le problème est que si nous décidons de modifier les critères d'autorisation d'apprentissage du tir à l'arc (par exemple en exigeant également l'autorisation des parents ou du tuteur), nous devrons mettre à jour chaque emplacement de notre système où ce test est effectué. Il serait préférable d'avoir une méthode peutEtudierLeTirALArc() sur les objets Étudiant, implémentant la logique en un seul endroit :
Et le code JavaScript suivant :
- if (etudiant.peutEtudierLeTirALArc()) {
- // permettre à l'élève d'entrer dans la classe
- }
Ainsi, si nous souhaitons modifier les règles d'étude du tir à l'arc, il nous suffit de mettre à jour la classe Etudiant, et tout le code l'utilisant continuera de fonctionner.
Dans de nombreux langages de programmation orientée objet, nous pouvons empêcher tout autre code d'accéder à l'état interne d'un objet en marquant certaines propriétés comme privées. Cela générera une erreur si du code extérieur à l'objet tente d'y accéder avec le code JavaScript suivant :
Dans les langages n'imposant pas ce type d'accès, les programmeurs utilisent des conventions de nommage, comme commencer le nom par un trait de soulignement, pour indiquer que la propriété doit être considérée comme privée.
POO et JavaScript
Dans cette page, nous avons décrit certaines fonctionnalités de base de la programmation orientée objet basée sur les classes, implémentée dans des langages comme Java et C++.
- Les constructors en JavaScript nous fournissent une sorte de définition de classe, nous permettant de définir la « forme » d'un objet, y compris les méthodes qu'il contient, en un seul endroit. Mais les prototypes peuvent également être utilisés ici. Par exemple, si une méthode est définie sur la propriété prototype d'un constructeur, tous les objets créés avec ce constructeur obtiennent cette méthode via leur prototype, et nous n'avons pas besoin de la définir dans le constructeur.
- La chaîne de prototypes semble être une façon naturelle d'implémenter l'héritage. Par exemple, si nous avons un objet Étudiant dont le prototype est Personne, il peut hériter de nom et surcharger sePresenter().
Il est important de comprendre les différences entre ces fonctionnalités et les concepts classiques de la POO décrits précédemment. Nous en soulignerons quelques-unes ici.
Tout d'abord, en POO basée sur les classes, les classes et les objets sont deux constructions distinctes, et les objets sont toujours créés comme des instances de classes. De plus, il existe une distinction entre la fonctionnalité utilisée pour définir une classe (la syntaxe de classe elle-même) et celle utilisée pour instancier un objet (un constructeur). En JavaScript, nous pouvons créer des objets sans définition de classe distincte, et c'est souvent le cas, à l'aide d'une fonction ou d'un littéral d'objet. Cela peut alléger considérablement le travail avec les objets par rapport à la POO classique.
Deuxièmement, bien qu'une chaîne de prototypes ressemble à une hiérarchie d'héritage et se comporte comme elle à certains égards, elle diffère à d'autres égards. Lorsqu'une sous-classe est instanciée, un objet unique est créé, combinant les propriétés définies dans la sous-classe avec celles définies plus haut dans la hiérarchie. Avec le prototypage, chaque niveau de la hiérarchie est représenté par un objet distinct, et ils sont liés entre eux via la propriété __proto__. Le comportement de la chaîne de prototypes s'apparente moins à de l'héritage qu'à de la délégation. La délégation est un modèle de programmation où un objet, lorsqu'on lui demande d'exécuter une tâche, peut l'exécuter lui-même ou demander à un autre objet (son délégué) de l'exécuter à sa place. À bien des égards, la délégation est une manière plus flexible de combiner des objets que l'héritage (par exemple, il est possible de modifier ou de remplacer complètement le délégué à l'exécution).
Cela dit, les constructeurs et les prototypes peuvent être utilisés pour implémenter des modèles de POO basés sur les classes en JavaScript. Cependant, leur utilisation directe pour implémenter des fonctionnalités comme l'héritage est délicate. JavaScript fournit donc des fonctionnalités supplémentaires, superposées au modèle de prototype, qui correspondent plus directement aux concepts de la POO basée sur les classes.