Most programming involves reusing already existing code, sometimes with a few changes. In object-oriented programming (which is the case of Kotlin), for example, the main tool for code reusage is inheritance (and composition, consequently), which we've already covered. In this topic, we will discuss an alternative to inheritance – delegation.
Syntax of delegation
Delegation is a process of using a certain object instead of providing implementation, and we're going to take a look at how exactly it works.
Suppose we have fairly simple code – an interface and it's implementation:
interface MyInterface {
fun print()
val msg: String
}
class MyImplementation : MyInterface {
override fun print() {
println(msg)
}
override val msg: String = "MyImplementation sends regards!"
}
Nothing new here: the interface declares a property and a function, and the class implements those.
Now, suppose that we need to create a new class, which would 1) have some functionality of its own, and 2) would implement the given interface at the same time. We'd stumble upon copy-pasting the code: we already have an implementation of this interface, but we need a different class, which, however, would still need to implement this interface.
That's where delegation comes into play: it turns out that we can happily code our new class, and when we need to use the implementation of the interface, we just reference the already existing implementation, and Kotlin does the rest. Like so:
class MyNewClass(base: MyInterface) : MyInterface by base {
override val msg = "Delegate sends regards."
}
Alright, but what are those "by" and "base" in this context? Let's take a closer look.
class MyNewClass(
base: MyInterface)
// ^ Here we expect an implementation of MyInterface as a parameter (named "base")
: MyInterface by base {
// ^ And here we state that MyInterface is implemented by the previously obtained parameter, the one named "base"
override val msg = "Delegate sends regards."
}
Essentially, in the constructor of this class, we require something that implements the interface MyInterface marked by a colon (:), and then, using the keyword by, we tell the derived class that whenever it is asked to perform anything "promised" by the MyInterface interface, it will use the provided object to do so.
The code looks like this:
// We create an instance of class, implementing MyInterface
val delegate = MyImplementation()
// Then we pass this implementation instance as a parameter
val delegatingObj = MyNewClass(delegate)
println(delegatingObj.msg)
It will print:
Delegate sends regards.Solving complications by overriding
However, what exactly will this code do?
val delegate = MyImplementation()
val delegatingObj = MyNewClass(delegate)
delegatingObj.print()
Notice that in the previous example, we accessed the msg property that the delegating class MyNewClass specifically overrides. Now that we access the method print() that we don't override in MyNewClass, what do you think the code will print?
Take some time to muse on the answer, then keep reading.
This code will print the following line:
MyImplementation sends regards!
Let's look at our class with delegation again:
class MyNewClass(base: MyInterface) : MyInterface by base {
override val msg = "Delegate sends regards."
}
It doesn't have any methods named print(). But it does have the base instance, which is an implementation of MyInterface, which does have the print() function, and that function is called when we write delegate.print(). So, the class MyNewClass just delegates this task to MyImplementation (the delegate). MyImplementation contains an overridden msg, which reads MyImplementation sends regards!, so the code just prints MyImplementation sends regards! to the console.
When you use delegation, be careful to draw a line between the overridden properties/methods of the delegating class and the ones which will use only the base implementation and its data.
Callback and Logger example
In the example above, we mainly used delegation to override some properties set by the interface and do something simple. Let's take a look at a more complex case featuring not one but two delegates!
This example is twice as complicated as what we've had before, so no worries if it looks unclear – once you develop a better intuitive feeling for the structure of delegate, it will make more sense.
First, let's understand the two interfaces we're going to use:
ICallbackReceiver: This interface outlines the structure for callbacks. It's used in the case when we need to "surround" a certain action with function calls doing something before (onBeforeAction()) and after (onAfterAction()) the execution of a function (action()).ILogger: This interface is simply a formatter for output. However, when used in delegation, it makes all output follow the same pattern, which can be useful for logging.
Here's how these interfaces look in code:
// Defines the contract for callbacks
interface ICallbackReceiver {
fun onBeforeAction()
fun onAfterAction()
fun action(function: () -> Unit) {
onBeforeAction()
function()
onAfterAction()
}
}
// Defines the contract for logging
interface ILogger {
fun getStubDateTime() = "05.11.2022-14:31:04" // placeholder date and time
val format: String
get() = "[${getStubDateTime()}]: "
fun print(s: String)
}
Now, let's provide an implementation for these interfaces:
BasicLogger: A simple implementation of theILoggerinterface. It prints the formatted output to the console.ConsoleNotifier: It implements two interfaces:ICallbackReceiverinterface, while defining the actions to be performed before and after the main action.ILogger, thatBasicLoggerobject will delegate to it for printing messages to the console instead of the usualprintln().
Here's what they look like in code:
// Simple implementation of ILogger interface
class BasicLogger : ILogger {
override fun print(s: String) = println(format + s)
}
// Implementation of ICallbackReceiver that uses BasicLogger for printing
class ConsoleNotifier(logger: ILogger) : ICallbackReceiver, ILogger by logger {
val onBeforeStr = "OnBefore!"
val onAfterStr = "OnAfter!"
// "print" is delegated to "logger"
override fun onBeforeAction() = print(onBeforeStr)
override fun onAfterAction() = print(onAfterStr)
}
Finally, we'll create a class, ExampleParser, that implements both interfaces using delegation. Notice that the ExampleParser class doesn’t need to know how to handle callbacks or print messages itself, it just delegates those responsibilities to other objects that know how to do it.
// Class implementing both interfaces by delegation
class ExampleParser(notifier: ConsoleNotifier, logger: BasicLogger) :
ICallbackReceiver by notifier,
ILogger by logger {
fun start() = action { parseFiles() }
fun parseFiles() {
print("Parsing...")
// do some file parsing
}
}
Keep in mind that there is another way of specifying the constructors of ExampleParser class, so that it becomes more flexible and expects any implementation of the ICallbackReceiver and ILogger interfaces. This means you could pass any object that implements these interfaces, not just ConsoleNotifier and BasicLogger. This allows for more flexibility and is generally a better practice as it follows the principle of "programming to an interface, not an implementation", which is a key principle in object-oriented programming.
This is the better version of ExampleParser:
class ExampleParser(notifier: ICallbackReceiver, logger: ILogger) :
ICallbackReceiver by notifier,
ILogger by logger {
...
}
Now, when you run the following code, instances of BasicLogger and ConsoleNotifier will be created. These instances will then be passed to the constructor of ExampleParser. When the start() function of ExampleParser is called, it will print messages using the format defined in BasicLogger, and it will also call the functions defined in ConsoleNotifier before and after parsing files.
fun main() {
val loggerInstance = BasicLogger()
val dateTimeNotifier = ConsoleNotifier(loggerInstance)
val simpleParser = ExampleParser(dateTimeNotifier, loggerInstance)
simpleParser.start()
}
Here's what the output will look like:
[05.11.2022-14:31:04]: OnBefore!
[05.11.2022-14:31:04]: Parsing...
[05.11.2022-14:31:04]: OnAfter!Conclusion
Delegation allows for a better code reusage, or rather makes it more convenient due to Kotlin's good language-level support of delegation. Instead of writing code to achieve certain functionality inside the class (potentially copying this code from already existing implementations), we can introduce an object that already has the functionality we need and use this object to get the desirable result.