Pourquoi je n’aime pas la portée privée

À quoi sert la portée privée

Toute personne ayant développé dans un langage de programmation orienté objet basé sur le C++ (Java, C#, etc.) connaît la portée privée. Cette portée d’attribut ou de méthode permet de s’assurer qu’aucune autre classe n’aura accès à l’attribut ou à la méthode en question. Cette portée sert principalement à protéger l’intégrité des valeurs d’un objet. Par exemple, si j’ai la classe Java suivante dans laquelle je mets l’entièreté de mes attributs publics:

public class Personne {

    public String nom;

    public int age;

}

Le client de ma classe peut donc assigner n’importe quelles valeurs aux attributs de ma classe. Par exemple, ce code Java serait valide:

Personne lPersonne = new Personne();
lPersonne.nom = "";
lPersonne.age = -8;

Le problème ici, c’est que malgré que le code est valide, les valeurs entrées ne font aucun sens. En effet, une personne ne peut pas avoir aucun nom et un age négatif. C’est la raison pour laquelle nous pouvons utiliser des accesseurs et des assignateurs (« getter » et « setter »). Je pourrais donc mettre mes attributs privés, les rendres accessibles par des accesseurs et valider les valeurs par des assignateurs. Voici l’exemple Java modifié:

public class Personne {

    private String nom;

    public String getNom() {
        return nom;
    }

    public void setNom(String aNom) {
        if (aNom != null && ! aNom.isEmpty()){
            nom = aNom;
        } else {
            throw new IllegalArgumentException("Le nom n'est pas valide.");
        }
    }

    private int age;

    public int getAge() {
        return age;
    }

    public void setAge(int aAge) {
        if (aAge >= 0){
            age = aAge;
        } else {
            throw new IllegalArgumentException("L'age n'est pas valide.");
        }
    }

}

On voit maintenant que le client de la classe ne peut plus accéder directement aux attributs puisque ces derniers sont privés. Le client doit maintenant utiliser les assignateurs (« setNom » et « setAge ») pour assigner des valeurs aux attributs. De plus ces assignateurs ont un mécanisme d’exceptions leur permettant de vérifier que les valeurs sont correctes avant d’effectuer l’assignation de l’attribut.

Le privé dans le contexte d’héritage

Les clients de la classe ne sont pas les seules entités pouvant assigner de mauvaises valeurs dans les attributs de l’objet. Cette problématique s’applique également aux descendants de la classe. Par exemple, si je faisais la class Bebe qui représente une Personne d’age entre 0 et 1. Il est possible dans ce cas que les parents n’aient pas encore trouvé de nom pour ce bébé. Je pourrais avoir le code Java suivant:

public class Bebe extends Personne {

    @Override	
    public void setNom(String aNom) {
        if (aNom != null){
            nom = aNom;
        } else {
            throw new IllegalArgumentException("Le nom n'est pas valide.");
        }
    }

    public void setAge(int aAge) {
        if (aAge >= 0 && aAge <= 1){
            age = aAge;
        } else {
            throw new IllegalArgumentException("L'age n'est pas valide.");
        }
    }

}

Il est important de voir que ce code ne compilera pas puisque les attributs nom et age sont privés et ne peuvent donc pas être assigné. On peut utiliser les assignateurs de la classe Personne pour faire le travail. Dans ce cas, j’aurais le code Java:

public class Bebe extends Personne {

    @Override	
    public void setNom(String aNom) {
        if (aNom != null){
            super.setNom(aNom);
        } else {
            throw new IllegalArgumentException("Le nom n'est pas valide.");
        }
    }

    @Override	
    public void setAge(int aAge) {
        if (aAge >= 0 && aAge <= 1){
            super.setAge(aAge);
        } else {
            throw new IllegalArgumentException("L'age n'est pas valide.");
        }
    }

}

Dans ce cas, le « setAge » fonctionnera sans problème puisque la particularité de Bebe (age entre 0 et 1) est conforme à la condition de personne (age plus grand ou égal à 0). Par contre, le « setNom » ne fonctionnera pas puisque le « setNom » de Bebe accepte les chaînes vide, mais le « setNom » de Personne les refuses.

Le problème

Maintenant que nous avons bien précisé à quoi sert la portée privée, je peux présenter le problème. Je veux préciser que tous les cas présentés plus haut sont tout à fait corrects et permettre de montrer que la portée privée permet de bien valider les valeurs assignées aux attributs de la classe.

Le problème est au niveau idéologique, mais peut avoir des répercussions au niveau fonctionnel.

Au niveau idéologique, c’est que la portée privée ne respecte pas le principe de programmation orientée objet. En effet, l’héritage dans le principe de programmation orientée objet est un lien de spécialisation/généralisation (l’enfant spécialise et le parent généralise). Par exemple, une Pomme est une version spécialisée d’un Fruit, qui lui est plus général. En d’autres mots, l’enfant représente une sous-catégorie du parent, avec des particularités supplémentaires. Donc, une Pomme est un Fruit, avec des particularités supplémentaires (forme, couleur, goût, etc.).

Le problème repose ici. L’enfant ne doit pas être considéré comme quelque chose de distinct du parent. L’enfant complète le parent. Il s’agit du même objet vu d’un angle différent (plus général ou plus spécifique). Il est donc complètement illogique qu’il existe des choses (au sens large) que le parent peut faire, mais que l’enfant ne peut pas faire (l’inverse n’est pas vrai puisque l’enfant spécialise le parent). En d’autres mots, puisque la Pomme est un Fruit, la Pomme devrait avoir accès à tout ce que le Fruit a accès.

Puisque la portée privée permet de rendre accessible certains attributs et méthodes au parent qui n’est pas accessible aux enfants et que dans le principe de programmation orientée objet, l’enfant devrait avoir accès à tout ce que le parent a accès, on en déduit que la portée privée n’est pas conforme au principe de programmation orientée objet.

Cette problématique peut également avoir un effet au niveau fonctionnel. En effet, il arrive souvent qu’on crée des attributs en lecture seul dans une classe. Afin de faire ce type d’attribut, on utilise un attribut privé qui ne contient qu’un accesseur ou « getter » (pas d’assignateur ou « setter »). Ce faisant, les classes descendantes se retrouvent à ne plus avoir de contrôle sur un de leurs propres attributs. Il est bien entendu possible de mettre un assignateur protégé, mais il arrive souvent que la classe que nous souhaitons hériter n’est pas une classe que nous pouvons modifier facilement (par exemple, classes de librairie) et il est donc complètement impossible d’y ajouter des assignateurs protégés.

À propos de la portée protégée

La portée protégée règle en effet le problème de non-conformité au principe de programmation orientée objet. En mettant toutes nos attributs et méthodes d’implémentation en portée protégée, nous nous assurons que les descendants de la classe ont accès à l’entièreté de leurs méthodes et attributs.

Par contre, un autre problème survient, en mettant tous en portée protégée, on se retrouve avec une classe qui ne peut plus protéger l’intégrité de ces valeurs. La seule solution consiste en fait en s’assurant de mettre tous les attributs privés et de mettre toutes les méthodes (incluant les accesseurs et les assignateurs) soit protégés ou publics. De plus, il faut s’assurer de toujours mettre des assignateurs et des accesseurs à tous les attributs, peu importe que ce soit des attributs en lecture ou en écriture seulement, ou bien des attributs/variables d’implémentation de la classe. De cette manière, malgré que le code ne respecte toujours pas le principe de programmation orientée objet (puisque les attributs ne sont pas directement accessibles par les descendants de la classe), on se retrouve tout de même avec quelque chose de relativement fiable.

Quels sont les alternatives?

Lors de l’écriture de son livre « Object-Oriented Software Construction », un des livres les plus influant du domaine de la programmation orientée objet, le chercher Bertrand Meyer a trouvé une solution très élégante à ce problème: les invariants de classe.

L’idée d’un invariant de classe, c’est d’établir dans la classe (ainsi que dans les classes descendantes), une liste de règles qui doivent toujours être respectées dans les objets qui instancient cette classe. En d’autres mots, il n’est pas de la responsabilité des assignateurs de la classe de valider les valeurs de la classe, mais bien à la classe elle-même. Puisque les invariants sont hérités et documentés, tous les ancêtres de la classe héritent de ces invariants et ont l’obligation de les respecter. Lorsqu’une méthode se termine, tous les invariants sont validés et si un de ces invariants n’est pas valide, une exception est lancée.

Voici un exemple d’invariant fait dans le langage Java en utilisant une librairie de contrat (Java ne gère pas les contrats par défaut):

@Contract(
           nom != null &&
           !nom.isEmpty() &&
           age >= 0)
public class Personne {

    protected String nom;

    public String getNom() {
        return nom;
    }

    public void setNom(String aNom) {
        if (aNom != null && ! aNom.isEmpty()){
            nom = aNom;
        } else {
            throw new IllegalArgumentException("Le nom n'est pas valide.");
        }
    }

    protected int age;

    public int getAge() {
        return age;
    }

    public void setAge(int aAge) {
        if (aAge >= 0){
            age = aAge;
        } else {
            throw new IllegalArgumentException("L'age n'est pas valide.");
        }
    }

}

Cette technique a l’avantage de protéger entièrement les attributs de la classe, et ce, même si les attributs sont directement assignables par les descendants de la classe. De plus, cette méthode respecte entièrement le principe objet puisque l’invariant s’applique autant à la classe Personne qu’à ses descendants. En d’autres mots, contrairement au mot clé privé qui enlève de l’information aux descendants de la classe, l’invariant qui permet la même protection des données, est une information qui est héritée (demeure donc accessible) dans les descendants.

Petite parenthèse par rapport à la portée protégée en Java

Les créateurs de langage ont souvent une certaine obsession de s’assurer de garder minimal le nombre de mots clés dans le langage. Il s’agit en effet d’un élément intéressant, car plus le langage est minimal, plus il sera facile de créer le compilateur ou l’interpréteur et plus il sera facile d’apprendre le langage. Par contre, certains mauvais « desing » de langage peut apparaître suite à cette préoccupation.

La portée protégée de Java est un excellent exemple de cela. Initialement, la portée protégée permettait de permettre à la classe en cours ainsi qu’à ces descendants d’accéder à des attributs ou des méthodes de la classe. Comme nous l’avons vu, cette portée est très intéressante puisqu’elle permet de respecter le principe de programmation orientée objet.

Par contre, il existe un autre type de porté, plus rare et dont l’utilisation est plus subtile, mais qui peut, dans certains cas, être très intéressant à avoir dans un langage. Il s’agit de la porté ami. Cette portée permet à certaines autres classes du système, en plus de la classe en cours et des descendants, d’avoir accès aux attributs ou méthodes. Il pourrait être intéressant par exemple pour la classe Tableau de gérer une autre classe Cellule, mais sans que d’autres clients puissent utiliser cette Cellule. Le Tableau serait donc considéré comme un ami de Cellule et en tant qu’ami, il aurait accès à certains attributs et certaines méthodes de Cellule qui ne sont pas publics.

De manière à pouvoir permettre ce type de portée, mais en réutilisant un type de portée qui existait déjà dans le langage, Java a ajouté comme règle à sa portée protégée indiquant qu’en plus de la classe en cours et de ses descendants, l’attribut ou la méthode protégée serait accessible à partir de toutes les classes du « package » en cours.

Cette mécanique cause plusieurs problèmes.

Premièrement, la portée protégée, qui je le rappelle est la seule manière en java d’avoir un attribut ou une méthode d’implémentation dans une classe tout en respectant le principe de programmation orientée objet, devient accessible par d’autres classes, ce qui contrevient au principe de masquage d’information de la programmation orientée objet. Il en revient qu’en Java, la seule manière de faire des classes qui respectent les principes de programmation orientée objet (autre que des classes ne contenant que des méthodes publiques), c’est de faire une classe par « Package », ce qui est loin d’être propre ou même pratique.

Ensuite, considérant la manière que la majorité des programmeurs écrivent du code, on voit qu’il est très rare qu’il y a une grande quantité de « package » Java. On obtient donc que le fait de faire des attributs ou des méthodes protégées revient généralement à faire des attributs ou des méthodes publics (ou presque). Ce fait peut causer de grande confusion puisque le compilateur laissera passer des manipulations qui clairement auraient dû être interdites.

Enfin, cette problématique fait que plusieurs programmeurs (particulièrement les programmeurs avec peu d’expérience) comprennent mal la portée protégée, ne l’utilise que très peu, ou inversement l’utilise toujours au lieu d’utiliser la portée publique.

Il aurait donc été plus acceptable de créer une mécanique de portée différente à la portée protégée pour gérer la portée amie en Java.