10. La programmation orientée objet
Jusqu'à présent, nous avons vu la programmation dite procédurale. C'est-à-dire que les différents éléments sont exécutés dans l'ordre, telle une procédure. Nous allons maintenant voir la programmation orientée objet (OOP) ; cette pratique amène une notion d'entité, elle permet de créer des objets qui ont chacun des données (variables) et des traitements (méthodes) spécifiques.
Une meilleure lisibilité, modularité et cohérence du programme est garantie avec la OOP. C'est en partie grâce au lien explicite qu'il existe désormais entre les données et traitements. La OOP repose sur quatre concepts principaux que nous allons voir ci-dessous :
- l'encapsulation
- l'abstraction
- l'héritage
- le polymorphisme
Une entité est, en programmation, une classe comportant des données et des traitements.
10.1 Encapsulation et abstraction
De manière générale, l'encapsulation et l'abstraction consistent à dissimuler certains détails d'implémentation à l'utilisateur. Il y a ainsi deux niveaux de perception d'un objet :
- le niveau externe : concerne le programmeur utilisateur, ce qu'il voit
- le détail des méthodes lui sont par exemple caché puisqu'il n'a pas besoin de savoir comment fonctionne une méthode mais simplement ce qu'elle fait
- le niveau interne : concerne le programmeur concepteur, ce qu'il développe
- c'est lui qui se charge des détails d'implémentation, comment fonctionne une méthode par exemple
Encapsulation : c'est le fait de regrouper en une seule et même entité des données et des traitements. Les données sont désignées par le terme "attribut", et les traitements sont appelés "méthode". Le tout, c'est-à-dire l'entité est défini au sein d'une classe. Celle-ci représente un objet et devient alors un nouveau type évolué.
Abstraction : c'est le fait d'avoir une descritpion générique (abstraite) d'une entité qui comporte des caractéristiques communes (données et traitements). C'est-à-dire que deux mêmes entités contiennent les mêmes caractéristiques, ont accès aux mêmes actions.
Exemple : imaginons que nous voulons représenter l'entité "personnage". Avant de se lancer dans la définition de cette nouvelle classe, il est important de réfléchir aux quelles caractéristiques que comporte une personnage :
- il porte un nom (attribut)
- il a un niveau de vie (attribut)
- il peut marcher (méthode)
- il peut courir (méthode)
- ...
class Character {
// Attributs
String name;
int lifeLevel;
// Méthodes
void walk() {
// ...
}
void run() {
// ...
}
}
10.2 Classes, attributs et méthodes
Classes
Comme nous l'avons déjà vu, une classe est un nouveau type de données évolué dont les instances sont des objets.
- La déclaration se fait avec le mot-clé
class
class NomClass { ... }
- La déclaration d'une variable du nouveau type peut se faire après que la classe ait été déclarée
NomClass nomInstance;
Une instance de classe est une variable dont le type est une classe.
Rappel : étant donné que ce type est considéré comme évolué, l'instance stocke alors la référence à l'objet et non l'objet lui-même.
Comme pour les variables, l'initialisation d'une instance peut se faire en même temps que la déclaration ou dans un deuxième temps :
// Déclaration-initialisation
NomClasse nomInstance = new NomClasse(...);
// Déclaration, puis initialisation
NomClasse nomInstance;
nomInstance = new NomClasse(...);
Il est important de ne pas oublier le mot-clé
new
.
Attributs
La déclaration des attributs se fait de la même manière que celle des variables : type nomAttribut;
Une instance peut accéder aux valeurs des attributs assez simplement : nomInstance.nomAttribut
Méthodes
Les méthodes des classes sont déclarées de la même manière que les méthodes vues au module précédent. Elles comportent également un type de retour, un nom, une liste de paramètres et un corps.
Ce sont les instances de classes qui vont pouvoir appeler et utiliser ces méthodes. Les paramètres passés en argument à la méthode peuvent alors être externe à la classe.
Un appel de méthode se fait à travers l'instance de classe, de façon similaire que l'accès à l'attribut : nomInstance.nomMethode(argument1, argument2, ...);
public
et private
Nous avons vu qu'avec l'encapsulation et l'abstraction, nous avions différents niveaux de perception. Certains éléments sont dissimulés à l'utilisateur et gardé pour le concepteur, tandis que pour d'autres éléments, l'implémentation est accessible pour tout utilisateur.
Pour représenter ces niveaux interne et externe, il existe deux mots-clés :
private
indique que l'implémentation de l'élément est inaccessible au programmeur utilisateur- de manière générale, tous les attributs sont privés ainsi que certains méthodes
public
indique que l'implémentation de l'élément est visible et que l'élément lui-même est utilisable par le programmeur utilisateur- de manière générale, seules quelques méthodes sont publiques
La bonne pratique est d'indiquer pour tout élément (classe, attribut et méthode) s'il est privé ou public. Sans spécification, l'élément aura le droit d'accès par défaut, c'est-à-dire visible, accessible et utilisable partout :
public
.
Du fait qu'un attribut soit private
, on ne peut plus y accéder par l'instance avec nomInstance.nomAttribut
. Il nous faut désormais ce qu'on appelle des getters et des setters pour chaque attribut privé qui en ont besoin.
- getter : méthode publique permettant d'accéder à l'attribut
- setter : méthode publique permettant de modifier l'attribut
La bonne pratique est d'implémenter des getters et setters pour les attributs privés plutôt que de mettre les attributs publics.
Les getters et setters sont utiles uniquement si les attributs ont besoin d'être accessibles ou modifiables hors de la classe !
/* Character.java */
// package ...
public class Character {
// Attributs
private String name;
private int lifeLevel;
// Getters et setters
public String getName() {
return this.name;
}
public void setName(String newName) {
this.name = newName;
}
public int getLifeLevel() {
return this.lifeLevel;
}
public void setLifeLevel(int newLifeLevel) {
this.lifeLevel = newLifeLevel;
}
// Autres méthodes
public walk() {
// ...
}
public run() {
// ...
}
}
this
désigne l'instance courante, c'est-à-dire un attribut de classe. La syntaxe sera toujoursthis.attribut
.Ceci permet de distinguer l'attribut d'un paramètre de méthode qui pourrait porter le même nom.
Constructeurs
Lorsque nous initialisons un objet, c'est-à-dire une instance de classe, nous utilisons en réalité le constructeur de l'objet qui permet d'initialiser les attributs également.
Un constructeur est donc une méthode publique qui permet de créer un objet et d'initialiser les attributs de l'objet. Cette méthode possède alors un paramètre par attribut de classe.
Schéma du constructeur :
NomClasse (type1 param1, type2 param2, ...) {
// Initialisation des attributs en utilisant les paramètres
this.attribut1 = param1;
this.attribut2 = param2;
// ...
}
Exemple :
public class Character {
// Attributs
private String name;
private int lifeLevel;
// Constructeur
Person(String name, int lifeLevel) {
this.name = name;
this.aglifeLevele = lifeLevel;
}
// ...
Le constructeur est une méthode spéciale : elle porte le même nom que la classe et n'a pas de type de retour.
Déclaration et initialisation d'un objet avec attributs :
NomClasse nomInstance = new NomClasse(argument1, argument2, ...);
Déclaration et initialisation d'un objet sans attribut ou utilisant le constructeur par défaut :
NomClasse nomInstance = new NomClasse();
Il existe deux constructeurs par défaut :
- le constructeur par défaut (explicite)
- le constructeur par défaut par défaut
Le premier constructeur par défaut est celui qui ne demande aucun paramètre puisqu'il précise lui-même des valeurs par défaut à ces attributs.
public class Character {
// Attributs
private String name;
private int lifeLevel;
// Constructeur par défaut
Person() {
this.name = "Charlie";
this.lifeLevel = 100;
}
// ...
Le second constructeur par défaut est celui que le compilateur génère automatiquement si aucun constructeur n'a été spécifié. Sa version est minimale et les attributs sont initialisés avec des valeurs par défaut.
0
pour les nombres entiers0.0
pour les nombres décimauxfalse
pour les booléensnull
pour les objets
public class Character {
// Attributs
private String name;
private int lifeLevel;
// ...
10.3 Héritage
L'héritage permet une notion de spécialisation. Une super-classe contient des caractéristiques communes dont une sous-classe hérite si nous les lions par l'héritage. La sous-classe est alors une version spécialisée et enrichie de la super-classe.
Concrètement, cela signifie que la sous-classe hérite de tous les attributs et méthodes de la super-classe, à l'exception du constructeur. L'héritage se fait grâce au mot-clé extends
.
public class Character {
// ...
}
public class Avatar extends Character {
// ...
}
public class Enemy extends Character {
// ...
}
La classe Avatar
hérite des attributs et des méthodes de Character
. Même si elle dispose des attributs de sa super-classe, elle n'y a pas directement accès, elle doit passer par les getters et setters.
Spécialisation : la redéfinition est le fait de redéfinir une méthode dans la sous-classe qui est héritée de la super-classe. Une sous-classe peut aussi se spécialiser en ayant ses propres attributs et méthodes.
Constructeur : bien qu'il y ait un constructeur dans la super-classe, chaque sous-classe doit avoir son propre constructeur. Cependant, les attributs hérités de la super-classe doivent être initialisés avec le constructeur de la super-classe. Cela se fait grâce au mot-clé super
.
/* Avatar.java */
// package ...
public class Avatar extends Character {
private String strategy;
public Avatar(String name, int lifeLevel, String strategy) {
// Initialisation des attributs hérités
super(name, lifeLevel);
// Initialisation des attributs propres à Avatar
this.strategy = strategy;
}
}
On distingue alors la relation "est-un" de la relation "a-un".
- "est-un" : un
Avatar
est unCharacter
(HERITAGE)- "a-un" : un
Character
a unname
(ATTRIBUT)
10.4 Polymorphisme
Le polymorphisme est étroitement lié à la notion d'héritage. Il permet qu'un même code puisse s'exécuter de façon différente selon l'entité à laquelle il s'applique.
Imaginons, par exemple, que notre super-classe Character
possède la méthode defend()
. Cette méthode doit être redéfinie dans chaque sous-classe car chaque type de personnage a sa propre manière de se défendre. Il est alors difficile pour Character
de définir sa méthode defend()
. La solution est d'abstraire cette méthode dans la super-classe avec le mot-clé abstract
.
/* Character.java */
// package ...
public class Character {
// ...
public abstract void defend();
}
Ainsi, à ce niveau-là, la méthode ne possède pas de corps et cela force toutes les sous-classes à la redéfinir.
Du moment qu'une classe possède une méthode abstraite, alors la classe elle-même devient abstraite. Cela signifie qu'il n'est pas possible d'avoir une instance de cette classe. Elle permet de définir des concepts génériques pour ses sous-classes.
/* Character.java */
// package ...
public abstract class Character {
// ...
public abstract void defend();
}
// Ce n'est plus possible de faire :
// Character character;
Désormais, toute classe héritant de Character
doit définir la méthode defend()
. Sinon, la sous-classe sera elle-même abstraite et ne pourra pas être instanciée.