article banner

Effective Kotlin Item 46: Avoid member extensions

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

When we define an extension function to some class, it is not added to this class as a member. An extension function is just a different kind of function that we call on the first argument that is there, which is called a receiver. Under the hood, extension functions are compiled to normal functions, and the receiver is placed as the first parameter. For instance, the following function:

fun String.isPhoneNumber(): Boolean = length == 7 && all { it.isDigit() }

Under the hood is compiled to a function similar to this one:

fun isPhoneNumber(`$this$: String): Boolean = $this$.length == 7 && $this`.all { it.isDigit() }

One of the consequences of how extension functions are implemented is that we can have member extensions or even define extensions in interfaces:

interface PhoneBook { fun String.isPhoneNumber(): Boolean } class Fizz : PhoneBook { override fun String.isPhoneNumber(): Boolean = this.length == 7 && this.all { it.isDigit() } }

Even though it is possible, there are good reasons to avoid defining member extensions (except for DSLs). Especially, do not define extensions as members just to restrict visibility.

class PhoneBookIncorrect { fun verify(number: String): Boolean { require(number.isPhoneNumber()) // ... } // Bad practice, do not do this fun String.isPhoneNumber(): Boolean = this.length == 7 && this.all { it.isDigit() } }

One big reason is that member extension functions do not really restrict visibility. They only make it more complicated to use the extension function, since the user would need to provide both the extension and the dispatch receivers:

PhoneBookIncorrect().apply { "1234567890".isPhoneNumber() }

You should restrict an extension’s visibility by using a visibility modifier, not by making it a member.

class PhoneBook { fun verify(number: String): Boolean { require(number.isPhoneNumber()) // ... } // ... } // This is how we limit extension functions visibility private fun String.isPhoneNumber(): Boolean = this.length == 7 && this.all { it.isDigit() }

If you need a function to be a member, for instance when it needs to use a class state, and you want to call it like an extension, consider using let.

class PhoneBook( private val phoneNumberVerifier: PhoneNumberVerifier ) { fun verify(number: String): Boolean { require(number.let(::isPhoneNumber)) // ... } // ... private fun isPhoneNumber(number: String): Boolean = phoneNumberVerifier.verify(number) }

Why to avoid extension functions

There are a few good reasons why we prefer to avoid member extensions:

  • Reference is not supported:
val ref = String::isPhoneNumber val str = "1234567890" val boundedRef = str::isPhoneNumber val refX = PhoneBookIncorrect::isPhoneNumber // ERROR val book = PhoneBookIncorrect() val boundedRefX = book::isPhoneNumber // ERROR
  • Implicit access to both receivers might be confusing:
class A { val a = 10 } class B { val a = 20 val b = 30 fun A.test() = a + b // Is it 40 or 50? }
  • When we expect an extension to modify or reference a receiver, it is not clear if we modify the extension receiver or the dispatch receiver (the class in which the extension is defined):
class A { //... } class B { //... fun A.update() ... // Does it update A or B? }
  • For less experienced developers, it might be counterintuitive or scary to see member extensions.

Avoid, not prohibit

This rule does not apply everywhere. The most obvious situation in which member extensions need to be used is when we define DSL builders (as presented in Item 34: Consider defining a DSL for complex object creation). Member extensions are also useful when we need to define a function that is called on some object representing a scope. One example might be a member function that produces a Channel using the produce function. Another might be an integration test function that calls an endpoint on TestApplicationEngine and is defined in an interface (as I explained in my article Traits for testing in Kotlin, this is a popular pattern used in backend integration tests). In both cases, defining this function as a scope is not our whim but serves a concrete purpose.

class OrderUseCase( // ... ) { // ... private fun CoroutineScope.produceOrders() = produce<Order> { var page = 0 do { val orders = api .requestOrders(page = page++) .orEmpty() for (order in orders) send(order) } while (orders.isNotEmpty()) } } interface UserApiTrait { fun TestApplicationEngine.requestRegisterUser( token: String, request: RegisterUserRequest ): UserJson? = ... fun TestApplicationEngine.requestGetUserSelf( token: String ): UserJson? = ... // ... }

We prefer to avoid member extensions, but we use them if they are the best option we have.

Summary

To summarize, if there is a good reason to use a member extension, it is fine. Just be aware of the downsides and generally try to avoid it. To restrict visibility, use visibility modifiers. Just placing an extension in a class does not limit its use from outside.