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