Delegation Pattern in Kotlin
Issues with inheritance to make your code reusable
When it comes to reusing certain classes for the behavior, we may be tempted to take a base class for a ‘default’ behavior, extend it through a child class and override its methods with additional functionality; something we call inheritance. There are some cases when this is perfectly fine. However, when it comes to code that is on continuous maintenance, issues with this will arrive sooner or later. Let’s have a look at them.
Tight coupling. The first one is quite obvious: it creates tight coupling between your class and its parent. Let’s look at an example adapted to Kotlin, based from Java Design:
In this example, you create an AccountType
class from which you inherit a method for calculating interest. And, if you want to calculate interest in a different way or put some process in between, then you can simply override the method.
The problem is that, should you want to create more account types, and decide that some of them should have the same interest calculation from stocks instead of the default one, then you’re going to have to duplicate the overridden method from the Stocks
class, leaving you with duplicated code, somewhat defeating the purpose of what you've made through inheritance. And say that, with time, the account types that use the Stocks
class's method become more numerous, so that it makes more sense to implement it as default. Then you would have to modify the base calculateInterest()
method from AccountType
, which is a big no-no from the Open-Closed Principle:
Open-Closed Principle: “Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.”
Code fragility. The second issue is that, by overriding functionality, you’re basically tinkering with the parent class’s behavior. As in the example above, you may want to reimplement the method completely. And with such a simple class, this may be achievable. But of course, when it comes to more complex classes, this may break a method’s functionality that may rely on communicating with other properties and methods that you’re unaware of. If the class you’re inheriting from is a third-party one over which you have no control, the problem gets compounded. Say, if you’re working on a team project and updating versions of this code makes sense, then the dependency of your code on this class could be broken. The example below is from the Fragmented podcast, in which they talk about extending a HashSet
to add a counter that keeps track of how many items are being added:
The code from main
makes very obvious that the count should be 3... Until it outputs 6. Why is that? Well, diving into the addAll
method shows that it uses the add
method for every object in the set it takes as a parameter. Complex classes that have this kind of interaction between methods could break the functionality we are providing by an override. And if it wasn't and the class was our own, if we wanted to create functionality like this, we would need to rewrite a lot more code just for the sake of not breaking anything.
Delegation as a better way of achieving reusability
So how can we make those examples better? Instead of making our class inherit from another class directly, we can make our class have an instance of that class, and handle the functionality that we wanted to get from that class. This is called delegation, and it comes with the immediate advantage that we don’t mess with the code from a parent class. Whatever it is, we let it act by itself; the delegate (as we call the instance) will handle the method calls independently of our code. But we can still override a method, should we want to (say, we wanted to add extra functionality in between), and if we want to make the delegate call the original method at some point, we can certainly do so. If not, just don’t override anything and the delegate will handle everything like normal, just like in inheritance. And Kotlin allows us to do this with very little code by using the by keyword. Here’s how it works:
- A class implements an interface by delegating its members to a specific object that conforms to it, instead of inheriting them from a class directly.
- The
by
keyword indicates that the class implements the interface by an object it contains, an object of which details we don't need to know (since it's an interface).
Let’s do this to our first example. We can start by writing an interface from which we can declare a default method, and make a class implement it:
Now, we can make the MoneyMarket
implement the interface and delegate its public members to an object:
Kotlin makes delegation very expressive and easy to understand; it’s essentially declaring that MoneyMarket
is an AccountType
due to having an object that has that functionality. We can do the same with the Stocks
class, that actually overrides a public method:
Now we can create them by passing an instance of a class that implements AccountType
:
As you can see, it’s very easy to implement the same reusability using delegate, but this approach allows us much more flexibility. We can even create completely different implementations of AccountType
and pass them to new instances of MoneyMarket
or Stocks
without ever worrying about having to change code there. So we comply with the Open-Closed principle, making our code not care about the details of other classes it uses.
Let’s fix the second example. HashSet
is already implementing the MutableSet
interface which has a lot of function declarations, so we can already start using that as our delegate type:
We are overriding the same methods as in our previous example, and adding our own functionality (increment the addCount
) but instead of making a super
call, we call our delegate to do that in both overrides. Since we are not really adding anything inside those methods, we don't even need to worry about the inner details. We just let the delegate handle this stuff for us.
An Android example
Now let’s look at an example in Android. Part of the Android Jetpack libraries, View Binding serves as a bridge between views and code by making the views available while keeping null and type safety. It can be implemented in an Activity like so:
In a fragment, though, the binding is nullable, and it should be nullified on the onDestroyView
stage of the lifecycle. Unfortunately, this brings the hassle of either null-checking the binding across the fragment, or having a non-null reference to the view:
We can use Kotlin’s delegation to pass this behavior to an external instance and avoid repeating this process for every fragment. An Android developer has already written a class that allows us to keep making safe calls to the binding variable without having to do null checks and also make it null at the end of the lifecycle to follow View Binding good practices:
As you can see, the specific implementation of ViewBindingHolder
handles lifecycle to expose the binding variable. Of course, we can create additional implementations of ViewBindingHolder
that follow other rules if we need to. For this case, now we can handle the binding with ease in every fragment, by delegating the lifecycle-handling functionality to an instance of ViewBindingHolderImpl
.
You can see that for this example, we’re not really creating a reference to our delegate from the constructor. Remember: you don’t have to if you won’t need to access the delegate. This is actually a very good example of how things get handled directly by the delegate because it is listening to lifecycle changes in the fragment.
You may be able to see that you are able to delegate to instances in Kotlin with final classes. Kotlin classes are final by default, and for a good reason: to make developers think twice before propagating the functionality via inheritance, which again, can cause problems, since changing the base class may break functionality in the subclasses. Delegating works by making an implementing class conform to an interface, and the final class instance just fulfills the functionality as a delegate. So we’re not really circumventing the ‘classes are final’ rule. We’re just not really extending them by any means, which is what we want in the first place.
From our first example, we can separate the interest calculations even further by making an InterestCalculator
interface:
Now, we separated the interest calculation functionality, and can now put different variations of it in different types of AccountType
without repeating code.
Conclusion
Delegation through Kotlin provides a very good alternative to inheritance when it comes to achieving reusability. Generally, it is a much safer alternative. This is not to say that inheritance should be completely deprecated, though. But through the examples stated in this writing, you can see that it is very useful to avoid modifying behavior in superclasses, and abiding by the Open-Closed principle, since it requires the use of interfaces.