1. Valeurs codées en dur et dette technique
Si vous pensez encore « bon, une chaîne "EMAIL" dans une méthode, ce n'est pas la fin du monde », c'est normal : sur un petit projet, on a l'impression que c'est ainsi qu'il faut faire. Mais ce sont précisément ces petits détails qui transforment plus tard l'application en quête « trouve où ça a changé », dont le boss final est Ctrl+Shift+F dans le dépôt. Voyons pourquoi les valeurs codées en dur sont une impasse dès le niveau Spring Core, même si nous n'avons ni Spring Boot, ni web, ni base de données.
Commençons par un exemple simple et un peu douloureux. Imaginez que nous écrivons des notifications de commande et que nous décidons « provisoirement » : par défaut, le canal est EMAIL et le nombre de tentatives est 3. Cela semble inoffensif jusqu'au moment où quelqu'un dit « et si en mode démo on mettait SMS » ou « sur CI, il ne faut rien envoyer du tout ».
public class NotificationDispatchService {
public void dispatch(String message) {
// Mal : les réglages sont cachés à l'intérieur de la méthode métier et mélangés à la logique.
// De l'extérieur de la classe, on ne voit pas que ces valeurs existent et sont importantes.
String defaultChannel = "EMAIL";
int retryLimit = 3;
// Pour l'exemple, on se contente d'imprimer un envoi "comme si".
System.out.println(defaultChannel + " -> " + message + ", retries=" + retryLimit);
// EMAIL -> Order created, retries=3
}
}
Dans cette version, il y a deux problèmes, tous deux désagréables. Premièrement, le réglage est caché à l'intérieur de la méthode et mélangé à la logique – vous ne voyez même pas qu'il s'agit d'un « endroit important » avant de commencer à modifier le comportement. Deuxièmement, modifier la valeur signifie modifier le code, recompiler, redémarrer – c'est-à-dire changer le programme pour ce qui est essentiellement un paramètre de lancement.
Même si vous faites un pas en avant et extrayez la valeur dans un champ ou un constructeur, ce n'est toujours pas une configuration externe. Mais c'est déjà un pas important : nous cessons de cacher les réglages au milieu de la logique et commençons à les traiter comme des données qui viennent « de l'extérieur » de la classe. Dans Spring, cela s'intègre très naturellement avec l'idée de Environment : plus tard, les propriétés de différentes sources y afflueront, mais pour l'instant, nous apprenons simplement à ne pas les coder en dur dans les méthodes.
2. Dépendances et réglages
En général, un débutant dit « mon service a des dépendances » et il jette dans le même panier les réglages : chaînes, nombres, drapeaux de modes. C'est un peu comme mettre dans la même poche les clés de l'appartement et un café chaud : formellement, les deux sont « nécessaires », mais les conséquences du mélange sont soudainement dramatiques. Il est maintenant important de séparer ces concepts, car Spring fonctionne très bien avec les dépendances-objets, mais les réglages nécessitent une discipline distincte.
Essayons d'expliquer en langage humain.
Dépendance – c'est un autre objet avec lequel votre classe collabore. Par exemple, AuditWriter ou OrderStore. Vous pouvez remplacer l'implémentation, mais le sens reste : c'est un « partenaire de travail ».
Réglage – c'est une valeur qui modifie le comportement sans changer la structure de l'application. Par exemple, « où écrire le rapport » ou « quel canal de notifications considérer comme défaut ». C'est plus comme des « règles du bureau » : aujourd'hui nous travaillons à distance, demain au bureau ; les collègues sont les mêmes, le mode est différent.
Voici un exemple avec ContextFlow où la différence est assez évidente :
| Ce dont le service a besoin | Exemple | Pourquoi c'est ainsi |
|---|---|---|
| Dépendance (objet) | OrderStore, , |
C'est un comportement sous forme de code. Nous remplaçons l'implémentation, mais nous continuons à « faire de l'audit », « stocker les commandes », « envoyer des notifications ». |
| Réglage (valeur) | contextflow.app-name, contextflow.report.output-dir | Ce sont des paramètres d'environnement et de lancement. On souhaite les modifier sans toucher au code Java. |
Et voici une conclusion pratique importante. Les dépendances, nous les injectons en tant qu'objets (nous savons déjà le faire). Les réglages, nous les injectons aussi – mais en tant que valeurs, et il faut le faire de manière à ce qu'ils soient visibles dans le contrat de la classe (dans le constructeur), et non cachés au milieu d'une méthode.
Si on regarde plus largement, Spring regroupe ensuite ces valeurs dans Environment – un endroit unique où l'application peut chercher des réglages sans savoir d'avance s'ils viennent d'un fichier, d'une variable d'environnement ou d'arguments de lancement. Mais avant de les lire, il faut apprendre à distinguer un « objet » d'un « paramètre ».
3. Configuration dans @Configuration
Après la section précédente, on pourrait avoir une fausse impression : « OK, ne gardons pas la chaîne dans la méthode. Mettons-la dans la configuration – et le problème est réglé. » C'est effectivement une amélioration, mais pas la fin de l'histoire. C'est comme arrêter de cacher l'argent sous le matelas et commencer à le mettre dans une boîte « Argent » – c'est plus ordonné, mais la banque n'est toujours pas apparue.
Faisons cette étape intermédiaire correctement : les réglages ne doivent pas vivre à l'intérieur des méthodes, donc nous les transmettons via le constructeur, et l'endroit où nous les définissons, nous le déplaçons dans le composition root – c'est-à-dire dans la classe de configuration Spring.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CoreConfig {
@Bean
public NotificationDispatchService notificationDispatchService() {
// Composition root : ici on voit comment l'application est "assemblée" et quelles valeurs sont choisies.
// Mais ce n'est toujours PAS une configuration externe : pour changer EMAIL/3, il faut modifier le code Java.
return new NotificationDispatchService("EMAIL", 3);
}
}
D'un point de vue architectural, c'est nettement mieux. Maintenant, nous avons un point unique où l'on voit « comment l'application est assemblée » et quelles valeurs sont choisies. Cela aide déjà à lire le projet : pas besoin de sauter de service en service pour chercher où l'auteur a mis "EMAIL" « pour cinq minutes ».
Mais le problème persiste : toute modification nécessite encore de modifier le code Java. C'est-à-dire que vous publiez toujours un nouveau build pour ce qui est, par essence, un paramètre de lancement. En réalité, cela devient très vite douloureux : différents environnements veulent différentes valeurs. Et même si vous n'avez qu'un seul environnement (votre ordinateur portable), un deuxième apparaîtra un jour (l'ordinateur portable d'un collègue), un troisième (CI), un quatrième (machine de démo). Et là, les ennuis commencent.
4. Littéraux dangereux : chemins et environnement
Il existe une catégorie particulière de valeurs codées en dur qui brisent la portabilité de façon quasi garantie : tout ce qui est lié au système de fichiers, aux chemins, aux noms de répertoires, aux adresses absolues et « chez moi ça marche ».
Si vous avez déjà vu quelque chose comme "C:\\Users\\Ivan\\Desktop\\report.txt" – félicitations, vous connaissez déjà ce genre. Dans ContextFlow, nous avons au moins deux endroits où un chemin peut facilement être codé en dur « pour une minute » : la sortie des rapports et l'audit fichier.
public class ReportOutputManager {
private final String outputDir;
public ReportOutputManager() {
// Dangereux : un chemin absolu attache le code à une machine et à un utilisateur spécifiques.
// Dans un autre environnement, ce répertoire peut ne pas exister – et tout "tombe" soudainement.
this.outputDir = "C:\\Users\\Ivan\\Desktop\\reports"; // ça marchait chez Ivan
}
}
Le problème ici n'est pas qu'Ivan est une mauvaise personne (peut-être qu'Ivan est une personne formidable). Le problème est que le chemin « d'Ivan » ne fait pas partie de la logique métier de l'application. C'est une partie de l'environnement où l'application est exécutée. Et l'environnement change plus souvent qu'on ne le souhaite.
Dans notre cours, il y a une discipline supplémentaire : tous les fichiers doivent atterrir dans build/, pour que le projet soit reproductible et ne jonche pas l'ordinateur de l'étudiant. Cela signifie que même les valeurs « par défaut » doivent être conçues avec soin. Par exemple, build/reports ou build/contextflow/reports. Mais il est préférable de garder cela comme un réglage, car demain on pourrait vouloir un autre répertoire.
5. Ensemble de réglages ContextFlow
Nous allons maintenant faire un pas très pratique : définir quelles valeurs dans notre projet sont des réglages, et nous mettre d'accord sur leurs clés. L'essentiel est que les clés de configuration sont aussi un contrat, presque comme les noms des méthodes publiques. Si vous nommez mal une clé, vous souffrirez pas immédiatement, mais plus tard… mais vous souffrirez selon un calendrier.
Dans le cadre de ContextFlow, un ensemble qui affecte réellement le comportement et qui change naturellement entre les lancements nous suffit :
| Clé | Exemple de valeur | Où cela a un impact |
|---|---|---|
| contextflow.app-name | ContextFlow | Messages techniques, en-têtes de rapports, sortie de scénario |
| contextflow.notifications.default-channel | console | Quel canal considérer comme « par défaut » si le scénario ne précise pas |
| contextflow.audit.mode | console / file | Quelle stratégie d'audit est utilisée dans le lancement actuel |
| contextflow.report.output-dir | build/reports | Où déposer les rapports |
| contextflow.locale.default | ru / en | Locale de base pour les messages et modèles à l'avenir (pour l'instant simple chaîne) |
Pour les valeurs opérationnelles comme default-channel et audit.mode, il est pratique de les garder en minuscules : ainsi elles sont plus faciles à lire et à ne pas confondre avec les constantes Java.
Remarquez le style de nommage. Le préfixe contextflow. est notre « système de coordonnées ». Il résout deux problèmes à la fois : premièrement, les clés n'entrent pas en conflit avec celles des autres ; deuxièmement, lorsque vous ouvrez le fichier de configuration, vous voyez ce qui concerne notre application.
Et encore un détail. Nous aurions pu nommer les clés « plus courtes » – comme appName ou dir. Mais c'est presque toujours une mauvaise idée. Le fichier de configuration est lu par des humains (parfois fatigués), et les clés doivent aider, pas jouer aux devinettes.
Discipline des clés contextflow.*
Avant d'apprendre à lire réellement les propriétés depuis des sources externes, nous devons nous mettre d'accord sur une discipline des clés. Cela semble ennuyeux, mais en réalité, cela économise beaucoup de nerfs.
Premièrement, la clé doit être stable. Si aujourd'hui vous nommez la clé contextflow.report.output-dir et demain vous la renommez en contextflow.report.dir, vous cassez tous les environnements qui utilisaient l'ancienne clé. Les clés sont un contrat de lancement.
Deuxièmement, la clé doit être « auto-explicative ». Cela ne signifie pas qu'elle doit être longue comme un roman, mais elle doit être compréhensible sans une excursion dans le code. audit.mode est déjà mieux que mode. contextflow.audit.mode est encore mieux, car on voit à quoi cela se rapporte.
Troisièmement, vous devez prévoir les valeurs par défaut à l'avance. Certains réglages peuvent avoir une valeur « par défaut » (par exemple, ContextFlow comme nom d'application). D'autres sont mieux rendus obligatoires, afin que l'application échoue au démarrage si la valeur n'est pas fournie. Nous appelons cela un comportement fail-fast, et cela évite généralement les lancements incorrects « silencieux ».
Dans le cadre du cours, nous viserons une logique simple : pour un lancement d'apprentissage, nous avons des valeurs par défaut raisonnables, mais si un réglage est critique pour le comportement (par exemple, le chemin d'écriture du fichier), nous ne devons pas silencieusement tomber dans un fallback incompréhensible. Mieux vaut échouer avec une erreur claire au démarrage que d'écrire tranquillement des rapports « quelque part pas au bon endroit ».
6. Ce qu'il ne faut pas mettre dans la configuration
À ce stade, une autre extrême naît souvent : « Puisque le codage en dur est mauvais, mettons tout dans la configuration ». Et il est important de ne pas perdre le bon sens, car la configuration n'est pas un cimetière de toutes les constantes.
Le critère classique est le suivant. Si la valeur fait partie d'une règle métier qui définit le sens du système, elle reste généralement dans le code. Si la valeur est opérationnelle, un paramètre environnemental qui change d'un lancement à l'autre, on la sort à l'extérieur.
Par exemple, OrderStatus.NEW et OrderStatus.CANCELLED ne sont pas des réglages. Si vous mettez « NEW » dans le fichier properties, l'application ne deviendra pas « plus flexible » ; elle deviendra plus étrange. Il en va de même pour la structure du domaine elle-même : les champs de Order, la liste des items, les invariants – ce n'est pas de la configuration.
En revanche, le chemin de sortie du rapport, le mode d'audit « console ou fichier », le nom de l'application, le canal de notifications par défaut – ce sont des réglages typiques de l'environnement. Ils ne modifient pas le domaine métier, ils changent « comment l'application se comporte dans ce lancement particulier ».
Si une image vous aide, le code est la recette du plat, et la configuration est « combien de sel mettre aujourd'hui ». Si vous mettez dans la configuration la liste des ingrédients et l'ordre de la préparation, vous n'obtenez pas une recette, mais un générateur de chaos.
7. Réglages via le constructeur
Faisons maintenant ce que l'on peut et doit faire avant même d'apprendre à lire les réglages depuis des fichiers : réécrivons les classes pour que les réglages soient des paramètres du constructeur. C'est une habitude d'ingénierie importante. Elle rend le code plus honnête : si une classe a besoin d'une valeur, elle est visible immédiatement, sans fouilles archéologiques dans la méthode dispatch().
Voici le même NotificationDispatchService, mais dans une version correcte :
public class NotificationDispatchService {
private final String defaultChannel;
private final int retryLimit;
public NotificationDispatchService(String defaultChannel, int retryLimit) {
// Les réglages deviennent partie du contrat de la classe : ils sont visibles lors de la création de l'objet.
// Cela rend la classe plus honnête et plus facile à tester/réutiliser.
this.defaultChannel = defaultChannel;
this.retryLimit = retryLimit;
}
}
Oui, il n'y a pas encore de magie Spring ici. Et c'est bien. Nous n'essayons pas « tout de suite tout avec des annotations ». Nous faisons simplement en sorte que la classe soit normale : elle ne cache pas sa configuration.
À ce stade, vous obtenez déjà un petit bonus de testabilité. Vous pouvez créer un service avec différentes valeurs dans un test, sans réécrire le code de la classe et sans accéder aux champs privés par réflexion. Même si vous n'écrivez pas encore de tests – vous créez déjà un code qui n'est pas désagréable à tester.
Effet visuel : comportement différent sans modification du code
Il est utile de voir une fois l'« effet waouh » sur l'exemple le plus simple. Une même classe se comporte différemment parce que nous lui avons passé des paramètres différents. La classe ne sait pas d'où ils viennent – et c'est une conception idéale pour la configuration.
public class Demo {
public static void main(String[] args) {
// Même logique, mais des "paramètres de lancement" différents.
NotificationDispatchService prod = new NotificationDispatchService("EMAIL", 3);
NotificationDispatchService demo = new NotificationDispatchService("CONSOLE", 0);
// Pour la démonstration, on montre simplement que les configurations sont différentes.
System.out.println("Prod configuré"); // Prod configuré
System.out.println("Demo configuré"); // Demo configuré
}
}
Bien sûr, cet exemple n'« envoie pas encore de notifications » – nous ne voulons pas alourdir le code. L'essentiel est ailleurs : une configuration différente est déjà séparée de la logique de la classe. Ensuite, nous apprendrons simplement à Spring à fournir ces valeurs non pas depuis le code Java, mais de l'extérieur. Mais la étape fondamentale est déjà franchie : la classe est devenue configurable.
Et voici une pensée importante qui servira tous les jours suivants. Spring ne rend pas votre code magiquement configurable. Spring aide simplement à assembler l'application à partir de morceaux. Si les morceaux sont honnêtes dès le départ (dépendances et réglages visibles), le conteneur travaille pour vous. Si les morceaux « cachent » tout à l'intérieur des méthodes – le conteneur assemblera bien, mais la maintenance sera difficile.
8. Erreurs typiques lors du travail avec des valeurs codées en dur
Erreur n°1 : transformer n'importe quelle constante en réglage.
Souvent, après la première victoire sur les valeurs codées en dur, apparaît le désir de « tout externaliser ». Résultat : la configuration gonfle et commence à piloter le sens métier qui devrait vivre dans le code. Si la valeur décrit le domaine métier (statuts de commande, invariants, règles de cycle de vie), ce n'est pas un réglage d'environnement – c'est une partie de la logique.
Erreur n°2 : confondre dépendance et réglage.
Parfois, un développeur voit que le service a besoin de « quelque chose » et, au lieu d'injecter une dépendance-objet (par exemple, AuditWriter), il commence à injecter une chaîne "auditMode" et à faire à l'intérieur du service if("file") ... else .... Cela dégrade la conception : le service devient plus intelligent que le conteneur, et le wiring se répand dans le code métier.
Erreur n°3 : cacher les réglages à l'intérieur des méthodes.
La pire variante est lorsque la valeur vit directement à l'intérieur d'une méthode (surtout si elle affecte le comportement). Ces réglages se multiplient rapidement, sont difficiles à trouver et se mélangent à la logique. Si la valeur est vraiment un « réglage », elle doit être un paramètre du constructeur ou au moins un champ, et non une variable locale au milieu d'un scénario.
Erreur n°4 : chemins absolus et « ça marche chez moi ».
Dès qu'un chemin absolu apparaît dans le code, l'application devient liée à une machine spécifique. Même si vous utilisez un chemin « joli », il peut cesser d'exister à tout moment dans un autre environnement. Pour le cours – écrivons strictement les artefacts dans build/, et plus tard nous apprendrons à le faire via une configuration externe.
Erreur n°5 : mauvais noms de clés et absence de préfixe.
Les clés comme dir, mode, value1 semblent « pratiques » tant que le fichier est petit. Dès qu'il y a plus de réglages, ces clés transforment la configuration en devinette. Le préfixe contextflow.* et des noms pertinents – ce n'est pas de la bureaucratie, c'est une façon de garder le projet lisible.
GO TO FULL VERSION