In recent reviews I stumbled upon extension functions which didn’t quite felt right, but I wasn’t able to determine where that gut feeling was coming from. After seeing quite a lot of them I came to some conclusions why that’s the case, and I gathered rules for me when extension functions make sense to use and when they should be avoided. I discovered an overuse of the language feature - something you often experience when you work with a new fancy language with nice features. In this post I’ll share my thoughts.

Rule 1: Use extension functions to extend classes not in your control

This is more or less a no-brainer, since this is one of the main usages of extension functions. It happens frequently that you want to extend some functionality of a library class. In languages without extension functions you end up with a bunch of Util classes like StringUtils, ArrayUtils and so on. With extension functions this is not necessary anymore, and your API gets much more discoverable. You can type "some String".split() instead of StringUtils.split("some String"). You don’t need to know of the existence of the StringUtils class to discover that functionality and this is a big advantage and makes it less likely that the same functionality gets implemented twice in your codebase.

Rule 2: Don’t write private extension functions in another Domain

You probably experience that often. You write the business logic of the feature you’re working on and suddenly you need to extract and transform the data of your entity. So what are your choices? You could do it in the very method your currently writing. Now you decide: “Hey, for better readability I should put that into a function”. So you extract it into a private function. Now what I experienced is that in the calling code it would be much nicer to write user.getEmailProvider() instead of getEmailProvider(user), so you write the private method as an extension function.

Don’t do this! You just wrote a function which is universal. At the moment you only use it in your domain, but your co-worker might need that functionality as well at some point.

Just implement it in User class as a old-fashioned boring method. Your co-worker can discover this functionality now and won’t end up implementing it again.

Rule 3: Only work on data of the object - avoid dependencies

In Boost Your Development With Proper API Design I stated that:

take care that every operation on your object exclusively transforms, modifies, or returns values that are bound to this very instance of the object

This is true for extension functions as well. They extend the functionality of the object and shouldn’t be misused to call functions on dependencies. Side effects are unexpected on your models, and you should avoid that in extension functions as well.

Rule 4: prefer private methods over private extension functions

When calling an extension function the reader can’t see where the declaration is coming from. When you use it in your service method the reader expects that the method is a functionality of the object and expects some kind of universal usage. He does not expect that the functionality is doing something that is just true for this domain. Take this as an example:

class UserService(private val passwordChecker: CompromisedPasswordChecker) {

    fun register(user: User) {
      if (user.hasConformPassword()) {
          // do something
      }
    }
    
    /*
    A
    lot
    of
    other
    code
    */
    
    fun User.hasConformPassword() = password.length > 8 && passwordChecker.isNotCompromised(password)

}

Note

This is also a violation of rule 3.

When you review the register(User) function you don’t expect that user.hasConformPassword is checking for compromised passwords, because it looks like it would just work on the data and don’t consider something else. Instead, if you’d wrote it as a plain-old private function the expectations are different. You suddenly consider that more could be checked in that method.

class UserService(private val passwordChecker: CompromisedPasswordChecker) {

    fun register(user: User) {
      if (hasConformPassword(user)) {
          // do something
      }
    }
    
    /*
    A
    lot
    of
    other
    code
    */
    
    fun hasConformPassword(user: User) = user.password.length > 8 && passwordChecker.isNotCompromised(user.password)

}

Summary

I love extension functions. They can make the code much more discoverable and by that heavily reduce code repetition and are just nice to use. But they can be confusing as well. Think about what you’d expect as a reader and use them when you believe the reader benefits from it. But don’t use them as a silver bullet!