cours13 - sbalev/processing101 GitHub Wiki
Avant de nous plonger dans les détails de la programmation orientée objet (POO), discutons brièvement le concept d'objet. Il est important de comprendre que les objets ne sont pas une nouveauté fondamentale. Nous allons continuer d'utiliser tout ce qu'on a appris jusqu'à maintenant : variables, conditionnelles, boucles, fonctions, etc. Ce qui est complètement nouveau est une façon de penser, une nouvelle façon de structurer et organiser notre code afin de le rendre plus lisible et plus modulaire, et donc plus facile à développer, maintenir et réutiliser.
Imaginez que vous faites le programme de votre journée. Il pourrait commencer ainsi :
- Se réveiller
- Prendre une douche
- Boire du café
- Prendre le petit déjeuner
- Prendre le tram
- ...
Quels objets sont impliqués ici ? L'objet principal c'est vous, un être humain, une personne. Vous avez certaines propriétés : peut-être mesurez-vous 195 cm, vous avez des cheveux roux, vous portez des lunettes et un nœud papillon. Vous êtes également capable de faire des choses : dormir et se réveiller, boire, manger, utiliser différents types de transport, etc.
Comment tout ça est-il lié à la programmation ? Les propriétés (ou encore données, attributs ou champs) d'un objet sont des variables et les choses qu'il peut faire (méthodes) sont des fonctions. La POO combine tout ce qu'on a appris jusqu'à maintenant, données et fonctionnalités, dans une chose, l'objet.
Énumérons quelques attributs et quelques méthodes d'un objet personne très simplifié :
- Attributs :
- prénom
- taille
- poids
- couleur des cheveux
- couleur des yeux
- Méthodes :
- dormir
- se réveiller
- boire
- manger
- se déplacer
La structure ci-dessus n'est pas une personne en soi, c'est simplement la description de ce que c'est une personne. Une personne a une taille, un poids, peut dormir, se déplacer etc. Cette distinction est très importante dans la POO. Ce « patron » d'être humain est appelé classe. Une classe n'est pas un objet. Vous êtes un objet. Je suis un objet. Nous sommes tous des instances de la classe être humain.
Pensez à un emporte-pièce. Il fait des biscuits mais n'est pas un biscuit. L'emporte-pièce est la classe et les biscuits sont des objets.
Source Wikipédia
Exercice 13.1. Décrivez la classe voiture. Donnez une liste d'attributs et une liste de méthodes.
Exercice 13.2. Le livre « Processing : s'initier à la programmation créative » de Jean-Michel Géridan et Jean-Noël Lafargue que vous trouverez dans le rayon 006 au premier étage de la BU serait-il modélisé par un objet ou par une classe ? Si c'est une classe, nommez-en certains objets. Si c'est un objet, décrivez sa classe.
Source Geektionerd
Pour mieux comprendre comment les objets font du monde un meilleur endroit, commençons par un exemple simple qui ne les utilise pas.
Exemple 13.1. Voiture
// données de la voiture
float x, y;
float vitesse;
color couleur;
float taille;
void setup() {
size(400, 400);
// initialiser la voiture
x = 0;
y = height / 2;
vitesse = 1;
couleur = color(255, 0, 0);
taille = 50;
}
void draw() {
background(0);
// dessiner la voiture
rectMode(CENTER);
fill(couleur);
rect(x, y, taille, taille / 2);
// bouger la voiture
x += vitesse;
if (x > width) {
x = 0;
}
}
Les données de la voiture sont des variables globales et ses fonctionnalités sont implémentées dans draw()
.
Cours 11 nous a appris comment rendre notre code modulaire en mettant à part les fonctionnalités dans des fonctions qu'on appelle depuis draw()
.
Exemple 13.2. Voiture avec fonctions
// données de la voiture
float x, y;
float vitesse;
color couleur;
float taille;
// fonctionnalités de la voiture
void dessiner() {
rectMode(CENTER);
fill(couleur);
rect(x, y, taille, taille / 2);
}
void bouger() {
x += vitesse;
if (x > width) {
x = 0;
}
}
void setup() {
size(400, 400);
// initialiser la voiture
x = 0;
y = height / 2;
vitesse = 1;
couleur = color(255, 0, 0);
taille = 50;
}
void draw() {
background(0);
dessiner();
bouger();
}
La POO nous permet d'extraire toutes les variables et les fonctions du programme principal et de les mettre à part dans un objet voiture. Cet objet contient :
- des attributs (variables à l'intérieur d'un objet)
- des méthodes (fonctions à l'intérieur d'un objet)
Avant d'écrire la classe Voiture
, donnons un aperçu rapide de notre programme principal qui l'utilise :
Voiture maVoiture;
void setup() {
size(400, 400);
// initialiser la voiture
maVoiture = new Voiture();
}
void draw() {
background(0);
maVoiture.dessiner();
maVoiture.bouger();
}
Nous reviendrons sur les détails de ce code plus tard. Notez pour l'instant comment toutes les variables globales ont été remplacées par une seule et comment au lieu d'initialiser toutes ces variables, on initialise une seule chose, un objet Voiture
. Elle n'est pas belle la vie ?
Où sont passées toutes ces variables ? Elles existent toujours mais elles sont maintenant dans notre objet et nous allons les définir dans la classe Voiture
.
Nous avons vu comment la POO rend notre code propre et lisible. Mais il nous reste encore la partie la plus difficile du travail : écrire le « patron » des objets, la classe.
On utilise la construction suivante pour définir une classe :
class Voiture {
// attributs
...
// constructeur(s)
...
// méthodes
...
}
Par convention le nom de la classe (dans notre cas Voiture
) commence par une majuscule. Cela permet de faire une distinction entre classes et variables (qui commencent par une minuscule). Le code de la classe est dans un bloc qu'on peut mettre n'importe où en dehors de setup()
et draw()
. La meilleure pratique consiste à mettre la classe dans un nouveau fichier qui porte le même nom que celui de la classe.
Et voici comment on place les éléments de l'exemple 13.2 dans notre classe Voiture
à partir de laquelle nous serons capables de créer des objets.
class Voiture {
// attributs
float x, y;
float vitesse;
color couleur;
float taille;
// constructeur
Voiture() {
x = 0;
y = height / 2;
vitesse = 1;
couleur = color(255, 0, 0);
taille = 50;
}
// méthodes
void dessiner() {
rectMode(CENTER);
fill(couleur);
rect(x, y, taille, taille / 2);
}
void bouger() {
x += vitesse;
if (x > width) {
x = 0;
}
}
}
Le corps de la classe contient trois groupes d'éléments :
-
Attributs : Il s'agit d'un ensemble de variables. On les appelle également champs ou encore variables d'instance parce que chaque objet (instance de la classe) a son propre ensemble de variables.
-
Constructeur : C'est une fonction spéciale à l'intérieur de la classe qui sert à instancier des objets. C'est ici qu'on écrit des instructions qui initialisent l'objet. Elle porte toujours le même nom que celui de la classe et elle est appelée quand on utilise l'opérateur
new
:Voiture maVoiture = new Voiture();
-
Méthodes : Ce sont des fonctions qu'on écrit de la façon habituelle en précisant le type de retour, le nom, la liste d'arguments et le bloc de code qui constitue le corps.
Maintenant qu'on a fait notre classe, regardons de plus près le code de notre programme principal :
// Pas 1 : Déclaration d'un objet
Voiture maVoiture;
void setup() {
size(400, 400);
// Pas 2 : Initialisation de l'objet
maVoiture = new Voiture();
}
void draw() {
background(0);
// Pas 3 : utilisation de l'objet
maVoiture.dessiner();
maVoiture.bouger();
}
Il y a bien longtemps, dans un cours 4 lointain, très lointain, nous avons vu qu'on déclare une variable en précisant son type et son nom :
int var;
Dans cet exemple nous déclarons une variable qui stocke des valeurs d'un type primitif (int
). La déclaration d'une variable qui stocke un objet est similaire :
Voiture maVoiture;
Ici le type de la variable est le nom de la classe. En définissant des nouvelles classes, on enrichit l'ensemble de types qu'on peut utiliser.
Dans le même cours très lointain nous avons vu qu'on peut utiliser l'opérateur d'affectation pour initialiser une variable (lui donner une valeur de départ) :
var = 42;
L'initialisation d'un objet est un peu plus compliquée. Au lieu d'affecter une valeur primitive comme par exemple un nombre entier ou réel, nous devons construire un objet en utilisant l'opérateur new
:
maVoiture = new Voiture();
L'opérateur new
crée un nouvel objet et l'initialise. Pour ce faire, il appelle le constructeur de la classe, la fonction spéciale Voiture()
que nous avons écrit. Ce constructeur initialise les champs du nouvel objet et s'assure que la nouvelle voiture est prête à démarrer.
Une autre différence importante est que si vous oubliez d'initialiser une variable de type primitif, Processing va lui affecter une valeur par défaut (zéro pour les types numériques, false
pour les booléens). La valeur par défaut des objets est null
. null
signifie rien. Rien du tout. Même pas zéro. Un néant absolu. Un vide interstellaire. Si votre sketch plante avec une erreur NullPointerException
(et c'est une erreur très commune), la cause est très probablement un oubli d'initialisation.
Après avoir déclaré et initialisé un objet, on peut commencer à appeler ses méthodes. Une personne peut parler, un chien peut aboyer, un poisson peut nager. On appelle une méthode sur un objet en utilisant la syntaxe suivante :
nomDeVariable.methode(arguments);
Dans notre exemple les méthodes n'ont pas d'arguments, nous les appelons donc ainsi :
maVoiture.dessiner();
maVoiture.bouger();
Et voici notre exemple complet en deux fichiers :
Exemple 13.3. Voiture objet
Fichier exemple_13_03_voiture_objet.pde
Voiture maVoiture;
void setup() {
size(400, 400);
// initialiser la voiture
maVoiture = new Voiture();
}
void draw() {
background(0);
maVoiture.dessiner();
maVoiture.bouger();
}
Fichier Voiture.pde
class Voiture {
// attributs
float x, y;
float vitesse;
color couleur;
float taille;
// constructeur
Voiture() {
x = 0;
y = height / 2;
vitesse = 1;
couleur = color(255, 0, 0);
taille = 50;
}
// méthodes
void dessiner() {
rectMode(CENTER);
fill(couleur);
rect(x, y, taille, taille / 2);
}
void bouger() {
x += vitesse;
if (x > width) {
x = 0;
}
}
}
Exercice 13.3. En supposant qu'il existe une classe Grenouille
avec des méthodes sauter()
et mangerMouche()
, donnez le code qui déclare et initialise une grenouille gamabunta
. Ensuite la grenouille fait 10 sauts en mangeant une mouche un saut sur deux.
Exercice 13.4. Reprenez l'exemple 11.3. Transformez la balle rebondissante en objet.
Dans notre exemple précédent on crée et initialise un objet en utilisant l'opérateur new
suivi par le constructeur de la classe :
Voiture maVoiture = new Voiture();
C'est une simplification utile pour introduire les bases de la POO mais nous avons un problème sérieux avec ce code. Et si on voulait créer deux voitures ?
Voiture vouture1 = new Voiture();
Voiture vouture2 = new Voiture();
En effet, ce code crée deux objets différents stockés dans deux variables, mais si on étudie le code de notre classe, on se rend compte que les deux voitures sont identiques : elles sont toutes les deux à la même position, de vitesse 1, couleur rouge et taille 50. On peut traduire en français ce code ainsi :
Créer une nouvelle voiture.
Ce qu'on veut dire à la place est plutôt :
Créer une nouvelle voiture à la position (0, 200) de vitesse 1, couleur rouge et taille 50.
Ainsi on pourrait également dire :
Créer une nouvelle voiture à la position (100, 300) de vitesse 5, couleur verte et taille 30.
On peut faire cela en mettant des arguments dans le constructeur :
Voiture maVoiture = new Voiture(0, 200, 1, color(255, 0, 0), 50);
Pour que cela marche, il faut ajouter un autre constructeur dans la classe Voiture
:
Voiture(float tmpX, float tmpY, float tmpVitesse, color tmpCouleur, float tmpTaille) {
x = tmpX;
y = tmpY;
vitesse = tmpVitesse;
couleur = tmpCouleur;
taille = tmpTaille;
}
Rappelons que les arguments sont des variables locales utilisées à l'intérieur de la fonction et remplies avec les valeurs qu'on passe quand on appelle la fonction. Dans notre cas ils ont un seul objectif, initialiser les variables d'instance. Ce sont des variables temporaires qui servent uniquement à passer des valeurs de l'endroit où l'objet est créé vers l'objet lui-même.
On peut les nommer comme on veut, mais il est préférable d'utiliser des noms qui rappellent les noms des attributs et de rester consistant. On peut même utiliser les mêmes noms, mais dans ce cas les arguments du constructeur vont « cacher » les attributs (de la même façon qu'une variable locale cache une variable globale). On peut néanmoins accéder aux variables de l'objet en utilisant le mot-clef this
:
Voiture(float x, float y, float vitesse, color couleur, float taille) {
this.x = x;
this.y = y;
this.vitesse = vitesse;
this.couleur = couleur;
this.taille = taille;
}
Ici this.x
signifie « la variable x
de cet objet » (l'objet qu'on est en train d'initialiser).
Maintenant on peut créer plusieurs objets, chacun avec ses propriétés uniques.
Exemple 13.4. Plusieurs voitures
Voiture v1, v2, v3;
void setup() {
size(400, 400);
v1 = new Voiture(100, height / 4, 5, 255, 30);
v2 = new Voiture(); // on utilise le constructeur "par défaut" sans arguments
v3 = new Voiture(50, 3 * height / 4, 3, color(0, 126, 198), 80);
}
void draw() {
background(0);
v1.dessiner();
v1.bouger();
v2.dessiner();
v2.bouger();
v3.dessiner();
v3.bouger();
}
Même si on crée plusieurs objets, on a besoin d'une seule classe. Peu importe le nombre de biscuits qu'on fait, un seul emporte-pièce suffit. Elle n'est pas classe la POO ? Pour mieux vous rendre compte de la puissance de cette approche, essayez de faire la même chose avec la version sans objet de l'exemple 13.2. Bon courage !
Exercice 13.5 Utilisez la classe suivante
float gravite = 0.1;
class Balle {
float x, y;
float rayon;
color couleur;
float elasticite;
float vitesse;
Balle(float x, float y, float rayon, color couleur, float elasticite) {
this.x = x;
this.y = y;
this.rayon = rayon;
this.couleur = couleur;
this.elasticite = elasticite;
vitesse = 0;
}
void dessiner() {
noStroke();
fill(couleur);
ellipseMode(RADIUS);
ellipse(x, y, rayon, rayon);
}
void bouger() {
y += vitesse;
vitesse += gravite;
}
void rebondir() {
if (y > height - rayon) {
vitesse *= - elasticite;
}
}
}
pour faire plusieurs balles qui tombent et rebondissent sous l'effet de la gravité comme dans l'exemple 8.4.
Exercice 13.6. Le sketch suivant implante un simple sélecteur de couleurs. Il utilise six boutons poussoirs pour monter / baisser le niveau des trois composantes de la couleur du fond.
int r = 0, g = 0, b = 0;
Bouton rUp, rDown, gUp, gDown, bUp, bDown;
String rgb;
void setup() {
size(400, 400);
int t = 25; // taille du bouton
rUp = new Bouton(width / 2 - 2 * t, height / 2 - t, t, color(255, 0, 0), '+');
rDown = new Bouton(width / 2 - 2 * t, height / 2 + t, t, color(255, 0, 0), '-');
gUp = new Bouton(width / 2, height / 2 - t, t, color(0, 255, 0), '+');
gDown = new Bouton(width / 2, height / 2 + t, t, color(0, 255, 0), '-');
bUp = new Bouton(width / 2 + 2 * t, height / 2 - t, t, color(0, 0, 255), '+');
bDown = new Bouton(width / 2 + 2 * t, height / 2 + t, t, color(0, 0, 255), '-');
}
void draw() {
background(r, g, b);
noStroke();
fill(0);
rectMode(CORNER);
rect(10, 10, width - 20, 40);
rgb = "color(" + r + ", " + g + ", " + b + ")";
fill(255);
textAlign(CENTER, CENTER);
text(rgb, width / 2, 30);
rUp.dessiner();
rDown.dessiner();
gUp.dessiner();
gDown.dessiner();
bUp.dessiner();
bDown.dessiner();
if (mousePressed) {
if (rUp.sourisDedans()) {
r++;
} else if (rDown.sourisDedans()) {
r--;
} else if (gUp.sourisDedans()) {
g++;
} else if (gDown.sourisDedans()) {
g--;
} else if (bUp.sourisDedans()) {
b++;
} else if (bDown.sourisDedans()) {
b--;
}
r = constrain(r, 0, 255);
g = constrain(g, 0, 255);
b = constrain(b, 0, 255);
}
}
void keyPressed() {
println(rgb);
}
Pour rendre ce sketch fonctionnel, écrivez la classe Bouton
qui va bien avec.
Jusqu'à maintenant on n'a utilisé que des méthodes qui n'ont pas de type de retour (void
). Ici vous devez écrire une méthode sourisDedans()
qui renvoie un booléen.
Exercice 13.7. Réutilisez la classe Bouton
de l'exercice précédent pour faire un bouton interrupteur qui allume et éteint la lumière (comme dans l'exemple 7.3).