Идиоматическое ведение журнала в Котлине

Идиоматическое ведение журнала в Котлине

1. Вступление

В этом руководстве мы рассмотрим несколько идиом журналирования, которые соответствуют типичным стилям программированияKotlin.

2. Регистрация идиом

Ведение журнала является повсеместной потребностью в программировании. Хотя это простая идея (просто напечатать материал!), Есть много способов сделать это.

Фактически, каждый язык, операционная система и среда имеют свое собственное идиоматическое, а иногда и своеобразное решение для ведения журнала; часто, фактически, больше чем один.

Здесь мы сосредоточимся на истории регистрации Kotlin.

Мы также будем использовать ведение журнала как предлог для погружения в некоторые расширенные функции Kotlin и изучения их нюансов.

3. Настроить

Для примеров кода мы будем использовать библиотекуSLF4J, но те же шаблоны и решения применимы кLog4J,JUL и другим библиотекам журналирования.

Итак, давайте начнем с включения зависимостейSLF4J API иLogback в наш pom:


    org.slf4j
    slf4j-api
    1.7.25


    ch.qos.logback
    logback-classic
    1.2.3


    ch.qos.logback
    logback-core
    1.2.3

Теперь давайте посмотрим, как выглядит ведение журнала для четырех разных подходов:

  • Недвижимость

  • Сопутствующий объект

  • Метод расширения и

  • Делегированное имущество

4. Регистратор как собственность

Первое, что мы можем попробовать - объявить свойство logger везде, где оно нам нужно:

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

    //...
}

Здесь мы использовалиjavaClass для динамического вычисления имени регистратора из имени определяющего класса. Таким образом, мы можем легко скопировать и вставить этот фрагмент в любое место.

Затем мы можем использовать регистратор в любом методе декларирующего класса:

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

Мы решили объявить регистратор какprivate, потому что мы не хотим, чтобы другие классы, включая подклассы, имели к нему доступ и регистрировались от имени нашего класса.

Of course, this is merely a hint для программистов, а не строго соблюдаемое правило, поскольку легко получить регистратор с тем же именем.

4.1. Сохранение набора текста

Мы могли бы немного сократить наш код, факторизовав вызов функцииgetLogger:

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

И поместив это в служебный класс,we can now simply call getLogger(javaClass) instead of LoggerFactory.getLogger(javaClass) throughout the samples below.

5. Регистратор в сопутствующем объекте

Хотя последний пример является мощным по своей простоте, он не самый эффективный.

Во-первых, для хранения ссылки на регистратор в каждом экземпляре класса требуется память. Во-вторых, даже если регистраторы кэшированы, мы все равно будем выполнять поиск в кеше для каждого экземпляра объекта, имеющего регистратор.

Посмотрим, будут ли дела с объектами-компаньонами лучше.

5.1. Первая попытка

В Java объявление регистратора какstatic - это шаблон, который решает указанные выше проблемы.

Однако в Kotlin у нас нет статических свойств.

Но мы можем имитировать их с помощьюhttps://www.example.com/kotlin-objects:

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

    //...
}

Обратите внимание, как мы повторно использовали вспомогательную функциюgetLogger из раздела 4.1. Мы будем ссылаться на него на протяжении всей статьи.

Итак, с помощью приведенного выше кода мы можем снова использовать регистратор точно так же, как и раньше, в любом методе класса:

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

5.2. Что случилось сjavaClass?

К сожалению, вышеупомянутый подход имеет недостаток. Потому что мы прямо ссылаемся на включающий класс:

LoggerInCompanionObject::class.java

мы потеряли легкость копирования и вставки.

But why not just use javaClass like before? На самом деле мы не можем. Если бы это было так, мы бы неправильно получили логгер с именем классаthe companion object’s:

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

Выше будет выводить немного неправильное имя регистратора. Взгляните на бит$Companion:

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

Фактически,IntelliJ IDEA marks the declaration of the logger with a warning,, потому что он распознает, что ссылка наjavaClass в сопутствующем объекте, вероятно, не то, что нам нужно.

5.3. Получение имени класса с отражением

Тем не менее, не все потеряно.

У нас есть способ получить имя класса автоматически и восстановить способность копировать и вставлять код, но для этого нам понадобится дополнительная часть размышлений.

Во-первых, давайте убедимся, что у нас есть зависимостьkotlin-reflect в нашем pom:


    org.jetbrains.kotlin
    kotlin-reflect
    1.2.51

Затем мы можем динамически получить правильное имя класса для ведения журнала:

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

Теперь мы получим правильный результат:

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

The reason we use enclosingClass объясняется тем фактом, что сопутствующие объекты, в конце концов, являются экземплярами внутренних классов, поэтомуenclosingClass  относится к внешнему классу или, в данном случае,LoggerInCompanionObject.

Кроме того, теперь мы можем подавить предупреждение, которое IntelliJ IDEA выдает наjavaClass, поскольку теперь мы поступаем с ним правильно.

5.4. @JvmStatic

В то время как свойства сопутствующих объектовlook похожи на статические поля, сопутствующие объекты больше похожи на одиночные объекты.

Однако у сопутствующих объектов Kotlin есть особенность, по крайней мере, при работе на JVM, чтоconverts companion objects to static fields:

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

5.5. Собираем все вместе

Давайте объединим все три улучшения. При объединении эти улучшения делают нашу конструкцию логирования копируемой и статической:

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

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

6. Регистратор из метода расширения

