Injection (Guice)¶
Toutes les dépendances sont câblées par Guice 7 : services, DAO, ViewModels et même les
controllers FXML. Aucun new métier dispersé dans le code.
La racine de composition¶
RacineInjecteur
assemble le graphe : le socle (CommunModule + PersistenceModule), installé explicitement,
et les modules de feature, auto-découverts via ServiceLoader<ModuleDeFeature>.
public static List<Module> modules() {
List<Module> modules = new ArrayList<>();
modules.add(new CommunModule()); // socle : toujours explicite
modules.add(new PersistenceModule());
Predicate<ModuleDeFeature> actif = Fonctionnalites.filtreActives(); // feature-flags
ServiceLoader.load(ModuleDeFeature.class) // features : découvertes
.stream().map(ServiceLoader.Provider::get)
.filter(actif) // features désactivées écartées
.sorted(Comparator.comparing(m -> m.getClass().getName())) // ordre déterministe
.forEach(modules::add);
return List.copyOf(modules);
}
Ajouter une feature ne touche donc plus la racine : il suffit d'un XxxModule extends
ModuleDeFeature déclaré comme service. Deux déclarations, gardées synchronisées par
DecouverteModulesTest (qui lit le module-info.class pour les comparer) :
- classpath (tests surefire
useModulePath=false, fat-jar/Launcher) :src/main/resources/META-INF/services/fr.univ_amu.iut.commun.di.ModuleDeFeature; - module-path (
javafx:run) :uses+provides … with …dansmodule-info.java.
L'ordre d'installation n'a aucun effet fonctionnel (les Set des points d'extension sont retriés
par ordre() côté chrome, OptionalBinder.setBinding l'emporte quel que soit l'ordre) ; le tri par
nom de classe garantit seulement la reproductibilité. Une feature peut être désactivée
(feature-flag) : voir Feature-flags ci-dessous.
Pourquoi commun.di peut dépendre des features
Une racine de composition connaît tout le monde : c'est son rôle. Le test ArchUnit
features_sans_cycle exclut explicitement commun/di/ de la détection de cycles. Depuis
l'auto-découverte, RacineInjecteur n'importe d'ailleurs plus aucun module de feature.
La CLI utilise un injecteur enfant
La feature cli ne s'installe pas dans la racine : elle crée un injecteur enfant
(RacineInjecteur.creer().createChildInjector(new CliModule())). L'enfant hérite de tout le
graphe et y ajoute ses aides : voir Interface en ligne de commande (CLI).
Feature-flags¶
Chaque ModuleDeFeature déclare son identité via fonctionnalite() :
Fonctionnalite(id, libellé,Categorie).
La catégorie décide de la désactivabilité :
| Catégorie | Désactivable ? | Défaut | Pour… |
|---|---|---|---|
COEUR |
non (garde-fou) | active | feature socle, ou feuille load-bearing (une autre feature/écran en dépend) |
OPTIONNELLE |
oui | active | feature autonome, activée par défaut |
EXPERIMENTALE |
oui | inactive | feature en cours de dev, mergée derrière un flag OFF |
Le registre Fonctionnalites
résout l'état actif de chaque feature, consulté par RacineInjecteur.modules() à la composition
(donc au démarrage : changer un flag prend effet au prochain lancement). Précédence, de la plus
forte à la plus faible :
- propriété système
-Dvigiechiro.features.<id>=on|off(override CI/dev) ; - alias rétro-compatible
-Dvigiechiro.features.desactivees=<NomClasseSimple>,…; - flag persisté
feature.<id>.activedansapp_setting, lu en pré-bootstrap (avant l'injecteur, sans créer de base, tolérant à une base absente) et posé par l'onglet « Fonctionnalités » de l'écran Réglages ; - défaut de la catégorie.
Garde-fou : une feature COEUR ne se désactive pas
Le registre ignore toute tentative de couper une feature COEUR (par flag ou alias) : la
retirer casserait l'injecteur (dépendance EAGER) ou un écran (contrat Ouvrir* consommé).
DecouverteModulesTest vérifie que désactiver toute feuille exposée laisse l'injecteur
constructible.
Sont OPTIONNELLE (désactivables) : import-vigiechiro (aucun Ouvrir*, OptionalBinder vide dès
l'origine) et les 6 feuilles autrefois couplées au runtime dont le contrat Ouvrir* a été
neutralisé (OptionalBinder vide côté consommateur + setBinding côté feuille, le consommateur
masquant son point d'entrée si absent) : diagnostic, lot, qualification, importation, analyse,
recherche (#1087). Le reste demeure COEUR : sites, passage, validation, audio,
bibliotheque, multisite, connexion, synchronisation-participation, depot-vigiechiro
(dépendances EAGER ; cf. Ajouter une fonctionnalité).
Ce que publie un module de feature¶
Un module de feature hérite de ModuleDeFeature
(lui-même un AbstractModule), qui ajoute un petit DSL de contribution masquant le boilerplate des
Multibinder. Sur le patron de
PassageModule :
public class PassageModule extends ModuleDeFeature {
@Override protected void configure() {
bind(OuvrirPassage.class).to(NavigationPassage.class); // contrat socle -> impl feature
indicateur(IndicateurPassages.class); // contribution à l'accueil (DSL)
}
@Provides @Singleton PassageDao passageDao(SourceDeDonnees s) { return new PassageDao(s); }
// ... autres @Provides ...
}
Mécanismes à retenir :
@Provides @Singletonassemble les DAO à partir de laSourceDeDonnees(singleton du socle). Les DAO eux-mêmes restent sans annotation d'injection : la couchemodel.daoignore Guice (objectif réutilisation O6).bind(Contrat).to(Impl)branche un contrat de navigationOuvrir*du socle sur l'implémentation de la feature (cf. Navigation).- Le DSL de
ModuleDeFeature(activite(...),indicateur(...),ongletReglages(...),actionMenu(...)) laisse une feature contribuer aux quatre points d'extension que le socle agrège sans connaître les contributeurs :
| Helper | Point d'extension | Le socle en fait… |
|---|---|---|
activite(X) |
ActiviteAccueil |
une carte sur l'accueil |
indicateur(X) |
IndicateurAccueil |
un compteur du tableau de bord |
ongletReglages(X) |
OngletReglages |
un onglet de l'écran Réglages |
actionMenu(X) |
ActionMenu |
une entrée du menu ☰ |
Chaque helper encapsule un Multibinder.newSetBinder(binder(), …).addBinding().to(X). Les points
non couverts (ex. RapprochementVigieChiro, un OptionalBinder) restent exprimés directement.
Des controllers FXML injectés¶
C'est la clé du câblage Vue↔ViewModel.
App
pose une controllerFactory sur le FXMLLoader : chaque controller est alors instancié par
Guice (injection par constructeur), donc reçoit ses ViewModels/services.
sequenceDiagram
participant App
participant Guice as Injecteur Guice
participant Loader as FXMLLoader
participant Ctrl as MainController
App->>Guice: RacineInjecteur.creer()
App->>App: MigrationSchema.migrer()
App->>Loader: new FXMLLoader(MainView.fxml)
App->>Loader: setControllerFactory(injector::getInstance)
App->>Loader: load()
Loader->>Guice: getInstance(MainController.class)
Guice-->>Ctrl: new MainController(NavigationViewModel, activités, ...)
Loader-->>App: vue prête (controller injecté)
Toute classe de navigation (Navigation*) réutilise ce patron : loader.setControllerFactory(injector::getInstance)
avant loader.load(), pour que le controller de l'écran ouvert soit injecté lui aussi.
Valeurs transverses : bindings nommés¶
Certaines valeurs partagées sont fournies par binding nommé. Exemple :
@Named("idUtilisateurCourant") (application mono-utilisateur : le premier utilisateur en base).
Un VM/service la reçoit par @Inject ... @Named("idUtilisateurCourant") String idUtilisateur.
Défaut d'injection surchargeable (@ImplementedBy)¶
Un contrat du socle peut porter une implémentation par défaut via @ImplementedBy(Defaut.class)
posé sur l'interface : l'injecteur l'utilise tant qu'aucun module ne lie explicitement ce contrat.
La racine de composition surcharge ce défaut pour la production, tandis que les tests isolés
récupèrent le défaut sans configuration. Le motif garde les tests déterministes et sans réseau :
le défaut est neutre, l'application branche la variante réelle.
Exemple, la fonctionnalité « Fiche de l'espèce » (#844) :
Contrat (commun) |
Défaut @ImplementedBy (tests) |
Surcharge production (CommunModule) |
|---|---|---|
SourceUniverselle |
LienGbif |
SourceUniversellePreferee (préférence GBIF / Wikipédia) |
ResolveurFiche |
ResolveurFicheIdentite (aucun réseau) |
ResolveurFicheGbif (résout la clé via l'API GBIF) |
ExecuteurFiche |
ExecuteurFicheSynchrone (déterministe) |
ExecuteurFicheAsynchrone (hors fil JavaFX + Platform.runLater) |
ExecuteurTache (#793) |
ExecuteurTacheSynchrone (déterministe) |
ExecuteurTacheAsynchrone (thread virtuel + Platform.runLater) |
Une surcharge explicite (bind(...).to(...) ou @Provides) l'emporte toujours sur le défaut. Les
tests E2E l'exploitent via Modules.override(RacineInjecteur.modules()).with(...) pour injecter un faux
ciblé (ex. un OuvreurDeLien qui enregistre l'URL au lieu d'ouvrir un navigateur), sans dupliquer la
liste des modules. Les outils de capture font de même avec ModuleCaptureCommun : les exécuteurs
y sont synchrones, sinon l'aperçu montre le voile d'occupation à la place du contenu (#1278).
Pour assembler une feature complète de bout en bout, voir Ajouter une fonctionnalité.