null

Использование Котлин-делегатов для реализации fallback-сценариев

А в чем проблема?

В разработке бизнес-логики для интеграции различных информационных систем часто необходимо учитывать fallback-сценарии, при которых для данных одной системы может не найтись соответствия в другой. Иными словами, при обработке запроса другой системы возникает необходимость подставить некий объект-заглушку, если в базе данных нашей системы не нашлось нужного соответствия типов. 
Так как объект, по сути, является константой (соответствующая ему запись в БД не меняется), то обращаться к БД каждый раз, когда нам нужно подставить заглушку, нерационально.

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

private val stubDocumentTypeId: Long
        get() = getDocumentTypeIdByExternalId(
            DocumentType.STUB_EXTERNAL_ID
        )

private fun getDocumentTypeIdByExternalId(documentTypeExternalId: UUID) =
        documentTypeRepository.getByExternalId(documentTypeExternalId)?.id
            ?: throw DocumentException(
                "DocumentType with external id $documentTypeExternalId not found"
            )


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

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

Вот как можно изменить код, чтобы использовать lazy:
 

private val stubDocumentTypeId: Long by lazy {
    getDocumentTypeIdByExternalId(
        DocumentType.STUB_EXTERNAL_ID
    )
}

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

Пару слов про Lazy-делегаты

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

Основные особенности lazy-делегатов:

  1. Отложенная инициализация: Значение свойства инициализируется только при первом доступе к нему.
  2. Кэширование: Значение вычисляется только один раз и сохраняется для последующего использования.
  3. Безопасность при многопоточности: По умолчанию lazy-делегаты безопасны для использования в многопоточных окружениях.

Синтаксис и использование

Для создания lazy-делегата используется функция lazy { ... }, которая принимает лямбда-выражение с инициализационным кодом:

val myLazyValue: String by lazy {
    println("Initializing...")
    "Hello, World!"
}

fun main() {
    println("Before accessing myLazyValue")
    println(myLazyValue)  // В первый раз будет напечатано "Initializing..."
    println(myLazyValue)  // Во второй раз "Initializing..." не будет напечатано, так как значение уже инициализировано
}

Параметры инициализации

Функция lazy имеет три возможных режима инициализации:

  1. SYNCHRONIZED (по умолчанию): Инициализация безопасна для многопоточных окружений. Используется synchronized для обеспечения потокобезопасности.

    val myLazyValue: String by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
        "Hello, World!"
    }
    
  2. PUBLICATION: Значение может быть инициализировано в нескольких потоках, но первый инициализатор выиграет, и его значение будет использовано. 

    val myLazyValue: String by lazy(LazyThreadSafetyMode.PUBLICATION) { "Hello, World!" }
  3. NONE: Не обеспечивает потокобезопасность. Подходит для использования в однопоточных окружениях.

    val myLazyValue: String by lazy(LazyThreadSafetyMode.NONE) { "Hello, World!" }

Еще немного примеров использования

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

Ленивое чтение конфигурационного файла

val config: Properties by lazy {
    val properties = Properties()
    properties.load(FileInputStream("config.properties"))
    properties
}

fun main() {
    println(config.getProperty("someKey"))
}

Ленивое создание сложного объекта

class MyClass {
    val complexObject: ComplexObject by lazy {
        println("Creating ComplexObject...")
        ComplexObject()
    }
}

fun main() {
    val myClass = MyClass()
    println("Before accessing complexObject")
    println(myClass.complexObject)  // В первый раз будет напечатано "Creating ComplexObject..."
    println(myClass.complexObject)  // Во второй раз объект уже будет создан
}

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