Использование функций расширения Kotlin: хорошее, плохое и уродливое

Использование функций расширения Kotlin: хорошее, плохое и уродливое

13 февраля 2022 г.

My name is Viacheslav Aksenov. I am a professional Java/Kotlin backend developer in one of the largest Russian Fintech companies. I am responsible for designing and developing microservices for internal employees.

Меня зовут Вячеслав Аксенов. Я профессиональный бэкенд-разработчик Java/Kotlin в одной из крупнейших российских финтех-компаний. Я отвечаю за проектирование и разработку микросервисов для внутренних сотрудников.


Also, sometimes I write small pet projects. You can find some of them on my GitHub.

Кроме того, иногда я пишу небольшие домашние проекты. Вы можете найти некоторые из них на моем GitHub.


In this article, I want to explain how to use the Kotlin extension functions the right way.

В этой статье я хочу объяснить, как правильно использовать функции расширения Kotlin.


What are Kotlin extension functions?

Что такое функции расширения Kotlin?


Extension functions in Kotlin allow you to natively implement the "decorator" pattern. For example, they allow you to write new functions for a class from a third-party library that you can't modify. Such functions can be called in the usual way as if they were methods of the original class.

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


Let see how this works:

Давайте посмотрим, как это работает:


```javascript

```javascript


