Using Kotlin Extension Functions: The Good, the Bad, and the Ugly

Written by aksenov | Published 2022/02/12
Tech Story Tags: kotlin | kotlin-extensions | extension-methods | clean-code | kotlin-best-practives | kotlin-extension-functions | programming | kotlin-tutorial

TLDRExtension functions in Kotlin allow you to natively implement the "decorator" pattern. They let you 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. Let's see how complex they can get and how to use them the right way.via the TL;DR App

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.

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

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

What are Kotlin extension functions?

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.

Let see how this works:

data class Account(
    val id: Long,
    var amount: BigDecimal
)

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

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

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

private static final String getFormattedAmount(Account $this$getFormattedAmount) {
   return "Account " + $this$getFormattedAmount.getId() + " stores " + $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.


//________third-party opened__________________________
// some complex class that store a lot of inner fields.
data class Client(
    val personalInfo: ClientPersonalInfo
)

data class ClientPersonalInfo(
    val address: ClientAddress
)

data class ClientAddress(
    val city: String
)
//________third-party closed__________________________

// some extension
fun 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.getCity() // with extension
}

  1. To improve any API (even your own) but in private scope.

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:

// Bad!
fun OwnClass.extension() = //some logic
    
// Good!    
private fun OwnClass.extension() = //some logic    

  1. To convert your own class to other.

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:

// util class converters

fun OwnClass.asOtherOwnClass(): OtherOwnClass = // convert logic
fun OwnClass1.asOtherOwnClass1(): OtherOwnClass1 = // convert logic
fun OwnClass2.asOtherOwnClass2(): OtherOwnClass2 = // convert logic

Potentially Dangerous:

  1. Public converter extension with with “to” prefix.

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.

  1. Complex extension with third-party APIs calling.

fun OwnClass.asOwnClass2(): OwnClass2 {
    val metaField1 = thirdPartyClient.getMetaField1()
    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.

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.

Sometimes you want to extend some logic for every class.

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

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.

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.

data class OwnClass(val id: Long, // other fields) {
    
    // public method is good!
    fun someMethod(): String {
        // some logic
    }
}
        
// extension is very bad!
fun OwnClass.someMethod(): String = //some logic 

  1. Complex extensions with a large block of logic.

fun OwnClass.someMethod(someParam: Boolean):  {
    val metaField1 = thirdPartyClient.getMetaField1()
    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!

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.


Featured Image by: https://unsplash.com/@altumcode


Written by aksenov | Java/Kotlin backend developer on Spring/Ktor. SQL writer, unit-test implementer
Published by HackerNoon on 2022/02/12