Good practice on using overridable (or even abstract) member extensions

Is there a good practice on when I should use overridable or even abstract member extensions, especially in interfaces and abstract classes?
Since Kotin supports overridable member extensions, when designing methods in interfaces, especially methods with a single parameter, I am confused about whether to choose an ordinary method or an extension method. The former is more close to the traditional Java style and it’s easier to call from outside, while the latter can save some code when implementing it if there are multiple receiver calls in the implementation, but it’s more difficult to call from outside (needs run or with). Is there a good practice on which one to choose when designing interfaces in Kotlin?

This is an interesting question. It is not quite easy, but there are at least some rules that can be applied to choose between extensions and (overridable (default)) functions.

Let’s look at the considerations:

  1. Does the function need access to private/protected members of the type → member function is the only way to go, restricted to (abstract) classes
  2. Should the function be overridable, or does it override/implement some parent function → member function is necessary
  3. Java interop → If Java experience is important, members are nicer than extension functions (but they still work).
  4. Multi-receiver → Multiple receivers are possible with extension functions defined inside another class. If you want/need this, extension functions are the only option.
  5. Is the function related to a specific usage context, but not generally useful (in the scope where the type is used) → this would be an extension function (unless 1/2)
  6. Should the function not be overridden → Interface members can not be final, so extension functions can be use instead of default implementations.
  7. otherwise → There is a judgement call here. Part on preference, part on source code organization.

From my perspective the usage context is the big one. Think of it as modularisation, Extension functions can live in a separate module that brings in additional dependencies. Btw. extension functions don’t really need run or with unless you do some tricky things with multiple receivers (which is only possible with extension functions).

Perhaps you are also looking at the question whether something should be an extension function or a function that takes the same receiver as regular parameter. For that, I would say, look at what the code using it would look like. What is more natural. Consider there that the receiver for extension functions can stay “hidden” in the calling code which may or may not be a good thing. However, don’t make something an extension because it makes your method body elegant (you can use run or with for that.

2 Likes

FYI, there is a missing feature when overriding member extension functions that is still not fixed 3 years after I submitted the issue:

https://youtrack.jetbrains.com/issue/KT-11488

1 Like

As a more concrete example, here is a JDBC PreparedStatement wrapper interface that I am working on like this:

interface QueryTask<R> {
    val sql: String
    fun setParameters(preparedStatement: PreparedStatement)
    fun parseResult(resultSet: ResultSet): R 
}

If I change this to using extensions it would be like:

interface QueryTask<R> {
    val sql: String
    fun PreparedStatement.setParameters()
    fun ResultSet.parseResult(): R 
}

So according to your suggestion, I should stick with the former and use run/with when implementing these methods. There is no need to to change it just because it makes the method body look more elegant. Is that right?

@ShreckYe From my perspective, the second version (with extension functions) is harder to use. Member extensions generally work best in the context of DSLs and builders (for example a fun makeTask(body: QueryTaskBuilder<R>.() -> Unit): QueryTaskHolder<R> but that would be different from the interface.

In general, when designing APIs what I like to do is to look at the usage. How is the API intended to be used? How is the API not supposed to be used (are there ways to prevent/discourage this incorrect usage)? If there are 2 APIs (an implementer API and a user API) can they be separated, or if not, prioritise the user API (as there are less implementers).

When writing code, I tend to avoid using run/with although it is the correct solution in cases - I find it tends to be confusing in more.

1 Like