UKOnline

Encapsulation et immuabilité

La section précédente nous a fait de découvrir les relations de composition et d'agrégation qui sont l'un des principes de base de la programmation orientée objets. Cette section détaille un second principe de base qui est celui d'encapsulation qui est une propriété des objets.

Encapsulation

Le principe d'encapsulation dit qu'un objet ne doit pas exposer sa représentation interne au monde extérieur. Les données stockées par l'objet doivent être cachées de l'utilisateur de l'objet, et toute interaction avec l'objet doit se faire via des méthodes. Prenons par exemple une classe Hour qui permet de représenter une heure :

On définit deux variables d'instance pour stocker l'heure et les minutes, déclarées public. On définit ensuite une classe Meeting qui représente une réunion, identifiée par un sujet, une heure de début et heure de fin. On peut connaitre la durée d'une réunion grâce à une méthode getDuration.

Pour calculer la durée de la réunion, on soustrait donc l'heure du début de l'heure de fin, après avoir ramené le tout en minutes. On suppose bien entendu que les réunions commencent et se terminent la même journée. Remarquez qu'on accède directement aux variables d'instances de la classe Hour, puisqu'elles sont publiques et qu'il n'y a pas d'accesseur.

On a ainsi créé une forte cohésion entre les classes Meeting et Hour; elles sont fortement liées. On ne peut plus changer l'implémentation de la classe Hour indépendamment de la classe Meeting. Imaginons par exemple qu'on change l'implémentation de la classe Hour en ne gardant plus qu'une seule variable d'instance pour stocker l'heure de la réunion sous forme de minutes :

Si on fait ce changement, la méthode getDuration de la classe Meeting ne fonctionnera plus. Ce qu'il faut faire pour rendre la classe Hour plus indépendante, c'est déclarer toutes les variables d'instance privées. Ensuite, il faut ajouter des méthodes qui permettent d'interagir avec l'objet. Ici, tout ce qu'on souhaite pouvoir faire avec un tel objet, c'est connaitre l'heure et les minutes; on ajoute donc une méthode getHour pour obtenir l'heure et une méthode getMinutes pour les minutes.

Maintenant, il suffit d'utiliser ces méthodes dans la classe Meeting :

La classe Meeting dépend maintenant beaucoup moins de l'implémentation de la classe Hour, on peut changer sa représentation interne, et donc également les deux méthodes getHour et getMinutes, sans que cela n'ait un quelconque impact sur le bon fonctionnement de la classe Meeting.

Selon le principe d'encapsulation, une classe apparait donc comme une boite noire. Depuis l'extérieur, on ne doit rien voir sur les détails internes de la classe; on ne voit que les constructeurs et méthodes publics. L'ensemble des méthodes publiques d'une classe est appelé interface de la classe. Tout utilisateur ne devrait interagir avec un objet que via les méthodes de son interface. La figure 23 illustre ce concept. La classe est représentée comme un boite noire dont les seules informations exposées au monde extérieur sont les deux méthodes publiques getHour et getMinutes.

Interface classe Hour
Vue d'une classe comme une boite noire exposant une interface.

Le principe d'encapsulation vise donc à bien séparer les fonctionnalités publiques offertes par un objet de leur implémentation. On sépare le « Quelles fonctionnalités sont disponibles ? » du « Comment ces fonctionnalités sont implémentées ? ». Pour pouvoir utiliser un objet, il est inutile de savoir comment l'objet gère, de manière interne, ses attributs; il suffit de connaitre les opérations disponibles et à quoi elles servent. On reviendra sur ce concept de boite noire et d'interface dans le chapitre suivant et au chapitre 8.

Immuabilité

On va maintenant s'intéresser à une propriété sur l'état des objets. Un objet est dit immuable (ou non-modifiable) si, une fois créé, son état ne changera plus jamais. Sinon, l'objet est dit mutable (ou modifiable).

Objet mutable

