Enregistrement idiomatique à Kotlin

Enregistrement idiomatique à Kotlin

1. introduction

Dans ce didacticiel, nous allons examiner quelques idiomes de journalisation qui correspondent aux styles de programmation typiques deKotlin.

2. Logging idiomes

La journalisation est un besoin omniprésent en programmation. Bien qu’apparemment une idée simple (il suffit d’imprimer des choses!), Il existe de nombreuses façons de le faire.

En fait, chaque langue, système d'exploitation et environnement possède sa propre solution de journalisation idiomatique et parfois idiosyncratique; souvent, en fait, plus d'un.

Ici, nous allons nous concentrer sur l'histoire de l'exploitation forestière de Kotlin.

Nous utiliserons également la journalisation comme prétexte pour plonger dans certaines fonctionnalités avancées de Kotlin et explorer leurs nuances.

3. Installer

Pour les exemples de code, nous utiliserons la bibliothèqueSLF4J, mais les mêmes modèles et solutions s'appliquent àLog4J,JUL et à d'autres bibliothèques de journalisation.

Alors, commençons par inclure les dépendancesSLF4J API etLogback dans notre pom:


    org.slf4j
    slf4j-api
    1.7.25


    ch.qos.logback
    logback-classic
    1.2.3


    ch.qos.logback
    logback-core
    1.2.3

Voyons maintenant à quoi ressemble la journalisation pour quatre approches différentes:

  • Une propriété

  • Un objet compagnon

  • Une méthode d'extension, et

  • Une propriété déléguée

4. Logger en tant que propriété

La première chose à faire est de déclarer une propriété d’enregistreur chaque fois que nous en avons besoin:

class Property {
    private val logger = LoggerFactory.getLogger(javaClass)

    //...
}

Ici, nous avons utiliséjavaClass pour calculer dynamiquement le nom de l'enregistreur à partir du nom de classe définissant. Nous pouvons donc facilement copier et coller cet extrait partout où nous voulons.

Ensuite, nous pouvons utiliser le logger dans n’importe quelle méthode de la classe déclarante:

fun log(s: String) {
    logger.info(s)
}

Nous avons choisi de déclarer le logger commeprivate car nous ne voulons pas que d’autres classes, y compris des sous-classes, y aient accès et se connectent au nom de notre classe.

Of course, this is merely a hint pour les programmeurs plutôt qu'une règle fortement appliquée, car il est facile d'obtenir un enregistreur avec le même nom.

4.1. Enregistrer un peu de frappe

Nous pourrions raccourcir un peu notre code en factorisant l'appelgetLogger à une fonction:

fun getLogger(forClass: Class<*>): Logger =
  LoggerFactory.getLogger(forClass)

Et en plaçant ceci dans une classe utilitaire,we can now simply call getLogger(javaClass) instead of LoggerFactory.getLogger(javaClass) throughout the samples below.

5. Logger dans un objet compagnon

Bien que le dernier exemple soit puissant par sa simplicité, ce n’est pas le plus efficace.

Tout d’abord, conserver une référence à un enregistreur dans chaque instance de classe coûte de la mémoire. Deuxièmement, même si les enregistreurs sont mis en cache, nous continuerons d'effectuer une recherche dans le cache pour chaque instance d'objet disposant d'un enregistreur.

Voyons si les objets compagnons s'en tirent mieux.

5.1. Une première tentative

En Java, déclarer le logger commestatic est un modèle qui répond aux préoccupations ci-dessus.

Dans Kotlin, cependant, nous n'avons pas de propriétés statiques.

Mais nous pouvons les émuler avechttps://www.example.com/kotlin-objects:

class LoggerInCompanionObject {
    companion object {
        private val loggerWithExplicitClass
          = getLogger(LoggerInCompanionObject::class.java)
    }

    //...
}

Remarquez comment nous avons réutilisé la fonction de commodité degetLoggerde la section 4.1. Nous continuerons à y faire référence tout au long de l'article.

Donc, avec le code ci-dessus, nous pouvons utiliser le consignateur exactement comme avant, dans n’importe quelle méthode de la classe:

fun log(s: String) {
    loggerWithExplicitClass.info(s)
}

5.2. Qu'est-il arrivé àjavaClass?

Malheureusement, l'approche ci-dessus présente un inconvénient. Parce que nous faisons directement référence à la classe englobante:

LoggerInCompanionObject::class.java

nous avons perdu la facilité du copier-coller.

But why not just use javaClass like before? En fait, nous ne pouvons pas. Si nous l'avions fait, nous aurions mal obtenu un enregistreur nommé d'après la classethe companion object’s:

//Incorrect!
class LoggerInCompanionObject {
    companion object {
        private val loggerWithWrongClass = getLogger(javaClass)
    }
}
//...
loggerWithWrongClass.info("test")

Ce qui précède produirait un nom de logger légèrement incorrect. Jetez un œil au bit$Companion:

21:46:36.377 [main] INFO
com.example.kotlin.logging.LoggerInCompanionObject$Companion - test

