ICS4U

Accueil > Classes et objets >

📚 Masquer les détails internes

Survol et attentes

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.

Définitions
Masquage de l’information
Selon certaines définitions de l’encapsulation, l’élément clé n’est pas nécessairement l’emballage des données avec les méthodes associées mais le fait de cacher les détails d’implémentation aux éléments externes à la classe. Cela permet de changer l’implémentation (le code dans la classe) sans changer l’interface (les appels de méthodes de l’extérieure de la classe). Un effet important de l’encapsulation est que les attributs ne sont pas accessibles directement de l’extérieur de la classe.
Mot-clés de visibilité
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.
Accesseurs et mutateurs
Méthodes 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.

Objectifs d’apprentissage

À la fin de cette leçon vous devrez être en mesure de :

Critères de succès

Problèmes potentiels sans encapsulation (masquage) des données

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.

Encapsulation pour protéger les données

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 classes
  • private 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.

Attributs privés -> méthodes accesseurs et méthodes mutateurs

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 :

  • accesseurs (“getters” en anglais) pour retourner la valeur d’un attribut privé;

    Les accesseurs sont du même type que la donnée demandée et ne prennent aucun paramètre.

  • mutateurs (“setters” en anglais) pour modifier la valeur d’un attribut privé.

    Les mutateurs sont de type void et prennent un paramètre du même type que la donnée à modifier.

Modèle de carré préservé dans la classe Square avec l’encapsulation

Voici 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é.

Accesseurs et mutateurs idiomatiques / formulaïques

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 :

Idiome pour les accesseurs

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;
}

Idiome pour les mutateurs

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;
}

Générer les accesseurs et les mutateurs avec les outils de l’EDI

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 :

  1. Déclarez chacun des attributs avec le mot-clé private.
  2. Cliquez sur le nom de la classe dans sa signature.
  3. Ici vous avez deux choix :
    1. Faites un clic droit sur le nom et cliquez ensuite sur l’option “Source Action…”
    2. Faites la combinaison Ctrl + . et consultez la section “More Actions”
  4. Selon votre jugement, vous avez les options suivantes :
    1. “Generate Getters and Setters…” qui produit les deux méthodes pour les attributs que vous sélectionnez par la suite
    2. “Generate Getters…” qui produit seulement les accesseurs pour les attributs que vous sélectionnez par la suite
    3. “Generate Setters…” qui produit seulement les mutateurs pour les attributs que vous sélectionnez par la suite
  5. Une version standard de ces méthodes sera générée, mais il faut :
    1. Les déplacer afin de garder les déclarations d’attributs ensemble au début de la classe et les définitions de méthodes plus bas
    2. Modifier le code interne des méthodes pour produire le comportement voulue, au besoin.
    3. Probablement reformatter le code (clic droit -> “Format Document”) afin de corriger les indentations brisées suite aux déplacements de code.

Indiquer la visibilité des membres dans un diagramme de classe UML

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

Point

Exercices

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.

  1. 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. 

  2. Voir la référence suivante pour les détails exacts.