Skip to main content

Code that compiles on the first try! CodeWithMpia wishes you very happy holidays.✨

Aller au contenu principal

POO en Dart : classes, héritage, mixins et patterns orientés objet

Ce guide t’apprend les notions fondamentales de programmation orientée objet en Dart (classes, héritage, mixins, interfaces, constructeurs) et montre comment les utiliser pour structurer proprement ton code Flutter.

M
Mpia
12/15/2025 39 min
67 vues
0 comme.
#Dart#OOP#Classes
POO en Dart : classes, héritage, mixins et patterns orientés objet

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 :

  1. Encapsulation : Regrouper les données et méthodes, cacher les détails internes

  2. Héritage : Créer de nouvelles classes basées sur des classes existantes

  3. Polymorphisme : Un même nom peut avoir différentes implémentations

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

  1. Un mixin ne peut pas avoir de constructeur

  2. On peut utiliser plusieurs mixins (contrairement à l'héritage)

  3. 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 ! 🚀🎯

Commentaires (0)

Laisser un commentaire

Aucun commentaire pour le moment. Soyez le premier à commenter !