A Discussion On How Kotlin Extension Functions Can Be Used
By: Güneş Maboçoğlu
Tags: Object Oriented Design, Object Oriented, Kotlin Extension, Design Patterns, Kotlin

Kotlin extension functions can be defined in a kotlin file or as a member of a class. Extension functions defined in a kotlin file could be used any where in the project, we can call this global scope. On the other hand, the extension functions defined inside a class can only be called from the scope of its class. To call these functions from another class, run or with functions can be used.

So, what is the difference between defining extension functions within global scope and defining them within a class scope? To explain the difference between them without touching upon extension functions defined within global scope, I will just try to explain the one defined within class scope first. I will be using home brewing as an example to explain this part. Home brewers already know it but for those who have no idea let me you some idea about it. For fermentation the yeast must be kept for a period of time in an environment with the appropriate temperature adjusted for its type. This can be coded as below by using extension function.

class Refrigerator {

fun start() {}

fun stop() {}

} class RefrigeratorBeerScope {

fun Refrigerator.adjustSuitableTemperatureForYeast() {

//...

}

} class Application {

fun main() {

//In this section, the refrigerator works traditionally

val refrigerator = Refrigerator()

refrigerator.start()

refrigerator.stop()

with (RefrigeratorBeerScope()) {

//At this point, the refrigerator is now working for yeast fermentation.

refrigerator.adjustSuitableTemperatureForYeast()

}

//RefrigeratorBeerScope is ends, and adjustTemperatureToKeepYeastSuitableTemperature extension function cannot be called any longer.

refrigerator.adjustTemperatureToKeepYeastSuitableTemperature()//this code cannot be compiled

}

}

What I want to explain with this example is that I have added new behavior to the refrigerator for a certain purpose. When the brewing process is over, we have achieved our goal. From this point on, the refrigerator can continue to be used traditionally.

Now, I want to continue with forEach function which I mentioned above.

class CollectionIterationScope {

fun <E> List<E>.forEach(consumer: Consumer<E>) { ... } fun <E> List<E>.iterator(): Iterator<E> { ... } fun <E> Set<E>.forEach(consumer: Consumer<E>) { ... }

...

} ...

val list = ArrayList()

with(CollectionIterationScope()) {

list.forEach { ... }

... }

...

Of course, this type of use will be a bit of a challenge. Most of the developers will choose to use the forEach method instead. What I want to talk about using the extension function defined in the class is not how easy it is to implement, but how it makes it easy to group functions successfully.

Let’s continue with another example:

class CreditCardUtil {

fun getCreditCardType(creditCardNumber: String): CreditCardType

fun isCardNumberValid(creditCardNumber: String): Boolean

...

} or could be class StringCreditCardNumberScope {

fun String.getCreditCardType(): CreditCardType

fun String.isValidCardNumber(): Boolean

....

}

I know these examples are not good enough. I just try to give a new perspective and start a discussion. What I want to touch on is that some classes may need to gain new behaviors or new attributes under certain conditions. Let’s continue with Collection.kt interface, it extends Iterable.kt interface. Of course, there is nothing strange about this. But let’s consider what would happen if the Collection.kt interface did not extend the Iterable.kt interface. In this case can we say that Collection.kt interface is no longer a collection? Can different implementations of Iterable.kt interface be not grouped in any other way? How would it be if CollectionIterationScope.kt was used instead? On the other hand, there are some attributes that make a difference. For example, when add method is added to List.kt interface, it turns into MutableList.kt interface.

fun addEmail() {

val emails = mutableListOf<String>()

with(MutableCollectionAsyncScope()) {

emails.asyncAdd("")

}

} class MutableCollectionAsyncScope {

fun <E> MutableCollection<E>.asyncAdd(element: E) {

Executors.newSingleThreadExecutor().execute {

add(element)

}

}

}

The same functionality could also be achieved using proxy pattern.(like AsyncCollection.kt class). However there is a conceptual difference between them. The proxy pattern is combining collection and asynchronously working attributes. If I have to summarize, most developers, while constructing a class, combine the main defining features that characterize the class with the secondary defining features that may be actually related to another class. I’m not saying that is wrong, I want to start a discussion on how to design better classes, better architectures and better patterns using kotlin extension functions.

I would like to thank Gizem Burhanoglu for giving me the idea for this article.