Encapsulation dans Dart
Encapsulation dans Dart
Objectifs du cours
- Dans cet article, nous allons vous expliquer en profondeur ce que c’est que l’encapsulation, à quoi il sert et pourquoi il est si important dans le paradigme orienté objet.
-
Qu’est-ce que l’encapsulation dans Dart
- Imaginez que vous possédez un coffre au trésor magique auquel vous seul pouvez accéder. Personne d’autre ne sait l’ouvrir, et ils ne peuvent pas jouer avec votre précieux butin.
- Dans la programmation Dart, c’est ce que fait l’encapsulation ! Il nous aide à masquer nos précieuses données dans une boîte secrète (classe alias) et nous permet de contrôler qui peut le toucher et comment.
- L’encapsulation est un concept très crucial de la POO. Cela aide à protéger les données. De plus, le code encapsulé semble plus propre et flexible et peut être modifié selon les besoins.
- Dart ne contient pas de mots clés pour restreindre l’accès, comme le public, protégé ou privé utilisé en Java. L’encapsulation se produit au niveau de la bibliothèque et non au niveau de la classe.
- Il existe une règle simple : tout identifiant (classe, membre de classe, fonction de niveau supérieur ou variable) commençant par un trait de soulignement _ est privé de sa bibliothèque.
-
Comprendre le concept d’encapsulation dans Dart
- L’encapsulation est l’un des piliers de la programmation orientée objet (POO). En Dart, elle consiste à regrouper les données (attributs) et les méthodes qui les manipulent au sein d’une même unité (la classe), tout en restreignant l’accès direct à certains composants pour protéger l’intégrité de l’objet.
- Dans Dart, l’encapsulation signifie cacher les données dans une bibliothèque, les empêchant ainsi d’accéder à des facteurs extérieurs. Cela vous aide à contrôler votre programme et à éviter qu’il ne devienne trop compliqué.
- L’idée est simple : l’utilisateur de votre classe ne doit pas savoir comment l’objet stocke ses données, il doit seulement savoir quelles actions il peut effectuer. Cela évite qu’un code externe ne vienne corrompre l’état interne de votre objet.
- La protection via l’underscore (_)
- Comme vous l’avez vu, Dart n’utilise pas de mots-clés comme private. Il utilise la portée au niveau de la bibliothèque (le fichier).
- Sans encapsulation : N’importe qui peut mettre un solde négatif ou incohérent.
- Avec encapsulation : Vous forcez le passage par une méthode qui vérifie la validité de l’action.
- Les outils de l’encapsulation : Getters et Setters
- Pour offrir un accès contrôlé aux membres privés, Dart propose les mots-clés get et set. Cela permet de transformer une simple variable en une « propriété » calculée ou filtrée.
- Getter : Permet de lire une valeur sans pouvoir la modifier.
- Setter : Permet de modifier une valeur avec une logique de contrôle.
- Deux classes définies dans le même fichier peuvent accéder aux membres _ l’une de l’autre. Ce n’est pas une visibilité stricte « classe ».
-
Immuabilité dans Dart
- L’immuabilité et la protection contre les erreurs de valeur nulle (Null Safety) sont les deux piliers du développement moderne avec Dart. Ces concepts permettent de créer des applications plus robustes, prévisibles et performantes.
- Dart encourage l’encapsulation robuste via :
final: champ assignable une seule fois (idéalement au constructeur)- Un champ final ne peut être assigné qu’une seule fois, généralement dans le constructeur (via initializing formal this.field ou dans l’initializer list).
- Cela empêche toute modification accidentelle de l’état interne après la création de l’objet.
- Impact sur l’encapsulation : les champs internes restent immuables de l’extérieur (sauf si l’objet pointé est mutable). On expose seulement ce qui est nécessaire via des getters (souvent implicites avec final).
const: immuabilité à la compilation (constructeur const)required et null safety (?, !): évitent les états partiellement initialisés-
La Null Safety (Sécurité du vide)
- Introduite avec Dart 2.12, la Sound Null Safety garantit qu'une variable ne peut pas être null à moins que vous ne l'ayez explicitement autorisé. Cela élimine la célèbre erreur
- NoSuchMethodError: The method '...' was called on null.
Les types non-nullables (par défaut)Les types nullables (?)- Pour autoriser une valeur nulle, on ajoute un point d'interrogation.
-
Les opérateurs clés
Assertion non-nulle (!): Vous dites au compilateur "Je suis sûr que ce n'est pas nul". À utiliser avec prudence.-
L'Immuabilité
- L'immuabilité consiste à empêcher la modification d'un objet après sa création.
-
Comment réaliser l'encapsulation en Dart
- Déclaration des attributs privés : Utilisez le préfixe
_pour déclarer les attributs comme privés à l'intérieur de la classe. - Définition des méthodes d'accès (getters et setters) : Utilisez des méthodes pour accéder et modifier les valeurs des attributs privés. Cela permet un contrôle sur l'accès aux données et peut implémenter une logique supplémentaire si nécessaire.
- Utilisation de méthodes pour les opérations : Plutôt que de manipuler directement les attributs privés depuis l'extérieur de la classe, utilisez des méthodes publiques pour effectuer des opérations sur ces attributs.
- Dans cet exemple, les attributs _firstName et _lastName sont privés et ne peuvent être accédés directement en dehors de la classe Person. Au lieu de cela, des getters et des setters sont utilisés pour accéder à ces attributs.
- La méthode getFullName() est utilisée pour obtenir le nom complet de la personne. Cela garantit que les données sont encapsulées et que l'accès est contrôlé à l'aide de méthodes.
-
Exemples
- Dans cet exemple, nous allons créer une classe nommée Employee . La classe aura deux propriétés privées _id et _name .
- Nous allons également créer deux méthodes publiques getId() et getName() pour accéder aux propriétés privées. Nous allons également créer deux méthodes publiques setId() et setName() pour mettre à jour les propriétés privées.
- Dans cet exemple, nous allons créer une classe nommée Employee . La classe a une propriété privée _name . Nous allons également créer une méthode publique getName() pour accéder à la propriété privée.

