A Discussion On How Kotlin Extension Functions Can Be Used

Güneş Maboçoğlu
4 min readDec 1, 2020

The extension functions should have a task related to the responsibility of the respective class. One of the most common mistakes made by developers is to write extension functions like a utility function. Here is an example to explain this better:

fun String.validateEmail(): Boolean {
//validation process
return result
}

These types of functions should not be defined as extension functions. Storing the e-mail information as String does not mean that String class is associated with the e-mail information. Instead this validation should reside in a class like EmailValidator.

class EmailValidator {
fun validate(email: String): Boolean {
//validation process
return result
}
}

fun CharSequence.IsEmpty(): Boolean function defined in the Kotlin library is a good example for use of Kotlin extension function. Actually CharSequence interface is a special version of the char array. Judging from this aspect, isEmpty function would be a natural member of the CharSequence class.

At this point I will go a little beyond the purpose of the article and ask the following question: How should we interpret two functions below?

fun <C, R> C.ifEmpty(defaultValue: () -> R): R where C : CharSequence, C : R = ....class ArrayList {
@Override
public void forEach(Consumer<? super E> action) {...}
}

CharSequence and ArrayList classes can be considered as different varieties of the array data structure. The functionalities above, forEach and ifEmpty, do not belong to these classes. We can see this in the clearest in forEach function. Neither the consumer class nor the loop type nor the operations of what to do with list items are directly related to the array data structure. How to define such functions should be questioned more.

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 beclass 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.

--

--