Pourquoi la journalisation en Kotlin est un problème (et comment le résoudre)
Si vous avez déjà écrit en Java, vous connaissez ce rituel : au début de chaque classe, vous écrivez private static final Logger logger = LoggerFactory.getLogger(MyClass.class);. Ennuyeux, répétitif, mais ça fonctionne. En passant à Kotlin, on souhaite quelque chose de plus élégant — et c'est là que les choses deviennent intéressantes.
Sur Stack Overflow, la question "Idiomatic way of logging in Kotlin" a recueilli des milliers de vues et des dizaines de discussions. Il s'avère que même en Kotlin moderne, il n'existe pas une seule manière "correcte" de journaliser. Dans cet article, nous analyserons trois approches principales, comparerons leurs performances et leur lisibilité, et choisirons la manière idiomatique de journaliser en Kotlin qui conviendra à 90% des projets.
Le problème : Logger en Kotlin n'est pas si simple
Kotlin est un langage qui valorise la concision. Mais SLF4J (la bibliothèque de journalisation la plus populaire dans le monde Java/Kotlin) nécessite de spécifier explicitement la classe pour chaque logger. En Java, cela se résout avec Lombok ou les modèles de l'IDE. En Kotlin, cela se fait via les fonctionnalités du langage lui-même.
Nous sommes confrontés à plusieurs défis :
- Ne pas dupliquer le nom de la classe (source d'erreurs lors de la copie)
- Ne pas sacrifier les performances (la journalisation est une opération fréquente)
- Préserver la lisibilité et la concision du code
- Prendre en charge l'initialisation paresseuse du logger
Voyons quelles solutions la communauté Kotlin propose.
Approche 1 : "Brutale" — Companion Object
La manière la plus directe est de déclarer le logger dans le companion object de la classe.
class UserService { companion object { private val logger = LoggerFactory.getLogger(UserService::class.java) }
fun createUser(name: String) { logger.info("Creating user: $name") }}Avantages :
- Simplicité et clarté
- Ne nécessite pas de dépendances supplémentaires
Inconvénients :
- Indication manuelle de la classe — risque d'erreur
- Beaucoup de code passe-partout
- Ne convient pas aux interfaces et aux classes abstraites
Cette approche est la base à partir de laquelle toutes les autres partent. Elle fonctionne, mais ce n'est pas la manière idiomatique de journaliser en Kotlin — trop de code boilerplate.
Approche 2 : Fonctions d'extension — style Kotlin
Kotlin permet d'étendre les classes via des fonctions d'extension. Pourquoi ne pas créer une fonction qui retourne un logger pour n'importe quelle classe ?
inline fun <reified T : Any> T.logger(): Logger { return LoggerFactory.getLogger(T::class.java)}
class OrderService { private val logger = logger()
fun processOrder(id: String) { logger.debug("Processing order: $id") }}Ici, nous utilisons un paramètre de type reified dans une fonction inline. Cela permet d'obtenir la classe à l'exécution sans réflexion.
Avantages :
- Concis :
private val logger = logger() - Sûr : la classe est récupérée automatiquement
- La fonction inline ne crée pas d'objets superflus
Inconvénients :
- Le logger est initialisé immédiatement à la création de l'objet, pas de manière paresseuse
- Ne convient pas aux fonctions de niveau supérieur (hors classe)
C'est déjà plus proche d'une solution idiomatique. Mais on peut faire encore mieux.
Approche 3 : Propriétés déléguées — l'élégance de Kotlin
Kotlin prend en charge les propriétés déléguées. Nous pouvons créer notre propre délégué qui initialisera paresseusement le logger et le mettra en cache.
class LoggerDelegate<in T : Any> : ReadOnlyProperty<T, Logger> { private var logger: Logger? = null
override fun getValue(thisRef: T, property: KPropert