JavaFX Intermédiaire

Architectures Scalables avec JavaFX : De la Théorie aux Patterns Professionnels

Maîtrisez les architectures robustes et les patterns de conception appliqués à JavaFX pour construire des applications desktop professionnelles et maintenables. Découvrez comment structurer vos projets comme dans les environnements d'entreprise.

Preparetoi.academy 30 min
{
  "cours": {
    "metadata": {
      "titre": "Architectures Scalables avec JavaFX : De la Théorie aux Patterns Professionnels",
      "description": "Maîtrisez les architectures robustes et les patterns de conception appliqués à JavaFX pour construire des applications desktop professionnelles et maintenables. Découvrez comment structurer vos projets comme dans les environnements d'entreprise.",
      "technologie": "JavaFX",
      "categorie": "Développement Desktop",
      "domaine": "Développement & Programmation",
      "niveau": "Intermédiaire",
      "langue": "Français",
      "type": "Mixte",
      "duree_minutes": 30,
      "nombre_sections": 5
    },
    "sections": [
      {
        "numero": 1,
        "titre": "Architecture MVC dans JavaFX : Fondations et Implémentation",
        "contenu": "## Architecture MVC dans JavaFX : Fondations et Implémentation\n\n### Définition\nL'architecture Modèle-Vue-Contrôleur (MVC) est un pattern de conception qui sépare une application en trois composants interconnectés : le Modèle (données et logique métier), la Vue (interface utilisateur) et le Contrôleur (logique de liaison). Dans JavaFX, cette architecture permet une séparation claire des responsabilités et facilite la testabilité et la maintenance du code.\n\n### Explication Détaillée\nDans un contexte professionnel, le pattern MVC est fondamental pour construire des applications scalables. Le Modèle contient toute la logique métier et la gestion des données, complètement indépendant de JavaFX. La Vue est définie en FXML ou en code JavaFX et ne contient que la présentation. Le Contrôleur fait le lien entre les deux, gérant les événements utilisateur et mettant à jour le Modèle. Cette séparation permet à plusieurs développeurs de travailler en parallèle : les experts UI sur la Vue, les développeurs backend sur le Modèle, et les intégrateurs sur le Contrôleur.\n\nCette approche présente des avantages majeurs : testabilité unitaire du Modèle sans dépendre de JavaFX, réutilisabilité du code métier dans d'autres contextes (API REST, CLI), et maintenabilité accrue. En environnement d'entreprise, cette structure facilite également la collaboration en équipe et la révision de code.\n\n### Bloc de Code\n```java\n// Modèle - Logique métier pure\npublic class UtilisateurModele {\n    private String nom;\n    private String email;\n    private double solde;\n    \n    public UtilisateurModele(String nom, String email, double solde) {\n        this.nom = nom;\n        this.email = email;\n        this.solde = solde;\n    }\n    \n    public boolean validerEmail(String email) {\n        return email.matches(\"^[A-Za-z0-9+_.-]+@(.+)$\");\n    }\n    \n    public void deposer(double montant) {\n        if (montant > 0) {\n            this.solde += montant;\n        }\n    }\n    \n    // Getters et setters\n    public String getNom() { return nom; }\n    public String getEmail() { return email; }\n    public double getSolde() { return solde; }\n}\n\n// Contrôleur - Liaison Vue-Modèle\npublic class UtilisateurControleur implements Initializable {\n    @FXML private TextField champNom;\n    @FXML private TextField champEmail;\n    @FXML private Label labelSolde;\n    @FXML private Button boutonDepot;\n    \n    private UtilisateurModele utilisateur;\n    \n    @Override\n    public void initialize(URL url, ResourceBundle rb) {\n        this.utilisateur = new UtilisateurModele(\"Alice\", \"alice@mail.com\", 1000);\n        mettreAJourVue();\n        boutonDepot.setOnAction(e -> traiterDepot());\n    }\n    \n    private void traiterDepot() {\n        try {\n            double montant = Double.parseDouble(champNom.getText());\n            utilisateur.deposer(montant);\n            mettreAJourVue();\n        } catch (NumberFormatException e) {\n            afficherErreur(\"Montant invalide\");\n        }\n    }\n    \n    private void mettreAJourVue() {\n        champNom.setText(utilisateur.getNom());\n        champEmail.setText(utilisateur.getEmail());\n        labelSolde.setText(\"Solde: \" + utilisateur.getSolde() + \"€\");\n    }\n    \n    private void afficherErreur(String message) {\n        Alert alert = new Alert(Alert.AlertType.ERROR, message);\n        alert.show();\n    }\n}\n```\n\n### Tableau Comparatif\n| Aspect | Avantage | Inconvénient |\n|--------|----------|---------------|\n| Testabilité | Modèle testable sans UI | Plus de fichiers à gérer |\n| Réutilisabilité | Code métier partageables | Architecture plus complexe |\n| Maintenance | Modifications isolées | Courbe d'apprentissage |\n| Collaboration | Parallélisation du travail | Coordination nécessaire |\n| Performance | Optimisations ciblées | Possible overhead minimal |\n\n### Astuce Professionnelle\nEn entreprise, utilisez une structure de répertoires cohérente : `modeles/`, `vues/`, `controleurs/`, `services/`, `utilitaires/`. Cela facilite l'onboarding de nouveaux développeurs et améliore la navigation dans les grands projets. Implémentez également une couche Service entre le Contrôleur et le Modèle pour les opérations complexes.\n\n### Attention - Point Critique\n⚠️ **Ne placez JAMAIS de logique métier dans le Contrôleur !** Un Contrôleur obèse est difficile à tester et viole le principe de responsabilité unique. Le Contrôleur doit uniquement orchestrer les interactions entre Vue et Modèle. Si vous trouvez du code métier dans le Contrôleur, extrayez-le immédiatement dans le Modèle ou une classe Service."
      },
      {
        "numero": 2,
        "titre": "Properties et Binding : La Réactivité en JavaFX",
        "contenu": "## Properties et Binding : La Réactivité en JavaFX\n\n### Définition\nLes Properties sont des observables spécialisées qui encapsulent des valeurs et notifient automatiquement les observateurs en cas de changement. Le Binding est un mécanisme qui établit une liaison entre deux properties de sorte que lorsque l'une change, l'autre se met à jour automatiquement. Ces deux mécanismes forment le cœur du système réactif de JavaFX.\n\n### Explication Détaillée\nDans les applications desktop modernes, la réactivité est essentielle pour une bonne expérience utilisateur. Les Properties offrent une alternative élégante aux getters/setters traditionnels. Contrairement aux simples variables, une Property peut notifier plusieurs observateurs de ses changements et peut être liée à d'autres Properties. Cette approche déclarative réduit le code \"boilerplate\" et minimise les erreurs causées par des mises à jour manquelles.\n\nLe Binding peut être unidirectionnel (une seule direction) ou bidirectionnel (deux directions). Par exemple, un champ texte dans l'UI peut être lié à une property du modèle : toute modification du champ met à jour automatiquement le modèle, et inversement. Cela élimine le besoin de code d'écoute manuel et les bugs liés aux mises à jour oubliées.\n\nEn environnement professionnel, cette approche facilite la synchronisation complexe entre plusieurs composants. Par exemple, l'activation d'un bouton dépend souvent de multiples conditions : validation d'email ET montant valide ET connexion utilisateur. Avec le Binding, ces dépendances se gèrent déclarativement et le système les maintient synchronisées automatiquement.\n\n### Bloc de Code\n```java\n// Modèle avec Properties\npublic class CommandeModele {\n    private final StringProperty nomClient = new SimpleStringProperty(\"\";\n    private final DoubleProperty montantArticle = new SimpleDoubleProperty(0);\n    private final IntegerProperty quantite = new SimpleIntegerProperty(0);\n    private final DoubleProperty montantTotal = new SimpleDoubleProperty(0);\n    private final BooleanProperty peutValider = new SimpleBooleanProperty(false);\n    \n    public CommandeModele() {\n        // Binding automatique du montant total\n        montantTotal.bind(montantArticle.multiply(quantite));\n        \n        // Validation automatique pour le bouton\n        Bindings.when(\n            nomClient.isNotEmpty()\n            .and(montantArticle.greaterThan(0))\n            .and(quantite.greaterThan(0))\n        ).then(true).otherwise(false).bindBidirectional(peutValider);\n    }\n    \n    // Properties (getters)\n    public StringProperty nomClientProperty() { return nomClient; }\n    public DoubleProperty montantArticleProperty() { return montantArticle; }\n    public IntegerProperty quantiteProperty() { return quantite; }\n    public DoubleProperty montantTotalProperty() { return montantTotal; }\n    public ReadOnlyBooleanProperty peutValiderProperty() { \n        return FXCollections.unmodifiableObservableValue(peutValider); \n    }\n}\n\n// Contrôleur avec Bindings\npublic class CommandeControleur implements Initializable {\n    @FXML private TextField champNom;\n    @FXML private TextField champMontant;\n    @FXML private Spinner<Integer> spinQuantite;\n    @FXML private Label labelTotal;\n    @FXML private Button boutonValider;\n    \n    private CommandeModele commande;\n    \n    @Override\n    public void initialize(URL url, ResourceBundle rb) {\n        this.commande = new CommandeModele();\n        \n        // Binding bidirectionnel pour le nom\n        Bindings.bindBidirectional(\n            champNom.textProperty(),\n            commande.nomClientProperty()\n        );\n        \n        // Binding bidirectionnel pour le montant\n        Bindings.bindBidirectional(\n            champMontant.textProperty(),\n            commande.montantArticleProperty(),\n            new NumberStringConverter()\n        );\n        \n        // Binding bidirectionnel pour la quantité\n        spinQuantite.getValueFactory().valueProperty()\n            .bindBidirectional(commande.quantiteProperty());\n        \n        // Binding unidirectionnel - le label affiche le total (lecture seule)\n        labelTotal.textProperty().bind(\n            Bindings.format(\"Total: %.2f€\", commande.montantTotalProperty())\n        );\n        \n        // Le bouton s'active/désactive automatiquement\n        boutonValider.disableProperty()\n            .bind(commande.peutValiderProperty().not());\n    }\n}\n\n// Exemple d'observateur personnalisé\npublic class EcouteProperty {\n    public static void exemple() {\n        SimpleStringProperty nom = new SimpleStringProperty(\"Alice\");\n        \n        // Ajout d'un observateur\n        nom.addListener((observable, ancienneValeur, nouvelleValeur) -> {\n            System.out.println(\"Changement: \" + ancienneValeur + \" -> \" + nouvelleValeur);\n        });\n        \n        nom.set(\"Bob\"); // Déclenche l'observateur\n    }\n}\n```\n\n### Tableau des Types de Properties\n| Type | Description | Cas d'Usage |\n|------|-------------|------------|\n| StringProperty | Encapsule un String | Noms, emails, descriptions |\n| DoubleProperty | Encapsule un double | Prix, montants, pourcentages |\n| IntegerProperty | Encapsule un int | Quantités, compteurs |\n| BooleanProperty | Encapsule un boolean | États, validations |\n| ListProperty | Encapsule une liste | Collectionner d'objets |\n| ObjectProperty | Générique | Objets complexes |\n\n### Astuce Professionnelle\nUtilisez `Bindings.when()` pour créer des conditions complexes de manière déclarative. Par exemple, désactiver un bouton si le formulaire n'est pas valide est aussi simple que `bouton.disableProperty().bind(formulaire.valideProperty().not())`. Cela remplace des dizaines de lignes de code d'écoute manuel et est beaucoup plus maintenable.\n\n### Attention - Point Critique\n⚠️ **Attention aux cycles infinis avec le Binding bidirectionnel !** Si A est lié à B et B à A dans une boucle circulaire sans protection, cela créera un cycle infini. Utilisez plutôt un Binding unidirectionnel ou une validation de changement pour éviter les appels récursifs. Testez toujours les scénarios où deux propriétés s'affectent mutuellement."
      },
      {
        "numero": 3,
        "titre": "Injection de Dépendances et FXML : Couplage Faible",
        "contenu": "## Injection de Dépendances et FXML : Couplage Faible\n\n### Définition\nL'Injection de Dépendances (DI) est un pattern de conception où les dépendances d'une classe sont fournies de l'extérieur plutôt que créées en interne. Associée à FXML (langage de markup XML de JavaFX), cette approche permet de séparer complètement la définition de l'interface utilisateur de la logique applicative, favorisant un couplage faible et une meilleure testabilité.\n\n### Explication Détaillée\nDans les applications d'entreprise, le couplage fort entre composants est une source majeure de problèmes : difficultés de test, impossibilité de changer d'implémentation, maintenance complexe. L'injection de dépendances résout ces problèmes en inversant le contrôle. Au lieu que une classe crée ses dépendances, ces dernières lui sont injectées, généralement par un conteneur DI comme Spring.\n\nFXML est un language XML déclaratif permettant de définir l'interface sans écrire de code Java pour la construction. Combiné à l'injection de dépendances, cela crée une architecture très flexible : les développeurs UI modifient FXML sans toucher au code Java, les contrôleurs sont injectés avec leurs dépendances sans connaître comment les obtenir.\n\nPour les petits projets, on peut implémenter une DI simple. Pour les projets d'envergure, on utilise Spring (avec le module javafx-spring) qui gère élégamment le cycle de vie des beans et leur injection. Cette approche est standard dans les entreprises utilisant Java.\n\n### Bloc de Code\n```java\n// Service - couche métier\npublic interface UtilisateurService {\n    Utilisateur chargerUtilisateur(String id);\n    void sauvegarderUtilisateur(Utilisateur utilisateur);\n    List<Utilisateur> listerUtilisateurs();\n}\n\npublic class UtilisateurServiceImpl implements UtilisateurService {\n    private final UtilisateurRepository repository;\n    private final Logger logger = LoggerFactory.getLogger(this.getClass());\n    \n    // Injection du repository\n    public UtilisateurServiceImpl(UtilisateurRepository repository) {\n        this.repository = repository;\n    }\n    \n    @Override\n    public Utilisateur chargerUtilisateur(String id) {\n        logger.info(\"Chargement utilisateur: {}\", id);\n        return repository.findById(id).orElse(null);\n    }\n    \n    @Override\n    public void sauvegarderUtilisateur(Utilisateur utilisateur) {\n        repository.save(utilisateur);\n        logger.info(\"Utilisateur sauvegardé: {}\", utilisateur.getId());\n    }\n    \n    @Override\n    public List<Utilisateur> listerUtilisateurs() {\n        return repository.findAll();\n    }\n}\n\n// Contrôleur avec injection de service\npublic class UtilisateurControleur implements Initializable {\n    @FXML private TableView<Utilisateur> tableauUtilisateurs;\n    @FXML private TableColumn<Utilisateur, String> colonneNom;\n    @FXML private TableColumn<Utilisateur, String> colonneEmail;\n    @FXML private TextField champRecherche;\n    \n    private final UtilisateurService utilisateurService;\n    private final ObservableList<Utilisateur> donnees = FXCollections.observableArrayList();\n    \n    // Injection du service\n    public UtilisateurControleur(UtilisateurService utilisateurService) {\n        this.utilisateurService = utilisateurService;\n    }\n    \n    @Override\n    public void initialize(URL url, ResourceBundle rb) {\n        // Configuration des colonnes\n        colonneNom.setCellValueFactory(new PropertyValueFactory<>(\"nom\"));\n        colonneEmail.setCellValueFactory(new PropertyValueFactory<>(\"email\"));\n        \n        // Liaison des données\n        tableauUtilisateurs.setItems(donnees);\n        \n        // Chargement initial\n        chargerUtilisateurs();\n        \n        // Recherche en temps réel\n        champRecherche.textProperty().addListener((obs, ancien, nouveau) -> {\n            filtrerUtilisateurs(nouveau);\n        });\n    }\n    \n    private void chargerUtilisateurs() {\n        try {\n            donnees.setAll(utilisateurService.listerUtilisateurs());\n        } catch (Exception e) {\n            afficherErreur(\"Erreur lors du chargement: \" + e.getMessage());\n        }\n    }\n    \n    private void filtrerUtilisateurs(String filtre) {\n        List<Utilisateur> tous = utilisateurService.listerUtilisateurs();\n        if (filtre == null || filtre.isEmpty()) {\n            donnees.setAll(tous);\n        } else {\n            donnees.setAll(tous.stream()\n                .filter(u -> u.getNom().toLowerCase().contains(filtre.toLowerCase()))\n                .collect(Collectors.toList()));\n        }\n    }\n    \n    private void afficherErreur(String message) {\n        new Alert(Alert.AlertType.ERROR, message).show();\n    }\n}\n\n// Factory pour l'injection (approche simple sans Spring)\npublic class ApplicationFactory {\n    private static final Map<Class<?>, Object> SINGLETONS = new HashMap<>();\n    \n    public static <T> T creer(Class<T> classe) {\n        if (SINGLETONS.containsKey(classe)) {\n            return (T) SINGLETONS.get(classe);\n        }\n        \n        T instance = null;\n        if (classe == UtilisateurService.class) {\n            UtilisateurRepository repo = creer(UtilisateurRepository.class);\n            instance = (T) new UtilisateurServiceImpl(repo);\n        } else if (classe == UtilisateurRepository.class) {\n            instance = (T) new UtilisateurRepositoryImpl();\n        }\n        \n        SINGLETONS.put(classe, instance);\n        return instance;\n    }\n}\n\n// Fichier FXML (UtilisateurVue.fxml)\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<?import javafx.scene.control.*?>\n<?import javafx.scene.layout.*?>\n\n<VBox xmlns=\"http://javafx.com/javafx/21\" spacing=\"10\" padding=\"10\">\n    <TextField fx:id=\"champRecherche\" promptText=\"Rechercher un utilisateur...\"/>\n    <TableView fx:id=\"tableauUtilisateurs\">\n        <columns>\n            <TableColumn fx:id=\"colonneNom\" text=\"Nom\" prefWidth=\"150\"/>\n            <TableColumn fx:id=\"colonneEmail\" text=\"Email\" prefWidth=\"250\"/>\n        </columns>\n    </TableView>\n</VBox>\n```\n\n### Tableau de Comparaison Architectures\n| Aspect | Sans DI | Avec DI Simple | Avec Spring |  \n|--------|---------|----------------|-------------|\n| Couplage | Fort | Faible | Très Faible |\n| Testabilité | Difficile | Facile | Très Facile |\n| Flexibilité | Limitée | Bonne | Excellente |\n| Complexité | Simple | Modérée | Initiale importante |\n| Idéal pour | Petits projets | Projets moyens | Projets d'entreprise |\n\n### Astuce Professionnelle\nCréez une interface pour chaque service, même si vous n'avez qu'une implémentation. Cela coûte peu mais offre une grande flexibilité future pour changer d'implémentation ou créer des mocks pour les tests. Par exemple, pour tester le Contrôleur sans accéder à la base de données, créez un mock de UtilisateurService qui retourne des données de test.\n\n### Attention - Point Critique\n⚠️ **Ne pas créer les Contrôleurs manuellement !** Avec FXML, le FXMLLoader crée les instances des Contrôleurs. Pour injecter des dépendances, configurez un ControllerFactory sur le FXMLLoader : `loader.setControllerFactory(classe -> ApplicationFactory.creer(classe))`. Si vous créez les Contrôleurs vous-même, l'injection ne fonctionnera pas et le FXML ne trouvera pas le contrôleur."
      },
      {
        "numero": 4,
        "titre": "Gestion d'État Avancée avec Observable Collections",
        "contenu": "## Gestion d'État Avancée avec Observable Collections\n\n### Définition\nLes Observable Collections (ObservableList, ObservableMap, ObservableSet) sont des collections JavaFX qui notifient automatiquement les observateurs des changements de contenu. Elles permettent de créer une application où l'UI se met à jour automatiquement quand les données changent, sans code de synchronisation manuel, formant ainsi un état applicatif réactif et prévisible.\n\n### Explication Détaillée\nEn développement desktop professionnel, la gestion de l'état applicatif est complexe. Les collections standard de Java ne notifient pas les observateurs de leurs changements, obligeant les développeurs à écrire du code de notification manuel source d'oublis et de bugs. Les Observable Collections résolvent ce problème.\n\nUne ObservableList peut être liée à un contrôle comme TableView ou ListView : tout élément ajouté/retiré/modifié dans la liste est immédiatement visible dans l'UI. Inversement, les sélections utilisateur dans l'UI affectent la liste. Cette synchronisation bidirectionnelle est automatique et fiable.\n\nPour les applications complexes, on combine Observable Collections avec un pattern de gestion d'état centralisé. Un seul objet (State ou Store) contient tout l'état de l'application, représenté par des Observable Collections. Tous les composants observent cet état centralisé, éliminant les incohérences d'état entre composants.\n\n### Bloc de Code\n```java\n// État centralisé de l'application\npublic class ApplicationState {\n    private final ObservableList<Produit> produits = FXCollections.observableArrayList();\n    private final ObservableList<Commande> commandes = FXCollections.observableArrayList();\n    private final ObjectProperty<Utilisateur> utilisateurConnecte = new SimpleObjectProperty<>();\n    private final DoubleProperty montantPanier = new SimpleDoubleProperty(0);\n    \n    public ApplicationState() {\n        // Initialiser les données\n        initialiserProduits();\n        // Observer les changements du panier\n        commandes.addListener((ListChangeListener<Commande>) change -> mettreAJourMontant());\n    }\n    \n    private void initialiserProduits() {\n        produits.addAll(\n            new Produit(\"P001\", \"Laptop\", 999.99),\n            new Produit(\"P002\", \"Souris\", 29.99),\n            new Produit(\"P003\", \"Clavier\", 79.99)\n        );\n    }\n    \n    private void mettreAJourMontant() {\n        double total = commandes.stream()\n            .mapToDouble(Commande::calculerTotal)\n            .sum();\n        montantPanier.set(total);\n    }\n    \n    // Méthodes de modification d'état\n    public void ajouterAuPanier(Produit produit, int quantite) {\n        Commande existante = commandes.stream()\n            .filter(c -> c.getProduit().getId().equals(produit.getId()))\n            .findFirst()\n            .orElse(null);\n        \n        if (existante != null) {\n            existante.augmenterQuantite(quantite);\n        } else {\n            commandes.add(new Commande(produit, quantite));\n        }\n    }\n    \n    public void retirerDuPanier(Commande commande) {\n        commandes.remove(commande);\n    }\n    \n    public void validerCommande() {\n        if (utilisateurConnecte.get() == null) {\n            throw new IllegalStateException(\"Connexion requise\");\n        }\n        commandes.clear();\n        montantPanier.set(0);\n    }\n    \n    // Properties\n    public ObservableList<Produit> getProduits() { return produits; }\n    public ObservableList<Commande> getCommandes() { return FXCollections.unmodifiableObservableList(commandes); }\n    public ObjectProperty<Utilisateur> utilisateurConnecteProperty() { return utilisateurConnecte; }\n    public ReadOnlyDoubleProperty montantPanierProperty() { return montantPanier; }\n}\n\n// Modèle de Produit\npublic class Produit {\n    private final String id;\n    private final String nom;\n    private final double prix;\n    \n    public Produit(String id, String nom, double prix) {\n        this.id = id;\n        this.nom = nom;\n        this.prix = prix;\n    }\n    \n    public String getId() { return id; }\n    public String getNom() { return nom; }\n    public double getPrix() { return prix; }\n}\n\n// Modèle de Commande\npublic class Commande {\n    private final Produit produit;\n    private final IntegerProperty quantite = new SimpleIntegerProperty();\n    \n    public Commande(Produit produit, int quantite) {\n        this.produit = produit;\n        this.quantite.set(quantite);\n    }\n    \n    public double calculerTotal() {\n        return produit.getPrix() * quantite.get();\n    }\n    \n    public void augmenterQuantite(int delta) {\n        quantite.set(Math.max(0, quantite.get() + delta));\n    }\n    \n    public Produit getProduit() { return produit; }\n    public IntegerProperty quantiteProperty() { return quantite; }\n}\n\n// Contrôleur utilisant l'état centralisé\npublic class PanierControleur implements Initializable {\n    @FXML private TableView<Commande> tableauPanier;\n    @FXML private TableColumn<Commande, String> colonneProduit;\n    @FXML private TableColumn<Commande, Integer> colonneQuantite;\n    @FXML private TableColumn<Commande, Double> colonnePrix;\n    @FXML private Label labelTotal;\n    @FXML private Button boutonValider;\n    \n    private final ApplicationState state;\n    \n    public PanierControleur(ApplicationState state) {\n        this.state = state;\n    }\n    \n    @Override\n    public void initialize(URL url, ResourceBundle rb) {\n        // Configuration des colonnes\n        colonneProduit.setCellValueFactory(cellData -> \n            new SimpleStringProperty(cellData.getValue().getProduit().getNom()));\n        colonneQuantite.setCellValueFactory(new PropertyValueFactory<>(\"quantite\"));\n        colonnePrix.setCellValueFactory(cellData -> \n            new SimpleObjectProperty<>(cellData.getValue().calculerTotal()));\n        \n        // Liaison de la table à l'état\n        tableauPanier.setItems(state.getCommandes());\n        \n        // Liaison du label du total\n        labelTotal.textProperty().bind(\n            Bindings.format(\"Total: %.2f€\", state.montantPanierProperty())\n        );\n        \n        // Le bouton valider s'active si panier non vide et utilisateur connecté\n        boutonValider.disableProperty().bind(\n            state.getCommandes().emptyProperty()\n            .or(state.utilisateurConnecteProperty().isNull())\n        );\n    }\n}\n\n// Exemple d'utilisation\npublic class ApplicationDemo {\n    public static void main(String[] args) {\n        ApplicationState state = new ApplicationState();\n        \n        // Observer les changements\n        state.getCommandes().addListener((ListChangeListener<Commande>) change -> {\n            while (change.next()) {\n                if (change.wasAdded()) {\n                    System.out.println(\"Ajouté: \" + change.getAdded
Accédez à des centaines d'examens QCM — Découvrir les offres Premium