Aller au contenu

Architecture

L'application est locale (base SQLite fichier, sans serveur), en JavaFX 26 / Java 25, injectée par Guice 7. L'organisation est en paquet-par-fonctionnalité : chaque écran/parcours vit dans son propre paquet, qui contient lui-même ses 4 couches MVVM.

Les patrons derrière cette architecture

Chaque choix (MVVM, contrats, DAO, Template Method…) applique un patron de conception au service des principes SOLID. La page Patterns et principes les détaille un par un (le quoi, le pourquoi ici, et les principes servis).

Carte du code

src/main/java/fr/univ_amu/iut/
├── App.java                     ← point d'entrée JavaFX (amorçage Guice + chrome)
├── module-info.java             ← module JPMS « vigiechiro » (open module)
├── commun/                      ← LE SOCLE partagé par toutes les fonctionnalités
│   ├── persistence/             ·   infrastructure DAO (SQLite, transactions, migrations)
│   ├── model/                   ·   domaine transverse (Horloge, Prefixe, Verdict, Statut...)
│   ├── viewmodel/               ·   état observable du chrome (NavigationViewModel...)
│   ├── view/                    ·   chrome (MainView, Navigateur) + contrats Ouvrir* / RafraichirAuRetour
│   ├── di/                      ·   racine de composition Guice (RacineInjecteur, modules socle)
│   └── outils/                  ·   harnais de capture d'écran (ApercuFx...)
├── sites/        passage/       importation/   qualification/   lot/
├── validation/   multisite/     diagnostic/    bibliotheque/    recherche/
├── analyse/      audio/         audit/         connexion/       maj/
├── saison/                                                     ← les features métier
├── cli/                         ← interface en ligne de commande (import/export scriptables)
└── perf/outils/                 ← bancs de mesure de performance

Les deux dernières entrées sont des surfaces transverses, documentées à part : cli/ (seconde façade headless sur le métier) et perf/outils/ (bancs de mesure des objectifs O3/O5).

Entre le socle et ces surfaces, l'application compte 16 features métier (chacune un dossier autonome sous fr.univ_amu.iut, hors commun, cli et perf).

Les 4 couches d'une fonctionnalité

Chaque feature (ex. sites/) suit le même découpage, du métier vers l'IHM :

Sous-paquet Rôle Règle clé
model/ Entités (records), services, et model/dao/ (accès SQLite) Aucune dépendance JavaFX (réutilisable, testable seul)
viewmodel/ État observable + logique de présentation Importe javafx.beans uniquement, jamais javafx.scene/fxml/stage
view/ Controller + *.fxml + *.css Se lie aux propriétés du ViewModel ; ne parle jamais à la base
di/ Le module Guice qui assemble la feature Publie ses services/VM au conteneur

Le sens MVVM

Le model ignore l'IHM ; le viewmodel porte l'état en propriétés observables (IntegerProperty, ObservableList…) sans toucher aux composants graphiques ; la view observe le viewmodel par data binding JavaFX.

Ce qui est garanti n'est pas ce que ce schéma suggère. ArchUnit interdit à une view de toucher JDBC - ni model.dao, ni java.sql - pas de nommer un type de model. En pratique beaucoup de classes de view importent un paquet model, pour y lire un record ou un enum, et c'est accepté : faire transiter chaque type métier par le viewmodel produirait une couche de recopie sans lecteur. La frontière tenue est la donnée, pas le vocabulaire.

Une feature peut être sans écran

Toutes n'ont pas les 4 couches. recherche/ (#144) est une feature de service : model + di seulement, pas de view. La recherche globale est exposée par un contrat commun.model.RechercheGlobale (Separated Interface), implémenté par recherche.model.ServiceRechercheGlobale qui agrège les autres features, et surfacé par le chrome (la barre de recherche de MainView) plutôt que par un écran dédié.

Les règles d'architecture sont des tests

Ces frontières ne sont pas qu'une convention : elles sont vérifiées automatiquement par ArchUnit. Casser une frontière fait échouer la CI.

Sauf si la dépendance se réduit à une constante

ArchUnit lit le bytecode, et une dépendance qui ne consiste qu'en une constante compile-time n'y laisse aucune trace : le compilateur inline la valeur, le .class ne cite jamais la classe qui la déclarait (JLS 13.1). Toutes les règles ci-dessous sont aveugles à ce cas : ce n'est pas un faux négatif isolé mais une catégorie, qui vaut pour toute String, int ou boolean constant.

Le cas s'est produit : une commande CLI a cité le viewmodel d'une feature pour une clé de réglage sans qu'aucune règle ne bronche (#2181). D'où un doublon au niveau des sources, IsolationFeatureSourcesTest, qui lit l'import dans le .java, présent quoi qu'en fasse le compilateur. Mesuré : en réintroduisant volontairement une telle dépendance, les six règles ArchUnit restent vertes et ce test échoue.

