L'encapsulation¶
🤖 Rédaction assistée par IA.
Définition¶
L'encapsulation consiste à regrouper les données et les traitements qui les manipulent au sein d'une classe et à masquer les détails de l'implémentation derrière une interface publique. Elle s'appuie sur :
- La visibilité des membres (
public,protected,private) - Les accesseurs (
getX(),setX(...),addX(...),removeX(...)...) qui permettent un accès indirect aux données
Cas d'école¶
Avec une classe Person dont les attributs sont publics, nous pouvons nous demander à quoi bon définir des attributs privés :
classDiagram
class Person {
+firstname: string
+lastname: string
+age: int
+country: string
+city: string
}
Après tout, il est plus simple d'écrire person.age plutôt que person.getAge().
Pour comprendre l'intérêt de l'encapsulation, imaginons que nous stockons des personnes ainsi modélisées... et que nous laissons passer un an. Les âges stockés sont tous faux !
Nous souhaitons alors changer la modélisation pour stocker la date de naissance (invariant temporel) et calculer l'âge à partir de celle-ci :
classDiagram
class Person {
+firstname: string
+lastname: string
-birthDate: Date
+country: string
+city: string
+getAge() int
}
L'attribut age étant public, tout le code client utilisant person.age (ex : if (person.age >= 18)) devra être modifié : nous sommes face à un changement cassant.
Solution¶
Permettre l'accès aux données uniquement via une indirection (ex : person.getAge()), c'est se donner la possibilité de modifier une classe sans casser le code client.
Si Person avait exposé une méthode getAge() dès le départ, il suffirait de modifier son implémentation : le code client person.getAge() >= 18 fonctionnerait avant et après le changement de modélisation.
Par exemple :
class Person {
constructor(
private firstname: string,
private lastname: string,
private birthDate: Date,
) {}
getFirstname(): string {
return this.firstname;
}
getLastname(): string {
return this.lastname;
}
getAge(): number {
const today = new Date();
let age = today.getFullYear() - this.birthDate.getFullYear();
const beforeBirthday =
today.getMonth() < this.birthDate.getMonth() ||
(today.getMonth() === this.birthDate.getMonth() && today.getDate() < this.birthDate.getDate());
if (beforeBirthday) {
age--;
}
return age;
}
}
import java.time.LocalDate;
import java.time.Period;
public class Person {
private String firstname;
private String lastname;
private LocalDate birthDate;
public Person(String firstname, String lastname, LocalDate birthDate) {
this.firstname = firstname;
this.lastname = lastname;
this.birthDate = birthDate;
}
public String getFirstname() {
return firstname;
}
public String getLastname() {
return lastname;
}
public int getAge() {
return Period.between(birthDate, LocalDate.now()).getYears();
}
}
#include <chrono>
#include <string>
#include <utility>
class Person {
public:
Person(std::string firstname, std::string lastname, std::chrono::year_month_day birthDate)
: _firstname(std::move(firstname)), _lastname(std::move(lastname)), _birthDate(birthDate) {}
const std::string& getFirstname() const { return _firstname; }
const std::string& getLastname() const { return _lastname; }
int getAge() const {
using namespace std::chrono;
const year_month_day today{floor<days>(system_clock::now())};
int age = int(today.year()) - int(_birthDate.year());
if (today.month() < _birthDate.month() ||
(today.month() == _birthDate.month() && today.day() < _birthDate.day())) {
--age;
}
return age;
}
private:
std::string _firstname;
std::string _lastname;
std::chrono::year_month_day _birthDate;
};
<?php
class Person
{
public function __construct(
private string $firstname,
private string $lastname,
private DateTimeImmutable $birthDate,
) {
}
public function getFirstname(): string
{
return $this->firstname;
}
public function getLastname(): string
{
return $this->lastname;
}
public function getAge(): int
{
return $this->birthDate->diff(new DateTimeImmutable())->y;
}
}
Variantes¶
Visibilité¶
Les mécanismes de visibilité varient d'un langage à l'autre :
| Langage | Visibilité disponible | Par défaut | Particularités |
|---|---|---|---|
| TypeScript | public, protected, private |
public |
private est vérifié uniquement à la compilation (contrairement à #attribut en JavaScript) |
| Java | public, protected, private |
package-private | protected donne aussi accès au package |
| Python | Aucune (conventions) | public | _attribut signale un membre interne, __attribut déclenche un name mangling |
| C++ | public, protected, private |
private (class), public (struct) |
friend permet d'ouvrir l'accès à une classe ou fonction |
| PHP | public, protected, private |
public |
readonly (PHP >= 8.1) pour un attribut non modifiable après initialisation |
Propriétés¶
Certains langages permettent d'exécuter du code lors de l'accès à un attribut : un attribut public peut alors être transformé en propriété calculée sans modifier le code client :
get/seten JavaScript et TypeScript (ex :get age(): number { ... })@propertyen Python- Les propriétés en C#
- Les property hooks en PHP >= 8.4
Avec ces langages, il est donc possible de se passer de getters introduits par précaution (sous réserve de disposer du mécanisme, ex : PHP >= 8.4 pour les property hooks) : un attribut public pourra être transformé en propriété le jour où le besoin se présentera.
Par exemple, en Python, la première version de Person pourra se contenter d'un attribut public :
class Person:
def __init__(self, firstname: str, lastname: str, age: int):
self.firstname = firstname
self.lastname = lastname
self.age = age
Lors du passage à la date de naissance, le décorateur @property permettra de calculer age sans modifier le code client (person.age >= 18) :
from datetime import date
class Person:
def __init__(self, firstname: str, lastname: str, birth_date: date):
self.firstname = firstname
self.lastname = lastname
self._birth_date = birth_date
@property
def age(self) -> int:
today = date.today()
before_birthday = (today.month, today.day) < (self._birth_date.month, self._birth_date.day)
return today.year - self._birth_date.year - int(before_birthday)
Encapsulation au niveau du module¶
L'encapsulation peut prendre d'autres formes que la visibilité des membres d'une classe. Par exemple, en TypeScript, une fonction ou une classe non exportée (export) n'est pas accessible en dehors de son module.
Mises en garde¶
- Il ne suffit pas d'ajouter mécaniquement des getters et des setters sur tous les attributs : il est plutôt question d'exposer uniquement ce qui doit l'être. En limitant la surface d'exposition, nous limitons le risque de changement cassant.
- Il reste un point discutable dans la modélisation de la classe
Person... Vous l'avez ?