En fait,IntelliJ IDEA marks the declaration of the logger with a warning, car il reconnaît que la référence àjavaClass dans un objet compagnon n'est probablement pas ce que nous voulons.

5.3. Dérivation du nom de classe avec réflexion

Pourtant, tout n'est pas perdu.

Nous avons un moyen de dériver automatiquement le nom de la classe et de restaurer notre capacité à copier et coller le code, mais nous avons besoin d'un élément de réflexion supplémentaire pour le faire.

Tout d'abord, assurons-nous d'avoir la dépendancekotlin-reflect dans notre pom:


    org.jetbrains.kotlin
    kotlin-reflect
    1.2.51

Ensuite, nous pouvons obtenir dynamiquement le nom de classe correct pour la journalisation:

companion object {
    @Suppress("JAVA_CLASS_ON_COMPANION")
    private val logger = getLogger(javaClass.enclosingClass)
}
//...
logger.info("I feel good!")

Nous allons maintenant obtenir le résultat correct:

10:00:32.840 [main] INFO
com.example.kotlin.logging.LoggerInCompanionObject - I feel good!

The reason we use enclosingClass découle du fait que les objets compagnons, en fin de compte, sont des instances de classes internes, doncenclosingClass e réfère à la classe externe, ou dans ce cas,LoggerInCompanionObject.

De plus, nous pouvons maintenant supprimer l’avertissement qu’IntelliJ IDEA donne surjavaClass puisque nous faisons ce qu’il faut avec lui.

5.4. @JvmStatic

Alors que les propriétés des objets compagnonslookressemblent à des champs statiques, les objets compagnons ressemblent davantage à des singletons.

Les objets compagnons Kotlin ont cependant une fonction spéciale, au moins lorsqu'ils sont exécutés sur une JVM, queconverts companion objects to static fields:

@JvmStatic
private val logger = getLogger(javaClass.enclosingClass)

5.5. Mettre tous ensemble

Mettons les trois améliorations ensemble. Lorsqu'elles sont jointes, ces améliorations rendent notre structure de journalisation statique, compatible avec la copie:

class LoggerInCompanionObject {
    companion object {
        @Suppress("JAVA_CLASS_ON_COMPANION")
        @JvmStatic
        private val logger = getLogger(javaClass.enclosingClass)
    }

    fun log(s: String) {
        logger.info(s)
    }
}

6. Enregistreur à partir d'une méthode d'extension

Bien qu’intéressant et efficace, l’utilisation d’un objet compagnon est prolixe. Ce qui a commencé comme une ligne est maintenant composé de plusieurs lignes à copier-coller dans la base de code.

De plus, l'utilisation d'objets compagnons produit des classes internes supplémentaires. Comparé à la déclaration simple de l’enregistreur statique en Java, l’utilisation d’objets associés est plus lourde.

Alors, essayons une approche utilisantextension methods.

6.1. Une première tentative

L'idée de base est de définir une méthode d'extension qui renvoie unLogger, afin que chaque classe qui en a besoin puisse simplement appeler la méthode et obtenir l'instance correcte.

Nous pouvons définir cela n'importe où sur le classpath:

fun  T.logger(): Logger = getLogger(javaClass)

Les méthodes d’extension sont copiées dans toutes les classes auxquelles elles s’appliquent; ainsi, nous pouvons simplement nous référer directement àjavaClass again.

Et maintenant, toutes les classes auront la méthodelogger comme si elle avait été définie dans le type:

class LoggerAsExtensionOnAny { // implied ": Any"
    fun log(s: String) {
        logger().info(s)
    }
}

While this approach is more concise than companion objects, nous pourrions vouloir aplanir d'abord certains problèmes.

6.2. Pollution du typeAny

Un inconvénient important de notre première méthode d'extension est qu'elle pollue le typeAny.

Comme nous l'avons défini comme s'appliquant à n'importe quel type, cela finit par être un peu envahissant:

"foo".logger().info("uh-oh!")
// Sample output:
// 13:19:07.826 [main] INFO java.lang.String - uh-oh!

En définissantlogger() surAny, nous avonspolluted tous les types dans le langage avec la méthode.

Ce n’est pas nécessairement un problème. Cela n’empêche pas les autres classes d’avoir leurs propres méthodeslogger.

Cependant, mis à part le bruit supplémentaire,it also breaks encapsulation. Les types peuvent désormais se connecter les uns aux autres, ce que nous ne voulons pas.

Etlogger will apparaît maintenant sur presque toutes les suggestions de code IDE.

6.3. Méthode d'extension sur une interface de marqueur

We can narrow our extension method’s scope with a markerinterface:

interface Logging

Une fois cette interface définie, nous pouvons indiquer que notre méthode d’extension s’applique uniquement aux types qui implémentent cette interface:

fun  T.logger(): Logger = getLogger(javaClass)

Et maintenant, si nous changeons notre type pour implémenterLogging, nous pouvons utiliserlogger comme avant:

class LoggerAsExtensionOnMarkerInterface : Logging {
    fun log(s: String) {
        logger().info(s)
    }
}

6.4. Paramètre de type réifié

Dans les deux derniers exemples, nous avons utilisé la réflexion pour obtenir le sablejavaClass donnant un nom distinctif à notre enregistreur.

Cependant, nous pouvons également extraire ces informations du paramètre de typeT, évitant ainsi un appel de réflexion lors de l'exécution. Pour ce faire, nous allons déclarer la fonction commeinline andreify the type parameter:

inline fun  T.logger(): Logger =
  getLogger(T::class.java)

Notez que cela change la sémantique du code en ce qui concerne l'héritage. Nous en discuterons en détail dans la section 8.

6.5. Combinaison avec les propriétés de l'enregistreur

Une bonne chose à propos des méthodes d'extension est que nous pouvons les combiner avec notre première approche:

val logger = logger()

6.6. Combinaison avec des objets compagnons

Mais l'histoire est plus complexe si l'on veutuse our extension method in a companion object:

companion object : Logging {
    val logger = logger()
}

Parce que nous aurions le même problème avecjavaClass as avant:

com.example.kotlin.logging.LoggerAsExtensionOnMarkerInterface$Companion

Pour en tenir compte, définissons d'abord une méthode qui obtient la classe de manière plus robuste:

inline fun  getClassForLogging(javaClass: Class): Class<*> {
    return javaClass.enclosingClass?.takeIf {
        it.kotlin.companionObject?.java == javaClass
    } ?: javaClass
}

Ici,getClassForLogging renvoie leenclosingClass ifjavaClass fait référence à un objet compagnon.

Et maintenant, nous pouvons à nouveau mettre à jour notre méthode d'extension:

inline fun  T.logger(): Logger
  = getLogger(getClassForLogging(T::class.java))

De cette façon, nous pouvons en fait utiliser la même méthode d'extension que le logger soit inclus en tant que propriété ou objet compagnon.

7. Logger en tant que propriété déléguée

Enfin, regardonsdelhttps: //www.example.com/kotlin-delegated-properties [egated]properties.

Ce qui est bien avec cette approche, c'est quewe avoid namespace pollution without requiring a marker interface:

class LoggerDelegate : ReadOnlyProperty {
    override fun getValue(thisRef: R, property: KProperty<*>)
     = getLogger(getClassForLogging(thisRef.javaClass))
}

On peut alors l'utiliser avec une propriété:

private val logger by LoggerDelegate()

En raison degetClassForLogging, cela fonctionne également pour les objets compagnons:

companion object {
    val logger by LoggerDelegate()
}

Et bien que les propriétés déléguées soient puissantes, notez quegetValue is re-computed each time the property is read.

Aussi, nous devons nous rappeler quedelegate properties must use reflection pour que cela fonctionne.

8. Quelques remarques sur l'héritage

Il est très courant d’avoir un enregistreur par classe. Et c’est pourquoi nous déclarons généralement les enregistreurs commeprivate.

Cependant, il y a des moments où nous voulons que nos sous-classes se réfèrent à l'enregistreur de leur superclasse.

Et selon notre cas d'utilisation, les quatre approches ci-dessus se comporteront différemment.

En général, lorsque nous utilisons la réflexion ou d'autres fonctionnalités dynamiques, nous sélectionnons la classe réelle de l'objet au moment de l'exécution.

Mais, lorsque nous nous référons statiquement à une classe ou à un paramètre de type réifié par son nom, la valeur sera fixée au moment de la compilation.

Par exemple, avec les propriétés déléguées, puisque l'instance de journalisation est obtenue dynamiquement à chaque fois que la propriété est lue, elle prendra le nom de la classe où elle est utilisée:

open class LoggerAsPropertyDelegate {
    protected val logger by LoggerDelegate()
    //...
}

class DelegateSubclass : LoggerAsPropertyDelegate() {
    fun show() {
        logger.info("look!")
    }
}

Regardons le résultat:

09:23:33.093 [main] INFO
com.example.kotlin.logging.DelegateSubclass - look!

Même silogger est déclaré dans la superclasse, il imprime le nom de la sous-classe.

La même chose se produit lorsqu'un enregistreur est déclaré en tant que propriété et instancié à l'aide dejavaClass.

Etextension methods exhibit this behavior, too, unless we reify the type parameter.

Inversement,with reified generics, explicit class names and companion objects, a logger’s name stays the same across the type hierarchy.

9. Conclusions

Dans cet article, nous avons examiné plusieurs techniques Kotlin que nous pouvons appliquer à la tâche de déclaration et d'instanciation des loggers.

Commençant simplement, nous avons progressivement accru la complexité lors d’une série de tentatives visant à améliorer l’efficacité et à réduire le passe-partout, en examinant les objets compagnon Kotlin, les méthodes d’extension et les propriétés déléguées.

Comme toujours, ces exemples sont disponibles enover on GitHub complets.