В то время как интересный и эффективный, использование сопутствующего объекта является многословным. То, что начиналось как однострочный, теперь состоит из нескольких строк для копирования-вставки по всей базе кода.

Кроме того, использование сопутствующих объектов создает дополнительные внутренние классы. По сравнению с простым объявлением статического логгера в Java использование сопутствующих объектов более сложное.

Итак, давайте попробуем подход с использованиемextension methods.

6.1. Первая попытка

Основная идея состоит в том, чтобы определить метод расширения, который возвращаетLogger,, чтобы каждый класс, которому он нужен, мог просто вызвать метод и получить правильный экземпляр.

Мы можем определить это в любом месте на пути к классам:

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

Методы расширения обычно копируются в любой класс, к которому они применимы; так что мы можем просто сослаться непосредственно наjavaClass again.

И теперь у всех классов будет методlogger, как если бы он был определен в типе:

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

While this approach is more concise than companion objects,, мы могли бы сначала решить с ним некоторые проблемы.

6.2. Загрязнение типаAny

Существенным недостатком нашего первого метода расширения является то, что он загрязняет типAny.

Поскольку мы определили его как применимый к любому типу, он становится немного инвазивным:

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

Определяяlogger() дляAny, мы получаемpolluted всех типов на языке с методом.

Это не обязательно проблема. Это не мешает другим классам иметь свои собственные методыlogger.

Однако, не считая лишнего шума,it also breaks encapsulation. Типы теперь могут регистрироваться друг для друга, что нам не нужно.

Иlogger will теперь появляется почти при каждом предложении кода IDE.

6.3. Метод расширения на интерфейсе маркера

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

interface Logging

Определив этот интерфейс, мы можем указать, что наш метод расширения применяется только к типам, которые реализуют этот интерфейс:

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

И теперь, если мы изменим наш тип для реализацииLogging, мы сможем использоватьlogger, как раньше:

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

6.4. Параметр Reified Type

В последних двух примерах мы использовали отражение, чтобы получитьjavaClass and, чтобы дать отличительное имя нашему регистратору.

Однако мы также можем извлечь такую ​​информацию из параметра типаT, избегая вызова отражения во время выполнения. Для этого мы объявим функцию какinline andreify the type parameter:

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

Обратите внимание, что это меняет семантику кода в отношении наследования. Мы обсудим это подробно в разделе 8.

6.5. Комбинирование со свойствами регистратора

Хорошая вещь о методах расширения состоит в том, что мы можем объединить это с нашим первым подходом:

val logger = logger()

6.6. Объединение с сопутствующими объектами

Но история будет более сложной, если мы хотимuse our extension method in a companion object:

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

Потому что раньше у нас была такая же проблема сjavaClass as:

com.example.kotlin.logging.LoggerAsExtensionOnMarkerInterface$Companion

Чтобы учесть это, давайте сначала определим метод, который более надежно получает класс:

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

ЗдесьgetClassForLogging возвращаетenclosingClass ifjavaClass относится к сопутствующему объекту.

И теперь мы можем снова обновить наш метод расширения:

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

Таким образом, мы можем фактически использовать один и тот же метод расширения независимо от того, включен ли регистратор как свойство или как сопутствующий объект.

7. Регистратор как делегированное свойство

Наконец, давайте посмотрим наdelhttps: //www.example.com/kotlin-delegated-properties [egated]properties.

Что хорошо в этом подходе, так это то, чтоwe avoid namespace pollution without requiring a marker interface:

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

Затем мы можем использовать его со свойством:

private val logger by LoggerDelegate()

Из-заgetClassForLogging это работает и для сопутствующих объектов:

companion object {
    val logger by LoggerDelegate()
}

И хотя делегированные свойства являются мощными, обратите внимание, чтоgetValue is re-computed each time the property is read.

Также мы должны помнить, чтоdelegate properties must use reflection, чтобы он работал.

8. Несколько замечаний о наследовании

Очень типично иметь по одному регистратору на класс. Вот почему мы также обычно объявляем регистраторы какprivate.

Однако бывают случаи, когда мы хотим, чтобы наши подклассы ссылались на регистратор их суперкласса.

И в зависимости от нашего варианта использования вышеупомянутые четыре подхода будут вести себя по-разному.

В общем, когда мы используем отражение или другие динамические объекты, мы выбираем фактический класс объекта во время выполнения.

Но когда мы статически ссылаемся на класс или параметр типа reified по имени, значение будет зафиксировано во время компиляции.

Например, с делегированными свойствами, поскольку экземпляр средства ведения журнала получается динамически каждый раз при чтении свойства, он будет принимать имя класса, в котором он используется:

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

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

Посмотрим на результат:

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

Несмотря на то, чтоlogger объявлен в суперклассе, он печатает имя подкласса.

То же самое происходит, когда средство ведения журнала объявляется как свойство и создается с использованиемjavaClass.

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

Наоборот,with reified generics, explicit class names and companion objects, a logger’s name stays the same across the type hierarchy.

9. Выводы

В этой статье мы рассмотрели несколько методов Kotlin, которые можно применить к задаче объявления и создания экземпляров логгеров.

Начнем с того, что мы постепенно увеличивали сложность в серии попыток повысить эффективность и уменьшить шаблон, взглянув на сопутствующие объекты Kotlin, методы расширения и делегированные свойства.

Как всегда, эти примеры доступны в полныхover on GitHub.

Related