Effective Kotlin Item 45: Consider extracting non-essential parts of your API into extensions
When we define final methods in a class, we need to decide whether we want to define them as members or as extension functions.
Both approaches are similar in many ways. Their use and even referencing them via reflection is very similar:
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.
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.
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:
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:
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
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.
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
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.
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.
The only exception is when an extension in the Kotlin stdlib has kotlin.internal.HidesMembers internal annotation.