Easy Augmentation with Decorators in Kotlin

Often in software design you may wish that a particular class had more functionality than it currently offers, or that you could tweak its behaviour slightly. In this contrived example, we’re going to add some logging to an existing class, but we’ve decided not to do it in the target class directly. There can be a few reasons for this:

  • You simply may not want to edit that class. Perhaps it’s a well established, well tested class that you don’t want to touch.
  • The opposite may be true - maybe it’s a legacy code nightmare and you’d rather not accidentally break it.
  • The class might not be under your control - it might be declared in a different package or externally through a library.
  • You don’t want to add yet another dependency to an existing class.
  • You may want to add this new functionality but not want to have to fix dozens of existing tests.
  • Lastly, you may want to make this added functionality reusable in other similar classes.

The only proviso for this is that the class you’re decorating exposes an interface - if it doesn’t you’ll want to extract one before you continue. Android Studio/IntelliJ thankfully makes this really easy - put your caret over a method in the class, hit CMD + SHIFT + A and choose Extract -> Interface.

This is another reason why you should always expose interfaces from other modules that define your module’s public API and preferably make the implementation private, but that’s a whole other blog post.

So for the sake of this post, we have an interface defined as follows:

interface TransactionSender {

    fun sendTransaction(
        sendDetails: SendDetails
    ): Single<SendFundsResult>

    fun someOtherFunctionality()
}

This interface defines a class which can make a transaction and returns a result of some description. It also defines another function which we’re not interested in for now. This example is adapted from an old project of mine, the V3 Blockchain Wallet. You can imagine that we might have multiple classes which implement this interface: one for each supported cryptocurrency type.

Remember, we’ve arbitrarily decided that we must include some logging on every transaction, so here is how we could approach it:

private class TransactionLogger(
    private val transactionSender: TransactionSender,
    private val eventLogger: EventLogger
) : TransactionSender by transactionSender {

    override fun sendTransaction(
        sendDetails: SendDetails
    ): Single<SendFundsResult> =
        transactionSender.sendTransaction(sendDetails)
            .doOnSuccess { sendResult ->
                if (sendResult.success) {
                    eventLogger.logEvent(sendDetails.toCustomEvent())
                }
            }
}

Here, we simply take a TransactionSender, wrap it in a class, and then use EventLogger to log the transaction details if the transaction was successful.

But the real magic here is in the by transactionSender bit, which is called Class Delegation in Kotlin. Here we’re effectively saying that the new class, TransactionLogger implements the TransactionSender interface, but all of the functions in that interface should be forwarded directly to the transactionSender constructor argument unless we say otherwise. We can then override functions from that interface if we choose to, and in the case of this example, we then forward the functionality back to transactionSender manually, but add some extra functionality around it.

For clarity, under the hood the compiler would output something similar to this (before we make any modifications to it):

private class TransactionLogger(
    private val transactionSender: TransactionSender,
    private val eventLogger: EventLogger
) : TransactionSender {

    override fun sendTransaction(
        sendDetails: SendDetails
    ): Single<SendFundsResult> =
        transactionSender.sendTransaction(sendDetails)

    override fun someOtherFunctionality() =
        transactionSender.someOtherFunctionality()
}

This can be extremely powerful, but lends itself very well to decorating certain behaviours of a class. Simply override the method that you wish to augment, build your new functionality and then forward the method call back to the delegate (or not if that’s what your decorator requires).

So now we have our decorator and it’s a nice and small, highly testable class. How do we go about using it? Simply wrap our target class. In this example we’re using Koin to inject classes, so we could do this:

factory<TransactionSender> {
    TransactionLogger(
        get<BitcoinTransactionSender>(),
        get<EventLogger>()
    )
}

This is cool, but we’re using Kotlin and we can do better. Plus, eagle eyed readers may have noticed that we made TransactionLogger private.

The final step is to create an extension function on TransactionSender objects:

fun TransactionSender.logTransaction(eventLogger: EventLogger): TransactionSender =
    TransactionLogger(this, eventLogger)

This allows us to do something like this:

factory<TransactionSender> {
    get<BitcoinTransactionSender>().logTransaction(get<EventLogger>())
}

What’s great here is you can chain these decorators over and over if you wish, so you can add multiple functionalities to an existing class in tiny, extremely testable units:

@Test
fun `on success, log transactionSender`() {
    val eventLogger: EventLogger = mock()
    val sendDetails: SendDetails = mock()
    val result: SendFundsResult = mock()
    val transactionSender: TransactionSender = mock {
        on { sendTransaction(sendDetails) } `it returns` Single.just(result)
    }
    transactionSender
        .logTransaction(eventLogger)
        .sendTransaction(sendDetails)
        .test()
        .assertComplete()
        .values()
        .first() `should equal` result

    verify(eventLogger).logEvent(any())
}

And because we’ve been working to interfaces not concrete implementations this entire time, we can now add logging to every TransactionSender object with very little effort!

Class Delegation in Kotlin is another powerful tool to add to your belt, and I hope this blog gave you some ideas as to where this might be useful in your own codebase. Go forth and decorate!

comments powered by Disqus