Les objets dans la programmation orientés objet
Avant de plonger dans l'étude détaillée du fonctionnement et des mécanismes de la programmation orientée objet (POO), il est utile de poser d'abord quelques bases conceptuelles en réfléchissant à ce que l'on appelle un «objet» dans ce contexte. Pour bien comprendre cette notion, il peut être intéressant d'adopter une perspective plus intuitive, en nous éloignant momentanément du code informatique et de la technique pure, pour raisonner à partir d'exemples issus de la vie quotidienne.
Imaginez donc que vous ne soyez pas en train d'écrire un programme informatique au sens strict, mais que vous cherchiez à structurer et planifier votre propre journée sous la forme d'un programme personnel, une sorte de suite ordonnée d'instructions à suivre. Ce « programme » pourrait se rapprocher d'une liste d'actions précises à effectuer, rédigée étape par étape, et dont l'exécution vous guiderait tout au long de la journée.
Ce scénario pourrait débuter de façon simple, par exemple avec quelques tâches initiales faciles à identifier, formant la première partie de votre emploi du temps. On pourrait imaginer que votre liste commence ainsi :
- Se réveiller.
- Boire un café (ou un thé).
- Prendre votre petit-déjeuner : céréales, bleuets et lait de soja.
- Prendre le métro.
De quoi s'agit-il ? Tout d'abord, même si cela n'apparaît pas immédiatement au vu de la manière dont nous avons rédigé les instructions ci-dessus, l'élément principal, c'est vous : un être humain, une personne. Vous présentez certaines propriétés. Vous avez une certaine apparence ; vous avez peut-être les cheveux bruns, portez des lunettes et avez l'air un peu ringard. Vous avez également la capacité de faire des choses, comme vous réveiller (et sans doute aussi dormir), manger ou prendre le métro. Un objet est comme vous, une chose qui a des propriétés et peut faire des choses.
Désignons les données et les fonctions d'un objet humain très simple :
Données humaines :
- Taille
- Poids
- Sexe
- Couleur des yeux
- Couleur des cheveux
Fonctions humaines :
- Dormir.
- Se réveiller.
- Manger.
- Utiliser un moyen de transport.
Avant d'aller plus loin, nous devons nous lancer dans une brève digression métaphysique. La structure ci-dessus ne représente pas un être humain en soi; elle décrit simplement l'idée, ou le concept, qui sous-tend un être humain. Elle décrit ce qu'est être humain. Être humain, c'est avoir de la taille, des cheveux, dormir, manger,... C'est une distinction cruciale pour la programmation d'objets. Ce modèle d'être humain est appelé une classe. Une classe est différente d'un objet. Vous êtes un objet. Je suis un objet. Ce type dans le métro est un objet. Nikola Tesla est un objet. Nous sommes tous des êtres humains, des exemples concrets de l'idée d'être humain.
Pensez à un emporte-pièce. Un emporte-pièce fait des biscuits, mais ce n'est pas un biscuit en soi. L'emporte-pièce est la classe, les biscuits sont les objets.
Utilisation d'un objet
Avant d'aborder l'écriture d'une classe, voyons brièvement comment l'utilisation d'objets dans notre programme principal (initialise() et dessin()) améliore le monde.
Prenons le pseudo-code d'un croquis simple qui déplace un rectangle horizontalement sur la fenêtre (nous considérerons ce rectangle comme une «voiture»).
Données (variables globales)
- Couleur de la voiture.
- Emplacement x de la voiture.
- Emplacement y de la voiture.
- Vitesse x de la voiture.
Configuration :
- Initialisation de la couleur de la voiture.
- Initialisation de l'emplacement de la voiture au point de départ.
- Initialisation de la vitesse de la voiture.
Dessin :
- Remplissage de l'arrière-plan.
- Affichage de la voiture à l'emplacement avec la couleur.
- Incrémentation de l'emplacement de la voiture par la vitesse.
Pour implémenter le pseudo-code ci-dessus, nous définirions des variables globales en début de programme, les initialiserions dans initialise() et appellerions des fonctions pour déplacer et afficher la voiture dans dessin(). Par exemple :
La programmation orientée objet permet d'entreposer toutes les variables et fonctions du programme principal dans un objet «voiture». Un objet «voiture» connaît ses données : couleur, emplacement, vitesse. Il connaît également ses fonctions, ses méthodes (fonctions internes) : la voiture peut rouler et être affichée.
Grâce à la conception orientée objet, le pseudo-code est amélioré pour ressembler à ceci :
Données (variables globales) :
- Objet «voiture».
Configuration :
Initialiser l'objet «voiture».
Dessiner :
- Remplir l'arrière-plan.
- Afficher l'objet «voiture».
- Conduire l'objet «voiture».
Remarque : nous avons supprimé toutes les variables globales du premier exemple. Au lieu d'avoir des variables distinctes pour la couleur, l'emplacement et la vitesse de la voiture, nous n'avons plus qu'une seule variable : la variable «voiture» ! Et, au lieu d'initialiser ces trois variables, nous initialisons une seule chose : l'objet « Voiture ». Où sont passées ces variables ? Elles existent toujours, mais elles sont désormais intégrées à l'objet «Voiture» (et seront définies dans la classe «Voiture», que nous aborderons dans un instant).
Au-delà du pseudo-code, le corps du croquis pourrait ressembler à ceci :
Nous allons entrer dans les détails du code ci-dessus dans un instant, mais avant cela, examinons comment la classe Voiture elle-même est écrite.
Écriture de l'emporte-pièce
L'exemple simple de Voiture ci-dessus illustre comment l'utilisation d'objets permet d'obtenir un code clair et lisible. Le plus gros du travail consiste à écrire le modèle d'objet, c'est-à-dire la classe elle-même. Lorsqu'on débute en programmation orientée objet, il est souvent utile de prendre un programme écrit sans objets et, sans en modifier les fonctionnalités, de le réécrire avec des objets. C'est exactement ce que nous allons faire avec l'exemple de la voiture, en recréant exactement la même apparence et le même comportement de manière orientée objet.
Toutes les classes doivent inclure quatre éléments : nom, données, constructeur et méthodes. (Techniquement, le seul élément requis est le nom de la classe, mais l'intérêt de la programmation orientée objet est de les inclure tous.)
Voici comment extraire les éléments d'un simple croquis non orienté objet et les placer dans une classe Voiture, à partir de laquelle nous pourrons ensuite créer des objets Voiture.
Nom de classe : Le nom est spécifié par «class QuelQueSoitLeNomQueVousChoisissez». Nous plaçons ensuite tout le code de la classe entre accolades après la déclaration du nom. Les noms de classe sont traditionnellement en majuscules (pour les distinguer des noms de variables, qui sont traditionnellement en minuscules).
Données&Nbsp;: Les données d'une classe sont un ensemble de variables. Ces variables sont souvent appelées variables d'instance, car chaque instance d'un objet contient cet ensemble de variables.
Constructeur : Le constructeur est une fonction spéciale au sein d'une classe qui crée l'instance de l'objet lui-même. C'est là que vous fournissez les instructions de configuration de l'objet. Il fonctionne comme la fonction initialise(), sauf qu'ici, il est utilisé pour créer un objet individuel dans le croquis, chaque fois qu'un nouvel objet est créé à partir de cette classe. Il porte toujours le même nom que la classe et est appelé par l'opérateur new : Voiture maVoiture = new Voiture();.
Fonctionnalité : Nous pouvons ajouter des fonctionnalités à notre objet en écrivant des méthodes.
Notez que le code d'une classe existe sous forme de son propre bloc et peut être placé n'importe où en dehors de initialise() et dessin().
Utilisation d'un objet : les détails
Nous avons vu précédemment comment un objet peut grandement simplifier les principales étapes d'une esquisse (c'est-à-dire initialise() et dessin()).
Examinons en détail les trois étapes ci-dessus pour utiliser un objet dans votre croquis.
Étape 1 : Déclaration d'une variable objet.
Une variable est toujours déclarée en spécifiant un type et un nom. Avec un type de données primitif, tel qu'un entier, cela ressemble à ceci :
- // Déclaration de variable
- int var; // nom du type
Les types de données primitifs sont des informations singulières : un entier, un nombre à virgule flottante, un caractère,... Déclarer une variable contenant un objet est assez similaire. La différence réside dans le fait qu'ici, le type est le nom de la classe, quelque chose que nous inventons, dans ce cas «Voiture». Les objets, soit dit en passant, ne sont pas des primitifs et sont considérés comme des types de données complexes. (Cela s'explique par le fait qu'ils entreposent plusieurs informations : des données et des fonctionnalités. Les primitifs ne entreposent que des données.)
Étape 2. Initialisation d'un objet.
Pour initialiser une variable (c'est-à-dire lui donner une valeur de départ), nous utilisons une opération d'affectation : variable égale quelque chose. Avec une primitive (comme un entier), cela ressemble à ceci :
- // Initialisation de variable
- var = 10; // var est égal à 10
Initialiser un objet est un peu plus complexe. Au lieu de lui attribuer simplement une valeur, comme un entier ou un nombre à virgule flottante, il faut construire l'objet. Un objet est créé avec l'opérateur new.
- // Initialisation d'objet
- maVoiture = new Voiture(); // L'opérateur new est utilisé pour créer un nouvel objet.
Dans l'exemple ci-dessus, «maVoiture» est le nom de la variable objet et «=» indique que nous la définissons comme égale à une nouvelle instance d'un objet Voiture. En réalité, nous initialisons ici un objet Voiture. Lorsqu'on initialise une variable primitive, comme un entier, on la définit simplement comme égale à un nombre. Or, un objet peut contenir plusieurs données. En repensant à la classe Voiture, nous constatons que cette ligne de code appelle le constructeur, une fonction spéciale nommée Voiture() initialisant toutes les variables de l'objet et s'assure que l'objet Voiture est prêt à fonctionner.
Autre chose : avec l'entier primitif «var», si vous aviez oublié de l'initialiser (en le définissant comme égal à 10), on lui aurait attribué une valeur par défaut : zéro. Un objet (comme «maVoiture»), en revanche, n'a pas de valeur par défaut. Si vous oubliez d'initialiser un objet, on lui attribuera la valeur null. null signifie rien. Ni zéro. Ni moins un. Le néant absolu. Le vide. Si vous rencontrez une erreur dans la fenêtre de message indiquant «NullPointerException» (erreur assez courante), elle est probablement due à un oubli d'initialisation d'un objet.
Étape 3. Utilisation d'un objet.
Une fois une variable objet déclarée et initialisée, nous pouvons l'utiliser. L'utilisation d'un objet implique l'appel de fonctions intégrées à cet objet. Un objet humain peut manger ; une voiture peut conduire ; un chien peut aboyer. L'appel d'une fonction à l'intérieur d'un objet s'effectue via la syntaxe à points :
| nomVariable.fonctionObjet(Fonction Parametres); |
Dans le cas de la voiture, aucune des fonctions disponibles n'a de paramètres, ce qui donne :
- // Les fonctions sont appelées avec la syntaxe à points.
- maVoiture.conduire();
- maVoiture.affichage();
Paramètres du constructeur
Dans les exemples ci-dessus, l'objet voiture a été initialisé à l'aide de l'opérateur new suivi du constructeur de la classe.
- Voiture maVoiture = new Voiture();
Cette simplification nous a été utile pour apprendre les bases de la programmation orientée objet. Cependant, le code ci-dessus présente un problème sérieux. Et si nous souhaitions écrire un programme avec deux objets « voiture » ?
Ceci atteint notre objectif; le code produira deux objets voiture, l'un entreposé dans la variable maVoiture1 et l'autre dans maVoiture2. Cependant, si vous étudiez la classe Voiture, vous remarquerez que ces deux voitures seront identiques : chacune sera blanche, démarrera au milieu de l'écran et aura une vitesse de 1. En français, cela se lit comme suit :
Créer une nouvelle voiture.
Nous souhaitons plutôt dire :
Créer une nouvelle voiture rouge, à l'emplacement (0,10) avec une vitesse de 1.
Nous pourrions donc également dire :
Créer une nouvelle voiture bleue, à l'emplacement (0,100) avec une vitesse de 2.
Nous pouvons le faire en plaçant des paramètres à l'intérieur de la méthode constructeur.
- Voiture maVoiture = new Voiture(color(255,0,0),0,100,2);
Le constructeur doit être réécrit pour incorporer ces paramètres :
L'utilisation de paramètres de constructeur pour initialiser des variables d'objet peut être quelque peu déroutante. Ne vous en voulez pas. Le code est étrange et peut paraître terriblement redondant : «Je dois placer des paramètres dans le constructeur pour chaque variable ?»
Néanmoins, c'est une compétence essentielle à maîtriser et, en fin de compte, c'est l'un des atouts de la programmation orientée objet. Mais pour l'instant, cela peut paraître complexe. Voyons comment fonctionnent les paramètres dans ce contexte.
Les paramètres sont des variables locales utilisées dans le corps d'une fonction et étant renseignées lors de l'appel de la fonction. Dans les exemples, ils n'ont qu'un seul but : initialiser les variables d'un objet. Ce sont ces variables comptant : la couleur réelle de la voiture, sa position x,... Les paramètres du constructeur sont temporaires et servent uniquement à transmettre une valeur depuis l'emplacement de création de l'objet vers l'objet lui-même.
Cela permet de créer divers objets avec le même constructeur. Vous pouvez également simplement utiliser le mot «temp» dans le nom de vos paramètres pour vous rappeler ce qui se passe (c vs. tempC). Vous verrez également les programmeurs utiliser un trait de soulignement (c vs. c_) dans de nombreux exemples. Vous pouvez bien sûr les nommer comme vous le souhaitez. Cependant, il est conseillé de choisir un nom cohérent et cohérent.
Examinons maintenant le même croquis avec plusieurs instances d'objet, chacune possédant des propriétés uniques :
- Voiture maVoiture1;
- Voiture maVoiture2; // Deux objets !
-
- void initialise() {
- size(200,200);
- // Les paramètres sont placés entre parenthèses lorsque l'objet est construit.
- maVoiture1 = new Voiture(color(255,0,0),0,100,2);
- maVoiture2 = new Voiture(color(0,0,255),0,10,1);
- }
-
- void dessin() {
- background(255);
- maVoiture1.conduire();
- maVoiture1.affichage();
- maVoiture2.conduire();
- maVoiture2.affichage();
- }
-
- // Même s'il y a plusieurs objets, nous n'avons besoin que d'une seule classe.
- // Peu importe le nombre de biscuits que nous préparons, un seul emporte-pièce est nécessaire.
- class Voiture {
- color c;
- float xpos;
- float ypos;
- float xspeed;
-
- // Le constructeur est défini avec des paramètres.
- Voiture(color tempC, float tempXpos, float tempYpos, float tempXspeed) {
- c = tempC;
- xpos = tempXpos;
- ypos = tempYpos;
- xspeed = tempXspeed;
- }
-
- void affichage() {
- stroke(0);
- fill(c);
- rectMode(CENTER);
- rect(xpos,ypos,20,10);
- }
-
- void conduire() {
- xpos = xpos + xspeed;
- if (xpos > width) {
- xpos = 0;
- }
- }
- }
Les objets sont aussi des types de données !
Si vous débutez avec la programmation orientée objet, il est important d'y aller doucement. Les exemples présentés ici se limitent à une seule classe et créent, au maximum, deux ou trois objets à partir de cette classe. Néanmoins, il n'y a aucune limitation réelle. Un sketch peut inclure autant de classes que vous le souhaitez.
Si vous programmiez le jeu Space Invaders, par exemple, vous pourriez créer une classe Vaisseau spatial, une classe Ennemi et une classe Balle, en utilisant un objet pour chaque entité de votre jeu.
De plus, bien que non primitives, les classes sont des types de données, tout comme les entiers et les nombres à virgule flottante. Et comme les classes sont constituées de données, un objet peut donc en contenir d'autres ! Par exemple, supposons que vous veniez de terminer la programmation d'une classe Fourchette et Cuillère. En passant à une classe PlaceSetting, vous incluriez probablement des variables pour un objet Fourchette et un objet Cuillère dans cette classe. C'est parfaitement raisonnable et assez courant en programmation orientée objet.
Les objets, comme tout type de données, peuvent également être transmis comme paramètres à une fonction. Dans l'exemple du jeu Space Invaders, si le vaisseau spatial tire sur l'ennemi, nous souhaiterions probablement écrire une fonction dans la classe Ennemi pour déterminer si l'ennemi a été touché par la balle.
- void hit(Balle b) {
- // Code pour déterminer si la balle a touché l'ennemi
- }
Lorsqu'une valeur primitive (entier, flottant,...) est passée à une fonction, une copie est effectuée. Avec les objets, ce n'est pas le cas, et le résultat est un peu plus intuitif. Si des modifications sont apportées à un objet après son passage à une fonction, elles affecteront cet objet utilisé ailleurs dans le schéma. On parle alors de passage par référence, car, au lieu d'une copie, c'est une référence à l'objet lui-même qui est passée à la fonction.