article banner

Effective Kotlin Item 45: Consider extracting non-essential parts of your API into extensions

This is a chapter from the book Effective Kotlin. You can find it on LeanPub or Amazon.

When we define final methods in a class, we need to decide whether we want to define them as members or as extension functions.

// Defining methods as members class Workshop(/*...*/) { //... fun makeEvent(date: DateTime): Event = //... val permalink get() = "/workshop/$name" }
// Defining methods as extensions class Workshop(/*...*/) { //... } fun Workshop.makeEvent(date: DateTime): Event = //... val Workshop.permalink get() = "/workshop/$name"

Both approaches are similar in many ways. Their use and even referencing them via reflection is very similar:

fun useWorkshop(workshop: Workshop) { val event = workshop.makeEvent(date) val permalink = workshop.permalink val makeEventRef = Workshop::makeEvent val permalinkPropRef = Workshop::permalink }

However, there are some significant differences between those two options. They both have their pros and cons, thus one way does not dominate over the other. Therefore, I suggest considering extracting non-essential parts of your API into extensions, not necessarily doing it. The point is to make smart decisions, and to do this we need to understand the differences between those two options.

The biggest difference between members and extensions in terms of use is that extensions need to be imported separately. For this reason, they can be located in a different package. This fact is used when we cannot add a member ourselves. It is also used in projects designed to separate data and behavior. Properties with fields need to be located in a class, but methods can be located separately as long as they only access the class’s public API.

Thanks to the fact that extensions need to be imported, we can have many extensions with the same name on the same type. This is good because different libraries can provide extra methods, therefore we won’t have a conflict. On the other hand, it would be dangerous to have two extensions with the same name but with different behavior. For such cases, we can cut the Gordian knot by making a member function. The compiler always chooses member functions over extensions1.

Another significant difference is that extensions are not virtual, meaning they cannot be redefined in derived classes. The extension function to call is selected statically during compilation. This is different behavior than member elements that are virtual in Kotlin. Therefore, we should not use extensions for elements that are designed for inheritance.

open class C class D : C() fun C.foo() = "c" fun D.foo() = "d" fun main() { val d = D() print(d.foo()) // d val c: C = d print(c.foo()) // c print(D().foo()) // d print((D() as C).foo()) // c }

This behavior is the result of the fact that extension functions under the hood are compiled into normal functions, where the extension’s receiver is placed as the first argument:

fun foo(`this$receiver`: C) = "c" fun foo(`this$receiver`: D) = "d" fun main() { val d = D() print(foo(d)) // d val c: C = d print(foo(c)) // c print(foo(D())) // d print(foo(D() as C)) // c }

Another consequence of this fact is that we define extensions on types, not on classes. This gives us more freedom. For instance, we can define an extension on a nullable or generic type:

inline fun CharSequence?.isNullOrBlank(): Boolean { contract { returns(false) implies (this@isNullOrBlank != null) } return this == null || this.isBlank() } public fun Iterable<Int>.sum(): Int { var sum: Int = 0 for (element in this) { sum += element } return sum }

The last important difference is that extensions are not listed as members in the class reference. This is why they are not considered by annotation processors, and we cannot extract elements that should be processed into extensions when we process a class using annotation processing. On the other hand, if we extract non-essential elements into extensions, we don’t need to worry about them being seen by these processors. We don’t need to hide them because they are not in the class anyway.

Let me show you two examples where defining extensions makes more sense than defining members. The first one is the Iterable interface and its many extensions like map or filter. These methods could be defined inside Iterable as members, but that would be a bad idea. They do not define the interface’s essential behavior but rather some utils that can be used on iterable objects. Thanks to the fact that these methods are extensions, the Iterable interface is clean and easy to understand.

// Kotlin stdlib interface Iterable<out T> { operator fun iterator(): Iterator<T> } public inline fun <T, R> Iterable<T>.map( transform: (T) -> R ): List<R> { // ... } public inline fun <T> Iterable<T>.filter( predicate: (T) -> Boolean ): List<T> { // ... }

Another example is a conversion function between two classes that represent a similar abstraction but on different layers of our application, such as a domain class Product and a data layer class ProductJson. The conversion functions toProduct and toProductJson could be members, but we generally prefer to define them as extensions. This way, we can keep our domain classes clean and free of data layer dependencies. This also lets us keep both these conversion functions next to each other, which makes them easier to maintain.

fun ProductJson.toProduct() = Product( id = this.id, title = this.title, imgSrc = this.img, description = this.desc, price = BigDecimal(this.price), type = enumValueOf<ProductType>(this.type) ) fun Product.toProductJson() = ProductJson( id = this.id, title = this.title, img = this.imgSrc, desc = this.description, price = this.price.toString(), type = this.type.name )

Summary

The most important differences between members and extensions are:

  • Extensions need to be imported
  • Extensions are not virtual
  • Members have higher priority
  • Extensions are on a type, not on a class
  • Extensions are not listed in the class reference

To summarize this, extensions give us more freedom and flexibility. However, they do not support inheritance or annotation processing, and it might be confusing that they are not present in the class they are called on. The essential parts of our API should generally be members, but there are good reasons to extract non-essential parts of your API as extensions.

1:

The only exception is when an extension in the Kotlin stdlib has kotlin.internal.HidesMembers internal annotation.