Un exemple simple en Dart illustrant l’encapsulation :
class CompteBancaire {
// Champs privés
String _titulaire;
double _solde;
// Constructeur
CompteBancaire({required String titulaire, double solde = 0.0})
: _titulaire = titulaire,
_solde = solde;
// Getters (lecture seule)
String get titulaire => _titulaire;
double get solde => _solde;
// Setter avec validation
set titulaire(String value) {
if (value.trim().isEmpty) throw ArgumentError('Le titulaire doit avoir un nom');
_titulaire = value;
}
// Méthodes métier (préférables aux setters directs)
void deposer(double montant) {
if (montant > 0) _solde += montant;
}
void retirer(double montant) {
if (montant > 0 && montant <= _solde) {
_solde -= montant;
} else {
throw StateError('Solde insuffisant ou montant invalide');
}
}
@override
String toString() => 'Compte($_titulaire, solde: $_solde €)';
}
Utilisation :
void main() {
final compte = CompteBancaire(titulaire: 'Alice', solde: 100);
compte.deposer(50);
// compte._solde = 999; // ❌ Erreur : membre privé
print(compte.solde); // ✅ 150.0
}
/// Représente une somme d'argent avec sa devise.
/// Cette classe est entièrement immuable et thread-safe.
class Money {
// Tous les champs sont final → immuables après construction
final double amount;
final String currency;
// Constructeur constant (quand possible)
const Money(this.amount, this.currency);
// Constructeur nommé avec validation + required
factory Money.euros(double amount) {
return Money._validated(amount, 'EUR');
}
factory Money.dollars(double amount) {
return Money._validated(amount, 'USD');
}
// Constructeur privé avec validation forte
const Money._validated(this.amount, this.currency)
: assert(amount >= 0, 'Le montant ne peut pas être négatif'),
assert(currency.isNotEmpty, 'La devise est obligatoire'),
assert(_isValidCurrency(currency), 'Devise non supportée');
// Méthode statique privée pour validation
static bool _isValidCurrency(String currency) {
const validCurrencies = {'EUR', 'USD', 'GBP', 'JPY'};
return validCurrencies.contains(currency.toUpperCase());
}
// ──────────────────────────────────────────────
// Opérations qui retournent un NOUVEL objet (immuabilité)
// ──────────────────────────────────────────────
Money add(Money other) {
if (currency != other.currency) {
throw ArgumentError('Impossible d\'additionner des devises différentes');
}
return Money(amount + other.amount, currency);
}
Money subtract(Money other) {
if (currency != other.currency) {
throw ArgumentError('Impossible de soustraire des devises différentes');
}
return Money(amount - other.amount, currency);
}
// Getters calculés (pas de setters !)
bool get isZero => amount == 0;
bool get isPositive => amount > 0;
String get formatted => '$amount $currency';
// Égalité et hashCode basés sur les valeurs (important pour les value objects)
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Money &&
amount == other.amount &&
currency == other.currency;
@override
int get hashCode => Object.hash(amount, currency);
@override
String toString() => 'Money($amount $currency)';
}
void main() {
// 1. Utilisation avec const (immuabilité compile-time)
const budget = Money(1500.0, 'EUR');
const bonus = Money(200.0, 'EUR');
// 2. Opérations → toujours un nouvel objet
final total = budget.add(bonus);
print(total); // Money(1700.0 EUR)
// 3. Factory avec validation
final salary = Money.euros(3200.50);
print(salary.formatted); // 3200.5 EUR
// 4. Tentative d'utilisation invalide (détectée à la compilation ou à l'exécution)
// Money(-100, 'EUR'); // AssertionError grâce à assert()
// 5. Impossibilité de modifier l'objet
// budget.amount = 2000; // Erreur de compilation (final)
// total.currency = 'USD'; // Erreur de compilation (final)
}
Exemple
class Utilisateur {
final String _email; // Ne changera jamais
String _pseudo; // Modifiable via setter
Utilisateur({required String email, String? pseudo})
: _email = email.toLowerCase(),
_pseudo = pseudo ?? 'Anonyme';
String get email => _email;
String get pseudo => _pseudo;
set pseudo(String value) {
if (value.length < 3) throw ArgumentError('Pseudo trop court');
_pseudo = value;
}
}
String name = "Alice"; // name = null; // Erreur de compilation !
String? profession; // Peut être une String ou null
print(profession!.length);
Appel conditionnel (?.) : Accède à la propriété uniquement si l'objet n'est pas nul.
print(profession?.length); // Affiche la longueur ou "null"
Opérateur de coalescence nulle (??) : Fournit une valeur par défaut.
String job = profession ?? "Sans emploi";
En Dart, on utilise principalement trois mots-clés : final, const, et late.final vs constCaractéristiquefinalconstInitialisationUne seule fois, à l'exécution.À la compilation uniquement.UtilisationPour des valeurs dont on ignore le contenu avant que le code tourne (ex: API).Pour des valeurs fixes connues à l'avance (ex: Pi, couleurs).MémoireNouvel objet créé à chaque fois.Canonique (un seul exemplaire en mémoire).Exemple :Dartfinal DateTime now = DateTime.now(); // Ok : déterminé à l'exécution
// const DateTime now = DateTime.now(); // Erreur : pas une constante à la compilation
Le mot-clé lateIl permet de déclarer une variable non-nullable dont l'initialisation est différée. Dart garantit qu'elle sera initialisée avant d'être lue.Dartlate final String description;
void init() {
description = "Initialisé plus tard";
}
3. Classes ImmuablesPour rendre une classe immuable, on combine final et un constructeur const. C'est une pratique essentielle dans Flutter pour optimiser le rendu des widgets.Dartclass User {
final String name;
final int age;
// Constructeur constant
const User({required this.name, required this.age});
}
void main() {
const user1 = User(name: "Bob", age: 30);
const user2 = User(name: "Bob", age: 30);
print(identical(user1, user2)); // true (même espace mémoire !)
}
Pourquoi est-ce important ?Réduction des bugs : La majorité des plantages applicatifs sont liés aux références nulles.Performance : Les objets const sont mis en cache par la machine virtuelle Dart, ce qui réduit la pression sur le Garbage Collector.Maintenance : Un code immuable est plus facile à tester et à comprendre, car l'état de l'objet ne change pas de manière imprévue au cours du cycle de vie du programme.
Exemple01
class Person {
// Attributs privés
String _firstName;
String _lastName;
// Constructeur
Person(this._firstName, this._lastName);
// Getters et setters pour _firstName
String get firstName => _firstName;
set firstName(String value) => _firstName = value;
// Getters et setters pour _lastName
String get lastName => _lastName;
set lastName(String value) => _lastName = value;
// Méthode pour afficher le nom complet
String getFullName() {
return '$_firstName $_lastName';
}
}
void main() {
// Création d'un objet Person
var person = Person('John', 'Doe');
// Utilisation des getters et setters
print('Prénom : ${person.firstName}');
print('Nom : ${person.lastName}');
// Modification des attributs privés à l'aide des setters
person.firstName = 'Alice';
person.lastName = 'Smith';
// Affichage du nom complet en utilisant une méthode
print('Nom complet : ${person.getFullName()}');
}
Exemple01
class Employee {
// Propriétés privées
int? _id;
String? _name;
// Méthode getter pour accéder à la propriété privée _id
int getId() {
return _id ?? 0; // Retourne 0 ou fournit une valeur par défaut si _id est null
}
// Méthode getter pour accéder à la propriété privée _name
String getName() {
return _name ?? ''; // Retourne une chaîne vide ou fournit une valeur par défaut si _name est null
}
// Méthode setter pour mettre à jour la propriété privée _id
void setId(int id) {
_id = id;
}
// Méthode setter pour mettre à jour la propriété privée _name
void setName(String name) {
_name = name;
}
}
void main() {
// Créer un objet de la classe Employee
Employee emp = Employee();
// Définir des valeurs à l'objet en utilisant le setter
emp.setId(1);
emp.setName("John");
// Récupérer les valeurs de l'objet en utilisant le getter
print("Id: ${emp.getId()}");
print("Nom: ${emp.getName()}");
}
Exemple 2 : Propriétés privées dans Dart
class Employee {
// Propriété privée
var _name;
// Méthode getter pour accéder à la propriété privée _name
String getName() {
return _name;
}
// Méthode setter pour mettre à jour la propriété privée _name
void setName(String name) {
this._name = name;
}
}
void main() {
// Créer une instance de la classe Employee
var employee = Employee();
// Définir une valeur à la propriété en utilisant le setter
employee.setName("Jack");
// Récupérer la valeur de la propriété en utilisant le getter
print(employee.getName());
}