On a déjà vu un exemple d'objet mutable au chapitre 4 : les objets de type Date. En effet, une fois un objet Date créé, on peut changer son état grâce à des méthodes mutateurs comme setDate par exemple, qui permet de changer le jour de la date. L'exemple suivant illustre le caractère mutable des objets Date :

On crée donc un objet et on stocke une référence vers celui-ci dans la variable d. L'objet représente initialement le 19 juin 2009. On peut ensuite changer son état avec la méthode setDate et l'objet représente finalement le 30 juin 2009 comme le confirme ce qui est imprimé sur la sortie standard :

Fri Jun 19 00:00:00 CEST 2009
Tue Jun 30 00:00:00 CEST 2009

Objet immuable

Par contre, les objets de type String sont immuables. Une fois une chaine de caractères créée, il est impossible de la modifier. Il n'y a aucune méthode mutateur dans la classe String. Il y a bien une méthode replace qui permet de remplacer toutes les occurrences d'un caractère par un autre caractère, dont on pourrait croire qu'elle change l'état de l'objet.

En réalité, cette méthode renvoie une nouvelle chaine de caractères, comme l'illustre l'exemple suivant :

L'exécution de ce programme affiche ce qui suit sur la sortie standard :

White
White
While

On voit donc bien que l'appel vers la méthode replace ne permet pas de changer l'état de l'objet, mais que la nouvelle chaine de caractères modifiée est en fait renvoyée. Les classes wrappers ainsi que les classes BigInteger et BigDecimal définissent également des objets immuables.

Classes StringBuilder et StringBuffer

Il existe deux classes de la librairie standard Java qui permettent également de représenter des chaines de caractères, mais dont les instances sont mutables. Il s'agit des classes StringBuilder et StringBuffer qui se trouvent dans le package java.lang. Voici un exemple utilisant un objet StringBuilder :

L'exécution du programme affiche bien White suivi de While sur la sortie standard. D'autres méthodes mutateurs sont disponibles dans cette classe. Une qui est assez intéressante est la méthode append. Celle-ci permet d'ajouter une chaine de caractères au bout d'une autre. Il s'agit donc d'une concaténation, mais à la différence de l'opérateur de concaténation utilisé avec les String qui recrée un nouvel objet à chaque fois, la méthode append modifie l'état de l'objet. Le programme construit une chaine de caractères en utilisant la classe String et StringBuilder :

Il faut savoir qu'il est beaucoup plus rapide de construire une chaine de caractères avec des objets StringBuilder et la méthode append, qu'avec des objets String et l'opérateur de concaténation. Enfin, la classe StringBuffer fonctionne exactement de la même manière que la classe StringBuilder. La seule différence est que cette dernière ne peut pas être utilisée dans un programme multi-thread, alors que StringBuffer pourra l'être.

Définir un objet immuable

Pour définir un objet immuable, il faut faire en sorte que son état ne puisse jamais changer. Or, on sait que l'état des objets est représenté par les variables d'instance. Il faut donc que celles-ci ne soient jamais changées, ni par les méthodes de la classe, ni par du code se trouvant dans d'autres classes, une fois initialisées.

Il existe diverses possibilités pour définir un objet immuable. La première consiste à rendre les variables d'instance privées et d'ensuite s'assurer que leurs valeurs ne sont jamais changée dans les méthodes de la classe. Les instances de la classe Hour présentée ci-dessous seront immuables :

Pour indiquer que les valeurs des variables d'instance ne vont jamais changer, il suffit de les déclarer comme des constantes avec le mot réservé final vu au premier chapitre. Pour rappel, il est impossible de changer la valeur d'une variable final une fois celle-ci initialisée. Toutes les variables d'instance déclarées final doivent donc être initialisées, soit lors de leur déclaration, soit dans tous les constructeurs de la classe. Si jamais ce n'est pas le cas, le compilateur générera une erreur de type « variable XXX might not have been initialized ».