data class Account(

учетная запись класса данных (


val id: Long,

действительный идентификатор: длинный,


var amount: BigDecimal

количество переменных: BigDecimal


private fun Account.getFormattedAmount(): String = "Account $id stores $amount"

private fun Account.getFormattedAmount(): String = "Учетная запись $id хранит $amount"


There is an Account data class, and getFormattedAmount is an extension that returns formatted data from the account.

Существует класс данных Account, а getFormattedAmount — это расширение, которое возвращает отформатированные данные из аккаунта.


If we compile this Kotlin code and decompile it to Java we see that extension is regular static method:

Если мы скомпилируем этот код Kotlin и декомпилируем его в Java, мы увидим, что расширение является обычным статическим методом:


```java

```java


private static final String getFormattedAmount(Account $this$getFormattedAmount) {

private static final String getFormattedAmount(Account $this$getFormattedAmount) {


return "Account " + $this$getFormattedAmount.getId() + " stores " + $this$getFormattedAmount.getAmount();

return «Учетная запись» + $this$getFormattedAmount.getId() + «сохраняет» + $this$getFormattedAmount.getAmount();


That was a pretty simple example of extensions. Let's see how complex they can get and how to use them the right way.

Это был довольно простой пример расширений. Давайте посмотрим, насколько сложными они могут быть и как правильно их использовать.


The Good:

Добро:


  1. To improve API of a third-party class, that cannot be modified in any other way.
  1. Для улучшения API стороннего класса, который нельзя изменить каким-либо другим способом.

```javascript

```javascript


//__third-party opened________

//__открыл стороннее________


// some complex class that store a lot of inner fields.

// какой-то сложный класс, в котором хранится множество внутренних полей.


data class Client(

клиент класса данных (


val personalInfo: ClientPersonalInfo

val персональная информация: ClientPersonalInfo


data class ClientPersonalInfo(

класс данных ClientPersonalInfo(


val address: ClientAddress

val адрес: ClientAddress


data class ClientAddress(

класс данных ClientAddress(


val city: String

вал город: Строка


//__third-party closed________

//_стороннее закрытое_____


// some extension

// некоторое расширение


fun Client.getCity() = personalInfo.address.city

весело Client.getCity() = personalInfo.address.city


// need to get just client address from on of inner fields

// нужно получить только адрес клиента из одного из внутренних полей


fun example(client: Client) {

забавный пример (клиент: Клиент) {


val address = client.personalInfo.address.city // without extension

val address = client.personalInfo.address.city // без расширения


val address = client.getCity() // with extension

val address = client.getCity() // с расширением


  1. To improve any API (even your own) but in private scope.
  1. Чтобы улучшить любой API (даже ваш собственный), но в частном порядке.

To use your own class extension in a single class, you must make the extension private! Because in this case, it is equivalent to a private method that should be available inside a single class.

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


Example:

Пример:


```javascript

```javascript


// Bad!

// Плохой!


fun OwnClass.extension() = //some logic

fun OwnClass.extension() = // немного логики


// Good!

// Хорошо!


private fun OwnClass.extension() = //some logic

приватное развлечение OwnClass.extension() = //некоторая логика


  1. To convert your own class to other.
  1. Чтобы преобразовать свой класс в другой.

It’s allowed to write some util class with a lot of extensions that convert your class to any else. But it would be good to use some unpopular prefixes for it:

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


```javascript

```javascript


// util class converters

// использовать преобразователи классов


fun OwnClass.asOtherOwnClass(): OtherOwnClass = // convert logic

fun OwnClass.asOtherOwnClass(): OtherOwnClass = // логика преобразования


fun OwnClass1.asOtherOwnClass1(): OtherOwnClass1 = // convert logic

fun OwnClass1.asOtherOwnClass1(): OtherOwnClass1 = // логика преобразования


fun OwnClass2.asOtherOwnClass2(): OtherOwnClass2 = // convert logic

fun OwnClass2.asOtherOwnClass2(): OtherOwnClass2 = // логика преобразования


Potentially Dangerous:

Потенциально опасно:


  1. Public converter extension with with “to” prefix.
  1. Расширение общедоступного конвертера с префиксом «to».

It's very easy to relax and start writing your own class-to-other class converters using the "to" prefix. But the danger of this way is that wherever you use this class - Intellij Idea will have garbage in the context.

Очень легко расслабиться и начать писать свои собственные преобразователи классов в другие, используя префикс "to". Но опасность этого пути в том, что где бы вы ни использовали этот класс — у Intellij Idea будет мусор в контексте.



  1. Complex extension with third-party APIs calling.
  1. Сложное расширение с вызовом сторонних API.

```javascript

```javascript


fun OwnClass.asOwnClass2(): OwnClass2 {

весело OwnClass.asOwnClass2(): OwnClass2 {


val metaField1 = thirdPartyClient.getMetaField1()

val metaField1 = ThirdPartyClient.getMetaField1()


val metaField2 = thirdPartyClient.getMetaField2()

val metaField2 = ThirdPartyClient.getMetaField2()


// ...

}

In this way, there is an extension with a signature that tells everybody from outside that this function just converts some class to another. But inside there is a lot of calls third-party APIs. It can be ambiguous for anybody who reads the code where this extension is using.

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


Sometimes similar extensions are necessary, and it’s ok. But never make them public and try to avoid them if it’s possible. The private method is better.

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


  1. Extending APIs of generic classes.
  1. Расширение API универсальных классов.

Sometimes you want to extend some logic for every class.

Иногда вы хотите расширить некоторую логику для каждого класса.


```javascript

```javascript


fun T.someMethod() = "//some logic"

fun T.someMethod() = "//некоторая логика"


If the name of the method is very general, or the generic is very general, then the scope of useful actions that this method can do tends to zero, and the context pollution will be very dramatic.

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


Bad practices:

Плохая практика:


  1. An uncontrolled extension of your own class's API at the global level.
  1. Неконтролируемое расширение API вашего собственного класса на глобальном уровне.

If there is a need for functionality that your class should provide globally. This should be done through the public methods of this class. Extensions are not suitable for this task! Instead of useful work, they will only litter the IDE context.

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


```javascript

```javascript


data class OwnClass(val id: Long, // other fields) {

класс данных OwnClass(valid id: Long, // другие поля) {


// public method is good!

// публичный метод — это хорошо!


fun someMethod(): String {

забавный метод(): строка {


    // some logic

// немного логики


}

// extension is very bad!

// расширение очень плохое!


fun OwnClass.someMethod(): String = //some logic

fun OwnClass.someMethod(): String = //некоторая логика


  1. Complex extensions with a large block of logic.
  1. Сложные расширения с большим блоком логики.

```javascript

```javascript


fun OwnClass.someMethod(someParam: Boolean): {

весело OwnClass.someMethod(someParam: Boolean): {


val metaField1 = thirdPartyClient.getMetaField1()

val metaField1 = ThirdPartyClient.getMetaField1()


val metaField2 = thirdPartyClient.getMetaField2()

val metaField2 = ThirdPartyClient.getMetaField2()


val metaField2 = thirdPartyClient.getMetaField2()

val metaField2 = ThirdPartyClient.getMetaField2()


if (someParam) {

если (какой-то параметр) {


    val metaField = ....

Вал метаПоле = ....


    // ...
}
// ...

}

If you write extensions in this way, it will become impossible to understand them after a very short time in the code. Breaking the logic will become illogical and very difficult, and it will become impossible to quickly refine someplace. A regular private method would be much better for this task. Keep your extensions small and simple!

Если вы будете писать расширения таким образом, то через очень короткое время в коде их станет невозможно понять. Ломать логику станет нелогично и очень сложно, а где-то быстро доработать станет невозможно. Обычный частный метод был бы намного лучше для этой задачи. Пусть ваши расширения будут маленькими и простыми!


:::warning

:::предупреждение


Never change any global settings via extensions! Extensions must not be scoped beyond themselves. Otherwise, you will not track the sources of changes in your code in any way!

Никогда не меняйте глобальные настройки через расширения! Расширения не должны выходить за рамки самих себя. Иначе вы никак не отследите источники изменений в вашем коде!


Conclusion:

Вывод:


Extension functions allow you to place almost anything in them (like any other method), but you need to do this very carefully. Because the extension function is supposed to be compact and limited in scope, and no one expects to see big business logic in these functions.

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


The best thing to do is to use them as a helper tool to modify third-party APIs to suit your needs. And all large pieces of business logic should be written in ordinary private methods. Otherwise, there is a risk of getting bogged down in spaghetti code, which will be impossible to refactor.

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


Рекомендуемое изображение: https://unsplash.com/@altumcode



Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE