Aujourd'hui, on va explorer en profondeur la programmation orientée objet (POO) en Dart. Si tu viens de langages comme JavaScript, Python ou Java, tu vas retrouver des concepts familiers, mais Dart apporte aussi ses propres spécificités qui rendent la POO encore plus puissante.
La POO, c'est pas juste un buzzword - c'est une façon de structurer ton code qui le rend plus organisé, réutilisable et facile à maintenir. Dans Flutter, tout est basé sur des classes (les widgets sont des classes !), donc maîtriser la POO en Dart est essentiel.
Dans cet article, je vais t'expliquer tous les concepts de la POO en Dart, des bases aux fonctionnalités avancées comme les mixins et les factory constructors. À la fin, tu sauras créer des architectures orientées objet robustes et élégantes.
Prêt à devenir un pro de la POO ? Let's go ! 🚀
Pourquoi la POO ?
Avant de plonger dans le code, comprenons pourquoi la programmation orientée objet est si importante.
Le problème du code procédural
Imagine que tu codes une application de gestion de bibliothèque de façon procédurale :
// Données éparpillées
String titreLivre1 = 'Clean Code';
String auteurLivre1 = 'Robert C. Martin';
int pagesLivre1 = 464;
bool disponibleLivre1 = true;
String titreLivre2 = 'Dart Apprentice';
String auteurLivre2 = 'Jonathan Sande';
int pagesLivre2 = 500;
bool disponibleLivre2 = false;
// Fonctions séparées
void afficherLivre(String titre, String auteur, int pages) {
print('$titre par $auteur ($pages pages)');
}
bool emprunterLivre(bool disponible) {
if (disponible) {
return false; // Plus disponible
}
return disponible;
}
C'est le chaos ! Les données sont éparpillées, il n'y a pas de structure claire, et dès que tu as plus de 2 livres, c'est ingérable.
La solution orientée objet
Avec la POO, tu encapsules les données et le comportement dans une classe :
class Livre {
String titre;
String auteur;
int pages;
bool disponible;
Livre({
required this.titre,
required this.auteur,
required this.pages,
this.disponible = true,
});
void afficher() {
print('$titre par $auteur ($pages pages)');
}
bool emprunter() {
if (!disponible) {
print('Livre déjà emprunté');
return false;
}
disponible = false;
print('Livre emprunté avec succès');
return true;
}
void retourner() {
disponible = true;
print('Livre retourné');
}
}
// Utilisation claire et organisée
void main() {
var livre1 = Livre(
titre: 'Clean Code',
auteur: 'Robert C. Martin',
pages: 464,
);
livre1.afficher();
livre1.emprunter();
}
Beaucoup mieux ! Les données et les méthodes sont groupées logiquement, et chaque livre est une entité autonome.
Les quatre piliers de la POO
La programmation orientée objet repose sur quatre concepts fondamentaux :
-
Encapsulation : Regrouper les données et méthodes, cacher les détails internes
-
Héritage : Créer de nouvelles classes basées sur des classes existantes
-
Polymorphisme : Un même nom peut avoir différentes implémentations
-
Abstraction : Définir des interfaces sans implémenter les détails
On va explorer chacun de ces concepts en détail.
Les classes : bases et constructeurs
Une classe est un plan (blueprint) pour créer des objets. C'est comme un moule à gâteau : tu définis la forme une fois, et tu peux créer autant de gâteaux (objets) que tu veux.
Anatomie d'une classe
class Personne {
// Propriétés (attributs)
String nom;
int age;
String? email; // Nullable
// Constructeur
Personne({
required this.nom,
required this.age,
this.email,
});
// Méthodes (comportements)
void sePresenter() {
print('Je suis $nom, j\'ai $age ans');
}
void vieillir() {
age++;
print('Joyeux anniversaire ! J\'ai maintenant $age ans');
}
// Getter
bool get estMajeur => age >= 18;
// Setter
set nouveauEmail(String email) {
if (email.contains('@')) {
this.email = email;
} else {
print('Email invalide');
}
}
}
Créer des instances (objets)
void main() {
// Créer un objet
var alice = Personne(
nom: 'Alice',
age: 25,
email: 'alice@example.com',
);
// Utiliser les méthodes
alice.sePresenter(); // Je suis Alice, j'ai 25 ans
alice.vieillir(); // Joyeux anniversaire ! J'ai maintenant 26 ans
// Accéder aux propriétés
print(alice.nom); // Alice
print(alice.age); // 26
// Utiliser les getters/setters
print(alice.estMajeur); // true
alice.nouveauEmail = 'alice.new@example.com';
}
Constructeurs multiples
Dart permet plusieurs constructeurs pour créer des objets de différentes façons.
Constructeur nommé
class Point {
double x;
double y;
// Constructeur principal
Point(this.x, this.y);
// Constructeur nommé : créer à l'origine
Point.origine() : x = 0, y = 0;
// Constructeur nommé : créer depuis coordonnées polaires
Point.polaire(double rayon, double angle)
: x = rayon * cos(angle),
y = rayon * sin(angle);
void afficher() {
print('Point($x, $y)');
}
}
void main() {
var p1 = Point(3, 4);
var p2 = Point.origine();
var p3 = Point.polaire(5, pi / 4);
p1.afficher(); // Point(3.0, 4.0)
p2.afficher(); // Point(0.0, 0.0)
p3.afficher(); // Point(3.54, 3.54)
}
Les constructeurs nommés sont super pratiques pour créer des objets de différentes façons avec un nom qui explique l'intention.
Factory constructor
Un factory constructor peut retourner une instance existante ou créer une sous-classe. C'est très puissant !
class Logger {
final String nom;
// Cache des instances
static final Map<String, Logger> _cache = {};
// Constructeur privé (remarque le _)
Logger._internal(this.nom);
// Factory constructor : retourne une instance cachée si elle existe
factory Logger(String nom) {
return _cache.putIfAbsent(nom, () => Logger._internal(nom));
}
void log(String message) {
print('[$nom] $message');
}
}
void main() {
var logger1 = Logger('App');
var logger2 = Logger('App');
// logger1 et logger2 sont la MÊME instance !
print(identical(logger1, logger2)); // true
logger1.log('Message 1'); // [App] Message 1
logger2.log('Message 2'); // [App] Message 2
}
Ce pattern (Singleton) garantit qu'il n'y a qu'une seule instance de Logger pour chaque nom.
Constructeur avec initializer list
L'initializer list permet d'initialiser des propriétés avant que le corps du constructeur s'exécute.
class Rectangle {
final double largeur;
final double hauteur;
final double aire;
// Calculer l'aire dans l'initializer list
Rectangle(this.largeur, this.hauteur)
: aire = largeur * hauteur {
// Corps du constructeur (optionnel)
print('Rectangle créé : ${largeur}x$hauteur');
}
// Autre exemple : validation
Rectangle.carre(double cote)
: assert(cote > 0, 'Le côté doit être positif'),
largeur = cote,
hauteur = cote,
aire = cote * cote;
}
Propriétés privées
En Dart, un underscore _ rend une propriété ou méthode privée (accessible seulement dans le même fichier).
class CompteBancaire {
String titulaire;
double _solde; // Privé
CompteBancaire(this.titulaire, this._solde);
// Getter public pour accéder au solde
double get solde => _solde;
// Méthode publique pour déposer
void deposer(double montant) {
if (montant > 0) {
_solde += montant;
print('Dépôt de $montant€. Nouveau solde: $_solde€');
}
}
// Méthode privée
bool _verifierSolvabilite(double montant) {
return _solde >= montant;
}
// Méthode publique qui utilise la méthode privée
bool retirer(double montant) {
if (_verifierSolvabilite(montant)) {
_solde -= montant;
print('Retrait de $montant€. Nouveau solde: $_solde€');
return true;
} else {
print('Solde insuffisant');
return false;
}
}
}
void main() {
var compte = CompteBancaire('Alice', 1000);
// OK
print(compte.solde); // 1000.0
compte.deposer(500);
// ERREUR si dans un autre fichier :
// print(compte._solde);
// compte._verifierSolvabilite(100);
}
L'encapsulation avec des propriétés privées protège l'intégrité des données. L'utilisateur ne peut pas modifier directement _solde, il doit passer par les méthodes deposer() et retirer() qui appliquent les règles métier.
Héritage : réutiliser et étendre
L'héritage permet de créer une nouvelle classe basée sur une classe existante. La nouvelle classe (enfant/sous-classe) hérite des propriétés et méthodes de la classe parente (super-classe).
Héritage simple
// Classe parente
class Animal {
String nom;
int age;
Animal(this.nom, this.age);
void manger() {
print('$nom mange');
}
void dormir() {
print('$nom dort');
}
void faireDuBruit() {
print('$nom fait du bruit');
}
}
// Classe enfant : hérite de Animal
class Chien extends Animal {
String race;
// Appeler le constructeur parent avec super
Chien(String nom, int age, this.race) : super(nom, age);
// Override : redéfinir une méthode du parent
@override
void faireDuBruit() {
print('$nom aboie: Wouf wouf!');
}
// Nouvelle méthode spécifique au Chien
void rapporter() {
print('$nom rapporte la balle');
}
}
class Chat extends Animal {
bool interieur;
Chat(String nom, int age, this.interieur) : super(nom, age);
@override
void faireDuBruit() {
print('$nom miaule: Miaou!');
}
void griffer() {
print('$nom fait ses griffes');
}
}
void main() {
var chien = Chien('Rex', 3, 'Labrador');
var chat = Chat('Minou', 2, true);
// Méthodes héritées
chien.manger(); // Rex mange
chat.dormir(); // Minou dort
// Méthodes overridées
chien.faireDuBruit(); // Rex aboie: Wouf wouf!
chat.faireDuBruit(); // Minou miaule: Miaou!
// Méthodes spécifiques
chien.rapporter(); // Rex rapporte la balle
chat.griffer(); // Minou fait ses griffes
}
Pourquoi utiliser l'héritage ?
L'héritage est utile quand tu as une relation "est-un" (is-a). Un Chien est un Animal, un Chat est un Animal.
Avantages :
-
Évite la duplication de code
-
Facilite la maintenance (modifier Animal affecte tous les animaux)
-
Permet le polymorphisme (on verra ça après)
Quand l'éviter :
-
Quand tu as une relation "a-un" (has-a) → utilise la composition
-
Quand l'héritage devient trop profond (plus de 2-3 niveaux)
-
Quand tu veux juste réutiliser des comportements → utilise des mixins
super : accéder au parent
Le mot-clé super permet d'accéder aux membres de la classe parente.
class Vehicule {
String marque;
int vitesseMax;
Vehicule(this.marque, this.vitesseMax);
void demarrer() {
print('$marque démarre...');
}
void accelerer(int vitesse) {
if (vitesse <= vitesseMax) {
print('Accélération à $vitesse km/h');
} else {
print('Vitesse maximale atteinte !');
}
}
}
class Voiture extends Vehicule {
int nombrePortes;
Voiture(String marque, int vitesseMax, this.nombrePortes)
: super(marque, vitesseMax);
@override
void demarrer() {
// Appeler la méthode du parent
super.demarrer();
// Ajouter un comportement spécifique
print('Bouclez vos ceintures !');
}
@override
void accelerer(int vitesse) {
print('🚗 Voiture: ');
super.accelerer(vitesse);
}
}
void main() {
var voiture = Voiture('Toyota', 180, 4);
voiture.demarrer();
// Output:
// Toyota démarre...
// Bouclez vos ceintures !
voiture.accelerer(120);
// Output:
// 🚗 Voiture:
// Accélération à 120 km/h
}
Classes abstraites : définir des contrats
Une classe abstraite est une classe qui ne peut pas être instanciée directement. Elle sert de modèle pour d'autres classes.
Pourquoi les classes abstraites ?
Imagine que tu crées différentes formes géométriques. Toutes ont une aire et un périmètre, mais le calcul est différent pour chaque forme.
abstract class Forme {
// Propriété abstraite (doit être implémentée)
double get aire;
double get perimetre;
// Méthode abstraite (doit être implémentée)
void dessiner();
// Méthode concrète (peut être utilisée telle quelle)
void afficherInfo() {
print('Aire: $aire');
print('Périmètre: $perimetre');
}
}
class Rectangle extends Forme {
double largeur;
double hauteur;
Rectangle(this.largeur, this.hauteur);
@override
double get aire => largeur * hauteur;
@override
double get perimetre => 2 * (largeur + hauteur);
@override
void dessiner() {
print('Dessine un rectangle ${largeur}x$hauteur');
}
}
class Cercle extends Forme {
double rayon;
Cercle(this.rayon);
@override
double get aire => pi * rayon * rayon;
@override
double get perimetre => 2 * pi * rayon;
@override
void dessiner() {
print('Dessine un cercle de rayon $rayon');
}
}
void main() {
// var forme = Forme(); // ERREUR : ne peut pas instancier une classe abstraite
List<Forme> formes = [
Rectangle(5, 3),
Cercle(4),
Rectangle(2, 8),
];
for (var forme in formes) {
forme.dessiner();
forme.afficherInfo();
print('---');
}
}
Les classes abstraites forcent les sous-classes à implémenter certaines méthodes. C'est comme un contrat : "Si tu veux être une Forme, tu DOIS implémenter aire, perimetre et dessiner()."
Abstract vs Interface
En Dart, il n'y a pas de mot-clé interface, mais tu peux utiliser n'importe quelle classe comme une interface avec implements.
// Utilisé comme interface
class Volant {
void voler() {
print('Vole dans les airs');
}
void atterrir() {
print('Atterrit au sol');
}
}
class Nageur {
void nager() {
print('Nage dans l\'eau');
}
}
// Implémenter plusieurs interfaces
class Canard implements Volant, Nageur {
// DOIT redéfinir toutes les méthodes
@override
void voler() {
print('Le canard vole');
}
@override
void atterrir() {
print('Le canard atterrit');
}
@override
void nager() {
print('Le canard nage');
}
}
void main() {
var canard = Canard();
canard.voler();
canard.nager();
canard.atterrir();
}
Différence clé :
-
extends(héritage) : Tu hérites de l'implémentation -
implements(interface) : Tu dois réimplémenter tout
Mixins : composition de comportements
Les mixins sont une des fonctionnalités les plus puissantes de Dart. Ils permettent d'ajouter des comportements à une classe sans utiliser l'héritage.
Le problème que les mixins résolvent
Imagine que tu as différents types de personnages dans un jeu :
-
Guerrier : peut attaquer et se défendre
-
Mage : peut lancer des sorts et se téléporter
-
Paladin : peut attaquer, se défendre ET lancer des sorts
Avec l'héritage simple, tu es coincé :
// Comment faire hériter Paladin de Guerrier ET Mage ?
// Impossible avec l'héritage simple !
Solution avec les mixins
// Mixin : comportements réutilisables
mixin Attaquant {
int force = 10;
void attaquer(String cible) {
print('Attaque $cible avec une force de $force');
}
}
mixin LanceurDeSorts {
int mana = 100;
void lancerSort(String sort) {
if (mana >= 20) {
mana -= 20;
print('Lance le sort: $sort (Mana restant: $mana)');
} else {
print('Pas assez de mana !');
}
}
}
mixin Defenseur {
int armure = 5;
void seDefendre() {
print('Se défend avec une armure de $armure');
}
}
mixin Teleporteur {
void teleporter(String destination) {
print('Se téléporte vers $destination');
}
}
// Classe de base
class Personnage {
String nom;
int vie = 100;
Personnage(this.nom);
void afficherStats() {
print('$nom - Vie: $vie');
}
}
// Guerrier : attaque et se défend
class Guerrier extends Personnage with Attaquant, Defenseur {
Guerrier(String nom) : super(nom) {
force = 15; // Plus fort qu'un personnage normal
armure = 10;
}
}
// Mage : lance des sorts et se téléporte
class Mage extends Personnage with LanceurDeSorts, Teleporteur {
Mage(String nom) : super(nom) {
mana = 150; // Plus de mana
}
}
// Paladin : combine attaque, défense ET sorts !
class Paladin extends Personnage with Attaquant, Defenseur, LanceurDeSorts {
Paladin(String nom) : super(nom) {
force = 12;
armure = 8;
mana = 80;
}
}
void main() {
var guerrier = Guerrier('Conan');
guerrier.afficherStats();
guerrier.attaquer('Dragon');
guerrier.seDefendre();
print('\n---\n');
var mage = Mage('Gandalf');
mage.afficherStats();
mage.lancerSort('Boule de feu');
mage.teleporter('Tour de mage');
print('\n---\n');
var paladin = Paladin('Arthas');
paladin.afficherStats();
paladin.attaquer('Démon');
paladin.seDefendre();
paladin.lancerSort('Lumière divine');
}
Règles des mixins
-
Un mixin ne peut pas avoir de constructeur
-
On peut utiliser plusieurs mixins (contrairement à l'héritage)
-
L'ordre des mixins compte (le dernier peut override le précédent)
mixin A {
void methode() => print('A');
}
mixin B {
void methode() => print('B');
}
class Test with A, B {}
void main() {
Test().methode(); // B (le dernier mixin gagne)
}
Mixins avec contraintes
Tu peux restreindre un mixin à certaines classes :
// Ce mixin ne peut être utilisé que sur des Personnage
mixin Volant on Personnage {
void voler() {
if (vie > 20) {
vie -= 20;
print('$nom s\'envole !');
} else {
print('Trop fatigué pour voler');
}
}
}
class Dragon extends Personnage with Volant {
Dragon(String nom) : super(nom);
}
// class Caillou with Volant {} // ERREUR : Caillou n'est pas un Personnage
Polymorphisme : plusieurs formes
Le polymorphisme permet à un objet d'adopter plusieurs formes. En pratique, ça signifie qu'une variable de type parent peut référencer n'importe quel objet enfant.
Polymorphisme en action
abstract class Animal {
String nom;
Animal(this.nom);
void faireDuBruit(); // Méthode abstraite
}
class Chien extends Animal {
Chien(String nom) : super(nom);
@override
void faireDuBruit() {
print('$nom: Wouf wouf!');
}
}
class Chat extends Animal {
Chat(String nom) : super(nom);
@override
void faireDuBruit() {
print('$nom: Miaou!');
}
}
class Vache extends Animal {
Vache(String nom) : super(nom);
@override
void faireDuBruit() {
print('$nom: Meuh!');
}
}
// Fonction qui accepte n'importe quel Animal
void faireParlerAnimal(Animal animal) {
animal.faireDuBruit(); // Polymorphisme !
}
void main() {
// Tous stockés dans une liste de type Animal
List<Animal> ferme = [
Chien('Rex'),
Chat('Minou'),
Vache('Marguerite'),
Chien('Médor'),
];
// Chaque animal fait son propre bruit
for (var animal in ferme) {
faireParlerAnimal(animal);
}
// Output:
// Rex: Wouf wouf!
// Minou: Miaou!
// Marguerite: Meuh!
// Médor: Wouf wouf!
}
C'est la magie du polymorphisme : on écrit du code qui travaille avec le type parent (Animal), mais qui s'adapte automatiquement au type réel de l'objet (Chien, Chat, Vache).
Type checking et casting
Parfois, tu as besoin de vérifier ou convertir le type d'un objet.
void traiterAnimal(Animal animal) {
// Type checking avec 'is'
if (animal is Chien) {
print('C\'est un chien !');
animal.faireDuBruit(); // animal est automatiquement casté en Chien
}
// Type checking négatif avec 'is!'
if (animal is! Chat) {
print('Ce n\'est pas un chat');
}
// Casting explicite avec 'as'
try {
var chien = animal as Chien;
chien.faireDuBruit();
} catch (e) {
print('Erreur: animal n\'est pas un Chien');
}
}
Patterns de conception orientés objet
Maintenant qu'on a les bases, explorons quelques patterns de conception classiques en Dart.
Singleton : une seule instance
Garantit qu'une classe n'a qu'une seule instance.
class BaseDeDonnees {
// Instance statique privée
static BaseDeDonnees? _instance;
// Constructeur privé
BaseDeDonnees._();
// Factory pour retourner l'instance unique
factory BaseDeDonnees() {
_instance ??= BaseDeDonnees._();
return _instance!;
}
void connexion() {
print('Connecté à la base de données');
}
}
void main() {
var db1 = BaseDeDonnees();
var db2 = BaseDeDonnees();
print(identical(db1, db2)); // true - même instance !
db1.connexion(); // Connecté à la base de données
}
Builder : construction fluide
Permet de construire des objets complexes étape par étape.
class Pizza {
String taille;
List<String> ingredients;
bool fromage;
bool sauce;
Pizza({
required this.taille,
required this.ingredients,
this.fromage = true,
this.sauce = true,
});
@override
String toString() {
return 'Pizza $taille avec ${ingredients.join(", ")}';
}
}
class PizzaBuilder {
String _taille = 'Moyenne';
List<String> _ingredients = [];
bool _fromage = true;
bool _sauce = true;
PizzaBuilder taille(String taille) {
_taille = taille;
return this; // Retourne this pour chaîner les appels
}
PizzaBuilder ajouterIngredient(String ingredient) {
_ingredients.add(ingredient);
return this;
}
PizzaBuilder sansFromage() {
_fromage = false;
return this;
}
PizzaBuilder sansSauce() {
_sauce = false;
return this;
}
Pizza build() {
return Pizza(
taille: _taille,
ingredients: _ingredients,
fromage: _fromage,
sauce: _sauce,
);
}
}
void main() {
// Construction fluide
var pizza = PizzaBuilder()
.taille('Grande')
.ajouterIngredient('Pepperoni')
.ajouterIngredient('Champignons')
.ajouterIngredient('Olives')
.sansFromage()
.build();
print(pizza);
// Pizza Grande avec Pepperoni, Champignons, Olives
}
Factory Method : création polymorphique
Laisse les sous-classes décider quelle classe instancier.
abstract class Transport {
void livrer();
// Factory method
factory Transport.creer(String type) {
switch (type) {
case 'camion':
return Camion();
case 'navire':
return Navire();
case 'avion':
return Avion();
default:
throw ArgumentError('Type de transport inconnu: $type');
}
}
}
class Camion implements Transport {
@override
void livrer() {
print('🚚 Livraison par camion sur route');
}
}
class Navire implements Transport {
@override
void livrer() {
print('🚢 Livraison par navire en mer');
}
}
class Avion implements Transport {
@override
void livrer() {
print('✈️ Livraison par avion en vol');
}
}
void main() {
var transports = [
Transport.creer('camion'),
Transport.creer('navire'),
Transport.creer('avion'),
];
for (var transport in transports) {
transport.livrer();
}
}
Bonnes pratiques de la POO
Terminons avec des conseils pour écrire du code orienté objet de qualité.
1. Préfère la composition à l'héritage
// ✗ Mauvais : héritage pour réutiliser du code
class Logger {
void log(String message) => print(message);
}
class ServiceUtilisateur extends Logger {
void creerUtilisateur() {
log('Création d\'un utilisateur');
}
}
// ✓ Bon : composition
class ServiceUtilisateur {
final Logger _logger;
ServiceUtilisateur(this._logger);
void creerUtilisateur() {
_logger.log('Création d\'un utilisateur');
}
}
2. Principe de responsabilité unique
Chaque classe ne devrait avoir qu'une seule raison de changer.
// ✗ Mauvais : trop de responsabilités
class Utilisateur {
String nom;
String email;
Utilisateur(this.nom, this.email);
void sauvegarderDansDB() { /* ... */ }
void envoyerEmail() { /* ... */ }
void genererRapport() { /* ... */ }
}
// ✓ Bon : responsabilités séparées
class Utilisateur {
String nom;
String email;
Utilisateur(this.nom, this.email);
}
class UtilisateurRepository {
void sauvegarder(Utilisateur user) { /* ... */ }
}
class EmailService {
void envoyer(String email, String message) { /* ... */ }
}
class RapportService {
void generer(Utilisateur user) { /* ... */ }
}
3. Encapsule les données
// ✗ Mauvais : données publiques
class Compte {
double solde; // N'importe qui peut modifier !
}
// ✓ Bon : données privées avec méthodes publiques
class Compte {
double _solde;
Compte(this._solde);
double get solde => _solde;
void deposer(double montant) {
if (montant > 0) {
_solde += montant;
}
}
bool retirer(double montant) {
if (montant > 0 && montant <= _solde) {
_solde -= montant;
return true;
}
return false;
}
}
4. Utilise final quand possible
// ✓ Bon : propriétés immutables
class Point {
final double x;
final double y;
const Point(this.x, this.y);
}
5. Documente tes classes
/// Représente un utilisateur de l'application.
///
/// Contient les informations de base d'un utilisateur
/// et fournit des méthodes pour gérer son profil.
class Utilisateur {
/// Le nom complet de l'utilisateur
final String nom;
/// L'adresse email (optionnelle)
final String? email;
/// Crée un nouvel utilisateur
///
/// [nom] est requis et ne peut pas être vide.
/// [email] est optionnel mais doit être valide si fourni.
Utilisateur({
required this.nom,
this.email,
}) : assert(nom.isNotEmpty, 'Le nom ne peut pas être vide');
/// Vérifie si l'utilisateur a un email valide
bool get aEmailValide => email != null && email!.contains('@');
}
Conclusion
La programmation orientée objet en Dart est riche et puissante. Elle te permet de créer des applications bien structurées, maintenables et évolutives.
Points clés à retenir :
-
Classes : Encapsulent données et comportements
-
Constructeurs : Multiples façons de créer des objets (nommés, factory)
-
Héritage : Réutiliser le code, relation "est-un"
-
Classes abstraites : Définir des contrats
-
Mixins : Composer des comportements sans héritage
-
Polymorphisme : Code flexible qui s'adapte aux types réels
Bonnes pratiques :
-
Préfère composition à héritage
-
Une classe = une responsabilité
-
Encapsule les données avec propriétés privées
-
Utilise final pour l'immutabilité
-
Documente tes classes
Patterns utiles :
-
Singleton pour instances uniques
-
Builder pour construction complexe
-
Factory pour création polymorphique
Avec ces concepts, tu es maintenant équipé pour créer des architectures orientées objet robustes en Dart !
Ressources pour aller plus loin :
Tu as des questions sur la POO en Dart ? Partage-les dans les commentaires !
Bon code orienté objet ! 🚀🎯

Laisser un commentaire