Déclarer toutes les variables d'instance final n'est pas suffisant pour définir des objets immuables. En effet, la seule chose garantie est que la valeur des variables ne sera pas changée. Mais, en ce qui concerne les variables d'instance de type objet, ce n'est pas parce qu'elles sont déclarées final que leur état ne pourra plus changer. Voyons cela avec l'exemple suivant :

Toutes les variables d'instances de cette classe sont des constantes et leur valeur ne peuvent donc jamais changer. Néanmoins, les instances de la classe Person sont mutables. En effet, la première instruction de la méthode getBirthday modifie l'état de la variable d'instance birthday. Si on retirait cette instruction, cela ne changerait rien, les instances de la classe seraient toujours mutables, à cause d'un effet de bord. On va voir d'où vient ce problème à la section suivante.

Effet de bord

Lorsqu'on définit un objet par composition ou agrégation, il faut faire très attention aux effets de bord qui peuvent survenir dans diverses situations. Ces effets de bord vont avoir comme conséquence que l'état de l'objet pourra être modifié.

On va repartir de la classe Person définie dans la section précédente. Revoici la définition de cette classe qui représente une personne identifiée par un nom et une date de naissance :

Supposons qu'une personne ne puisse pas changer de nom, ni de date de naissance, les instances de la classe Person devraient donc être immuables. Les deux variables d'instance sont de type objet et peuvent donc potentiellement poser problème. En ce qui concerne la première, rappelez-vous que les objets String sont immuables et la déclarer final est donc suffisant. Par contre, les objets Date sont mutables et des problèmes peuvent survenir. Voici un premier exemple de programme dans lequel on créé, puis modifie un objet Person :

La première instruction crée un objet Date représentant le 11 mai 1990 et stocke une référence vers cet objet dans la variable locale d. Ensuite, on crée un objet Person qui représente John, né le 11 mai 1990. On stocke une référence vers cet objet dans la variable p. On affiche ensuite la date de naissance de la personne en appelant la méthode getBirthday de la classe Person. On modifie ensuite l'objet référencé par la variable d et on réaffiche la date de naissance de la personne; celle-ci a changé !

Fri May 11 00:00:00 CEST 1990
Sat May 11 00:00:00 CET 1974

Un effet de bord peut donc se produire lorsqu'une méthode stocke dans une de ses variables d'instance une référence vers un objet mutable reçu en paramètre. La solution consiste à stocker une copie du paramètre reçu. On va donc modifier le constructeur afin d'effectuer une copie du paramètre mutable :

Mais cela ne résout pas tout, voyons l'autre cas dans lequel un effet de bord pourra se produire :

L'exécution de ce programme va également modifier l'état de l'objet référencé par la variable p. L'effet de bord a pu se produire car une méthodes de la classe renvoie une référence vers un des objets référencé par une des variables d'instance. La solution consiste à renvoyer une copie des objets référencés par une des variables d'instance :

Nous ne sommes pas en mesure de comprendre précisément pourquoi ces effets de bords se produisent maintenant. Il faudra attendre le chapitre suivant afin de comprendre cela. Ce qu'il est important de retenir à ce stade, c'est qu'il faut prendre les effets de bord en compte lorsqu'on souhaite définir des objets immuables.

Voici une condition suffisante pour définir des objets immuables :

  • déclarer final toutes les variables d'instance de type primitif ;
  • déclarer final toutes les variables d'instance référençant des objets immuables ;
  • les variables d'instance référençant des objets mutables doivent être déclarées final, aucune méthode mutateur ne peut être appelée sur ces objets, aucune méthode de la classe ne peut renvoyer une référence vers ces objets, et ces variables d'instance ne peuvent être initialisées directement à partir de paramètres des constructeurs.

Il s'agit donc bien de conditions suffisantes, c'est-à-dire que si elles sont toutes satisfaites pour une classe, vous avez la garantie que ses instances seront immuables, mais il est possible de définir des objets immuables sans pour autant que toutes ces conditions soient satisfaites.