Un des principes de la programmation orientée objet et de protéger l’accès aux attributs. On ne lis pas directement leurs valeurs et on ne les modifie pas directement non plus. Plutôt on écrit des méthodes qui retournent la valeur des attributs et des méthodes qui modifient la valeur des attributs. Ainsi, les attributs sont cachés via une interface (les méthodes); cette couche forme la capsule protectrice autour des données.
public
et private
sont les deux mots-clés de visibilité les plus importants en Java. public
rend les membres de la classe visibles à l’extérieur de la classe. private
les rend invisibles à l’extérieur de la classe. Par défaut (sans mot-clé explicite), la visibilité est public
aux autres classes dans le même dossier (package
) et private
à l’extérieur du dossier.public
qui donnent accès aux attributs private
d’une classe. Les accesseurs (get[Attribut]
) retournent la valeur de l’attribut. Les mutateurs (set[Attribut]
) modifient la valeur de l’attribut selon la valeur du paramètre. Autre que simplement assigner ou retourner la valeur, ces méthodes peuvent aussi appliquer différents traitements (validation de la valeur, mise à jour d’autres attributs, etc.). Cela fait partie des “détails d’implémentation” qui sont cachés à l’extérieur de la classe mais qui permettent à la classe de fonctionner comme prévu.À la fin de cette leçon vous devrez être en mesure de :
private
et public
.Voici un exemple pour illustrer l’utilité du masquage de l’information. Il présente un cas où l’absence de ce type d’encapsulation peut mener à un objet qui ne respecte plus les règles de sa définition (un carré qui n’est plus un carré).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* Classe pour représenter un carré
* Version sans encapsulation
*/
class Square {
double side;
double area;
double perimeter;
Square(double side) {
update(side);
}
Square() {
this(1); // valeur par défaut
}
/**
* Méthode pour valider et mettre à jour les attributs
* @param side la longueur de côté
*/
void update(double side) {
if (side < 0) {
System.out.println("Longueur invalide");
return;
}
this.side = side;
// area et perimeter dépendent de side
this.area = side*side;
this.perimeter = 4*side;
}
@Override
public String toString() {
return String.format("Côté = %.2f, A = %.2f, P = %.2f",
side, area, perimeter);
}
}
Cette classe a trois attributs mais deux, area
et perimeter
, dépendent de la valeur du dernier, side
.
Regardons maintenant la classe pilote qui inclut le code valide suivant :
1
2
3
4
5
6
7
8
class Main {
public static void main(String[] args) {
Square sq = new Square(2);
System.out.println(sq);
sq.area = -27; // instruction valide qui brise le concept de carré
System.out.println(sq);
}
}
qui produit la sortie suivante :
1
2
Côté = 2.00, A = 4.00, P = 8.00
Côté = 2.00, A = -27.00, P = 8.00
Cette sortie montre un carré qui ne respecte plus la définition d’un carré. Notre objet n’est plus un modèle valide pour ce qu’il essaye de représenter. Le problème est la 3e ligne dans main
où on modifie directement l’attribut area
.
L’encapsulation prévient ce genre d’incohérence en nous permettant de spécifier la visibilité (voire l’accès) des attributs et des méthodes. Pour le moment, seulement deux mots-clés sont importants 1 :
public
qui rend les membres de la classe visibles dans toutes les autres classesprivate
qui rend les membres de la classe visibles seulement à l’intérieur de la classe (pour les autres membres)L’absence d’un mot-clé de visibilité donne au membre la visibilité par défaut qui est public
.2 Donc, dans l’exemple de la classe Square
ci-dessus, la classe et tout ses membres sont public
.
Une classe encapsulée déclare ses données (les attributs) private
et inclut des méthodes public
pour lire les données et pour modifier les données.
Ces méthodes s’appellent :
Les accesseurs sont du même type que la donnée demandée et ne prennent aucun paramètre.
Les mutateurs sont de type void et prennent un paramètre du même type que la donnée à modifier.
Square
avec l’encapsulationVoici une version encapsulée de la classe Square :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* Classe pour représenter un carré
* Version appliquant encapsulation
*/
class Square {
// attributs masqués à l'extérieur de la classe
private double side;
private double area;
private double perimeter;
Square(double side) {
update(side);
}
Square() {
this(1); // valeur par défaut
}
// update() est le seul mutateur pour la classe puisqu'il
// modifie tous les attributs de manière contrôlée.
/**
* Méthode pour valider et mettre à jour les valeurs.
* @param side la longueur de côté
*/
public void update(double side) {
if (side < 0) {
System.out.println("Longueur invalide");
return;
}
this.side = side;
// area et perimeter dépendent de side
this.area = side*side;
this.perimeter = 4*side;
}
// trois accesseurs pour lire les valeurs à l'extérieur de la classe
public double getSide() {
return this.side;
}
public double getArea() {
return this.area;
}
public double getPerimeter() {
return this.perimeter;
}
@Override
public String toString() {
return String.format("Côté = %.2f, A = %.2f, P = %.2f",
side, area, perimeter);
}
}
Maintenant la ligne
1
sq.area = -27;
dans le pilote produit une erreur parce que l’accès à area
est rendu private
. On ne peut plus accidentellement (ou intentionellement) briser notre objet dans son objectif de représenter un carré.
Dans l’exemple de la classe Square
, le mutateur était taillé sur mesure pour l’objet, ce qui est souvent requis. Mais il y a aussi plein de cas où des mutateurs plus génériques sont suffisants. La classe Point
ci-dessous est un bon exemple.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Point {
private int x;
private int y;
public void setX(int x){
this.x = x;
}
public void setY(int y){
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}
Dans cette classe partiellement implémentée (il manque : constructeurs, toString, etc.) mais qui est entièrement encapsulée, on voit que les accesseurs (get[Attribut]
) et les mutateurs (set[Attribut]
) sont presque identiques. Ils suivent l’idiome pour ces types de méthodes :
Une méthode nommé “get” + le nom de l’attribut qui est sans paramètre et qui retourne la valeur de l’attribut.
1
2
3
public [type] get[Attribut](){
return [attribut];
}
comme
1
2
3
public int getX() {
return x;
}
Une méthode nommé “set” + le nom de l’attribut qui prend un paramètre du même type que l’attribut et qui assigne la valeur du paramètre à l’attribut.
1
2
3
public void set[Attribut]([type] [attribut]){
this.[attribut] = [attribut];
}
comme
1
2
3
public void setX(int x){
this.x = x;
}
En fait, le code pour les mutateurs et les accesseurs dans l’exemple de la classe Point
a été copié-collé-modifié manuellement, ce qui est une tâche répétitive qui ne nécessite pas beaucoup d’engagement intellectuel de la part du développeur et qui peut mener à des erreurs d’inattention, surtout quand il y a plusieurs attributs dans la classe.
La bonne nouvelle et que si les mutateurs et les accesseurs sont du genre répétitifs comme dans la classe Point
, les outils de votre EDI (VS Code, Eclipse, etc.) peuvent les générer automatiquement.
C’est quand même important de s’assurer que ces méthodes reflètent réellement le comportement attendu et de modifier ou de remplacer les méthodes au besoin. Par exemple, dans la classe
Square
il fallait définir un mutateur spécialisé pour respecter le concept d’un carré.
Dans VS Code :
private
.Ctrl + .
et consultez la section “More Actions”Dans un diagramme de classe UML, on indique la visibilité des membres avec des symboles spécifiques :
+
pour public
-
pour private
Voici les diagrammes de classe UML pour l’exemple Point
ci-dessus.
1
2
3
4
5
6
7
8
9
10
11
12
13
@startuml Point
!theme toy
class Point {
- x : int
- y : int
+ setX(int) : void
+ setY(int) : void
+ getX() : int
+ getY() : int
}
@enduml
Changez la visibilité des tous les attributs de votre objet et générez des accesseurs et mutateurs appropriés.
Testez ces nouvelles méthodes dans la classe pilote. Vérifiez que l’accès direct à chacun des attributs via la classe pilote produit une erreur.
Il y a aussi le mot-clé protected
qui s’applique lorsqu’on a des classes qui héritent explicitement le contenu d’une classe parent. On en discutera lors de la leçon sur l’héritage dans une prochaine unité sur l’orienté objet. ↩
Voir la référence suivante pour les détails exacts. ↩