Beautify third-party API with Kotlin

Nicolas Fränkel - Dec 19 '21 - - Dev Community

Scala has popularized the "Pimp my library" pattern:

This is just a fancy expression to refer to the ability to supplement a library using implicit conversions.

-- Pimp My Library Pattern in Scala

Kotlin does provide the same capability. However, it achieves it via extension functions. While the generated bytecode is similar to Java's static methods, the developer experience is the same as adding functions to existing types.

This approach has limitations, though. One cannot update the type hierarchy.

Imagine a library that offers a component with an open/close lifecycle. The component opens when you instantiate it, but you need to make sure to close it after usage, e.g., a file, a stream, etc.

Before Java 7, you had actually to close the component explicitly:

Component component;
try {
    component = new Component();
    // Use component
} finally {
    component.close();
}
Enter fullscreen mode Exit fullscreen mode

Java 7 introduced the try-with-resource statement so that you can write something like this:

try (Component component = new Component()) {
    // Use component
}                                                // 1
Enter fullscreen mode Exit fullscreen mode
  1. Component is closed here in a generated finally block

However, Component must implement AutoCloseable.

AutoCloseable API class diagram

Kotlin provides the use() extension function on Closeable.
Hence, one can replace the try-with-resource statement with a simple function call:

Component().use {
  // Use component as it
}                                                // 1
Enter fullscreen mode Exit fullscreen mode
  1. Component is closed here in a finally block

That being said, imagine that the library above implements neither Closeable nor AutoCloseable. We cannot use use(). Kotlin delegation to the rescue!

The Delegation pattern is widespread in Object-Oriented languages:

In delegation, an object handles a request by delegating to a second object (the delegate). The delegate is a helper object, but with the original context. With language-level support for delegation, this is done implicitly by having self in the delegate refer to the original (sending) object, not the delegate (receiving object). In the delegate pattern, this is instead accomplished by explicitly passing the original object to the delegate, as an argument to a method.

-- Wikipedia

Implementing the Delegation pattern in Java requires writing a lot of boilerplate code. The more methods the original class has, the more boring it is:

interface class Component {
    void a();
    void b();
    void c();
}

public class CloseableComponent extends Component implements Closeable {

    private final Component component;

    public CloseableComponent(Component component) {
        this.component = component;
    }

    void a() { component.a(); }
    void b() { component.b(); }
    void c() { component.c(); }
    public void close() {}
}
Enter fullscreen mode Exit fullscreen mode

Kotlin supports the Delegation pattern out-of-the-box via the by keyword. One can rewrite the above code as:

interface Component {
    fun a() {}
    fun b() {}
    fun c() {}
}

class CloseableComponent(component: Component) : Component by component,
                                                 Closeable {                  // 1
    override fun close() {}
}
Enter fullscreen mode Exit fullscreen mode
  1. Delegate all calls of a(), b(), and c() to the underlying component

We can finally write the desired code:

CloseableComponent(RealComponent()).use {
    // Use component as it
}
Enter fullscreen mode Exit fullscreen mode

Even better, it works with third-party code to improve an external library with this approach.

The icing on the cake, one can also call the code from a try-with-resource Java statement:

try (CloseableComponent component = new CloseableComponent(new RealComponent())) {
    // Use component
}
Enter fullscreen mode Exit fullscreen mode

As I wrote above, one can do it in Java also. In general, however, the sheer amount of boilerplate code that one needs to write to implement delegation is a significant impediment. Kotlin makes it a breeze.

We miss one last step to make our code easier to write. How do we get the CloseableComponent? Let's create an extension function on Component:

fun Component.toCloseable() = CloseableComponent(this)
Enter fullscreen mode Exit fullscreen mode

And now, usage is fluent:

RealComponent().toCloseable().use {
    // Use component
}
Enter fullscreen mode Exit fullscreen mode

In this post, we have seen how to improve the API provided by third-party libraries. We achieved it by combining Kotlin extension functions and delegation.

To go further:

Originally published at A Java Geek on December 19th, 2021

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .