Написать пост

Что такое функции расширения Kotlin и где их правильно применять?

Аватарка пользователя Viacheslav Aksenov

Поговорим о функциях расширения Kotlin. А также рассмотрим случаи, когда их использование может помочь, а когда сделает код сложнее.

Меня зовут Аксёнов Вячеслав, я профессиональный Java/Kotlin бэкенд разработчик в крупном энерпрайзе. В моей зоне ответственности – проектирование систем из набора сервисов и выстраивание сложной бизнес логики. Также время от времени я пишу свои пет проекты, некоторые из которых вы можете найти в моем github:

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

Чем являются функции расширения Kotlin на самом деле

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

Рассмотрим пример:

			data class Book(
    val id: Long,
    val author: String,
    val title: String,
    val price: BigDecimal
)

class SomeService {

  fun analyzeBook(book: Book) {
    val formattedInfo = book.getFormattedInfo()
    ....
  }

  private fun Book.getFormattedInfo(): String = "Book $author - $title has price - $price"
}
		

Обычный dto класс Book, у которого есть некий набор полей. И есть класс SomeService, в котором есть публичный метод для анализа книги. Предполагаем, что для анализа книги требуется форматированные данные из dto. Однако класс закрыт для расширения, возможно он лежит в сторонней библиотеке. С помощью функции расширения можно написать метод, который будет словно бы принадлежать данному классу. При том обратите внимание, что на этот метод можно повесить любой модификатор доступа — в данном случае private.

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

			private static final String getFormattedAmount(Book $this$getFormattedInfo) {
   return "Book " + $this$getFormattedAmount.getAuthor() + " - " + $this$getFormattedAmount.getTitle() + " has price - " + $this$getFormattedAmount.getPrice();
}
		

Таким образом, на собственном опыте мы увидели, что функции расширения для JVM являются статическими final методами. Но у каждого инструмента есть спектр задач, для решения которых он подходит лучше всего. К примеру забивать гвозди бутербродом будет очень некомфортно, бутерброды нужно кушать. Давайте определимся, в каких задачах функции расширения показывают себя лучше всего и действительно облегчают жизнь.

Расширение API класса, который вам не принадлежит

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

			// чужой сложный класс
data class Bank(
    val bankInfo: BankInfo,
    ....
)

data class BankInfo(
    val bankAddressInfo: BankAddressInfo
    ....
)

data class BankAddressInfo(
    val city: String,
    .....
)
// конец сложного класса

// функция расширения
fun Bank.getCity() = bankInfo.bankAddressInfo.city

// сравнение
fun example(client: Client) {
    val address = client.personalInfo.address.city // без расширения
    val address = client.getCity() // с расширением
}
		

Конвертация моделей из одной в другую

Потенциально опасное использование, но если держать в голове следующие правила, то очень даже удобное.

			// файл с утилитами

fun OwnClass.asOtherOwnClass(): OtherOwnClass = // логика конвертации
fun OwnClass1.asOtherOwnClass1(): OtherOwnClass1 = // логика конвертации
fun OwnClass2.asOtherOwnClass2(): OtherOwnClass2 = // логика конвертации
		

Обратите внимание на нейминг функций. Следует их начинать с непопулярных названий методов, либо делать конвертеры приватными в области видимости только тех мест, где они нужны. Иначе можно замусорить весь контекст проекта. Взгляните на утрированный пример:

Что такое функции расширения Kotlin и где их правильно применять? 1

Расширение возможностей любого класса, но в ограниченной области видимости

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

			private fun OwnClass.asOwnClass2(): OwnClass2 {
    val metaField1 = thirdPartyClient.getMetaField1()
    val metaField2 = thirdPartyClient.getMetaField2()
    // ...
}
		

В данном примере внутри функции расширения Kotlin используется поле сервиса — thirdPartyClient, из которого получаются некие мета данные. Это вполне допустимое использование функции расширения, но следует быть предельно осторожным если вы помещаете внутрь такого метода любое внешнее поле! Для этого должна быть веская причина!

Расширение функционала дженериков

Да, Kotln позволяет делать даже такое, и иногда это действительно полезно. К примеру вам может потребоваться расширить API всех наследников некоего класса, но без добавления метода в этот класс.

			fun <T> T.someMethod() = {
  // некая логика
}
		

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

Для чего никогда нельзя применять функции расширения

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

  • Первое. Бесконтрольное расширение любого функционала вместо написание обычного метода на глобальном уровне очень сильно засорит контекст IDE и станет большой болью в поиске всех мест, где был расширен какой-то класс.
  • Второе. Если размер функции расширения Kotlin превышает 5 строк, значит настало время писать обычный метод.
			fun OwnClass.someMethod(someParam: Boolean):  {
    val metaField1 = thirdPartyClient.getMetaField1()
    val metaField2 = thirdPartyClient.getMetaField2()
    val metaField2 = thirdPartyClient.getMetaField2()
    if (someParam) {
        val metaField = ....
        // ...
    }
    // ...
}
		

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

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

В заключение.

Функции расширения — удобный синтаксический сахар Kotlin, который позволяет использовать язык практически как угодно. Однако будьте бдительны и применяйте их только там, где они действительно упростят жизнь. Эти методы должны быть маленькими и решающими конкретную задачу, никто не намерен сделать cmd+click по методу и провалиться в ад с огромным количеством бизнес-логики.

Используйте их как вспомогательный инструмент и не злоупотребляйте их мощью ?

Следите за новыми постами
Следите за новыми постами по любимым темам
3К открытий3К показов