Il ne couvre que la règle d'isolation inter-feature. Un vert d'ArchUnit dit donc « aucune dépendance dans le bytecode », pas « aucune dépendance ».

Test Ce qu'il garantit
model_sans_javafx Aucun paquet ..model.. ne dépend de javafx.. (le métier reste réutilisable).
persistance_sans_javafx commun.persistence et ..model.dao.. ignorent JavaFX.
viewmodel_sans_javafx_ui Un ..viewmodel.. n'importe pas javafx.scene/fxml/stage (javafx.beans OK).
view_sans_jdbc Un ..view.. ne touche jamais ..model.dao.. ni java.sql.. : il passe par les VM/services.
features_sans_cycle Les slices fr.univ_amu.iut.(*) sont sans cycle (hors racine de composition commun.di).
pas_de_dependance_inter_feature_vers_la_vue Une feature ne dépend pas du view/viewmodel d'une autre feature (le socle commun est partagé).

Le chrome (fenêtre + zone centrale + fil d'Ariane) est porté par le socle commun.view :

  • Navigateur pilote la zone centrale et l'historique (pile d'écrans EtapeNavigation). Les écrans restent vivants au retour (état préservé).
  • Contrats optionnels qu'un écran peut implémenter sur son controller : GardeQuitter (confirmer la sortie si saisie non enregistrée), EmplacementNavigation (fil d'Ariane hiérarchique), RafraichirAuRetour (recharger ses données quand on y revient), AuDepartEcran (rendre une ressource quand l'écran quitte l'historique : un fichier temporaire, un verrou), SuitLaRevision (se recharger quand une écriture structurelle est validée, le Navigateur portant l'abonnement).

Comment une feature en ouvre une autre sans en dépendre (inversion de dépendance) : le socle publie un contrat Ouvrir* dans commun.view, la feature cible l'implémente via sa classe Navigation* (bindée par son module Guice), et la feature appelante l'obtient par injection.

// commun/view/OuvrirPassage.java : le CONTRAT (dans le socle)
public interface OuvrirPassage {
    void ouvrir(Long idPassage, ContexteSite contexte);
}

// passage/view/NavigationPassage.java : l'IMPLÉMENTATION (dans la feature passage)
@Singleton
public class NavigationPassage implements OuvrirPassage {
    @Override public void ouvrir(Long idPassage, ContexteSite contexte) {
        FXMLLoader loader = new FXMLLoader(NavigationPassage.class.getResource("Passage.fxml"));
        loader.setControllerFactory(injector::getInstance);          // controller injecté par Guice
        Parent vue = loader.load();
        ((PassageController) loader.getController()).ouvrirSur(idPassage, contexte);
        navigateur.empiler(vue, "passage", controleur.libelleFil(), controleur);
    }
}

Ainsi sites ouvre M-Passage sans dépendre de passage.view : la règle ArchUnit pas_de_dependance_inter_feature_vers_la_vue reste respectée. La liste de référence des contrats Ouvrir* (12) est maintenue à un seul endroit : Navigation.

Persistance

SQLite (fichier vigiechiro.db dans l'espace de travail par défaut, relocalisable via l'onglet « Emplacements » des réglages, cf. ADR 1038), via des DAO écrits en PreparedStatement (pas d'ORM). Le schéma évolue par migrations versionnées src/main/resources/db/migration/V0x__*.sql, appliquées par MigrationSchema sur la SourceDeDonnees.

Le cœur du domaine est l'agrégat « nuit de capture » (feature passage), qui avance dans un workflow à états :

IMPORTE → TRANSFORME → VERIFIE → PRET_A_DEPOSER → DEPOT_EN_COURS → DEPOSE (7 états, cf. StatutWorkflow)

Le septième, RECUPERE, est hors de cette file : c'est une nuit que la synchronisation a rapatriée de Vigie-Chiro (#2581), donc qui n'a franchi aucune de ces étapes. Sa seule suite est DEPOSE, quand la réactivation lui rend son audio. Voir ADR 2581 pour la règle qui distingue un état observé d'un état déclaré.

Le détail des entités, des tables et de la correspondance avec le MCD du brief est sur Modèle de données et domaine ; le mécanisme d'accès (DAO, transactions, migrations) sur Persistance.

Injection de dépendances

RacineInjecteur assemble le socle + tous les modules */di/*Module.java des features. Les Controller FXML sont eux aussi injectés via une controllerFactory posée sur le FXMLLoader. Certaines valeurs transverses sont fournies par binding nommé (ex. @Named("idUtilisateurCourant"), l'utilisateur courant de l'application mono-utilisateur).


Pour mettre tout cela en pratique, suivez Ajouter une fonctionnalité.