Introduction to Class Delegation
source link: https://typealias.com/start/kotlin-delegation/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Introduction to Class Delegation
Roger is out for dinner at a restaurant, when the waiter walks up and asks, “Can I start you off with something to drink, sir?”
“I’d like a soda, please,” says Roger.
“One moment, sir.” The waiter walks back to a counter in the kitchen, fills a glass of soda, and sets it down on the table. “Are you ready to order your meal, sir?”
Roger replies, “Yes, I’d like the salmon on rice, please.”
“I’ll get that in for you,” says the waiter. He walks back to the kitchen again. This time, though, he doesn’t prepare the order himself. Instead, he hands it off to the chef, who skillfullly prepares the delicious dinner. Once it’s ready, the chef hands the meal to the waiter, who returns and places it on the table for Roger.
Delegation in Restaurants and Code
As this story shows, sometimes a waiter can fulfill an order without involving the chef - such as when Roger ordered his beverage - but in other cases, such as when Roger ordered an entrée, the waiter has to hand off the order to the chef to fulfill.
Similarly, in Kotlin, sometimes an object is fully capable of fulfilling a request (such as a function call) on its own, and in other cases, it might need to hand off the request to another object.
Whether this happens in real life or in Kotlin, it’s called delegation.
By the way: Delegation vs. Forwarding
Technically, what you’re going to see in this chapter is more precisely known in the broader programming community as “forwarding” rather than “delegation”. However, in the Kotlin world, it’s always referred to as delegation, so we’ll continue to use that term here.Manual Delegation
Let’s review the relationship between the customer, the waiter, and the chef.
- The customer places an order with the waiter.
- When the order is for an entrée, the waiter delegates the entrée preparation to the chef.
Notice that customers never interact with the chef directly - they only ever interact with the waiter, who will interact with the chef on the customers’ behalf.
We can model this customer-waiter-chef relationship in Kotlin. To get started, let’s replace the drawings above with boxes, which will roughly convert our illustration into a simple UML class diagram.
UML class diagram. Customer -> Waiter -> Chef.ordersfromdelegatesentrees toCustomerWaiter+ prepareBeverage(name: String): Beverage?+ prepareEntree(name: Entree): Entree?+ acceptPayment(money: Int)Chef+ prepareEntree(name: String): Entree?
Now we’re ready to create classes to represent the waiter and the chef, along with some enum classes for the beverages and entrées.
class Chef {
fun prepareEntree(name: String): Entree? = when (name) {
"Tossed Salad" -> Entree.TOSSED_SALAD
"Salmon on Rice" -> Entree.SALMON_ON_RICE
else -> null
}
}
class Waiter(private val chef: Chef) {
// The waiter can prepare a beverage by himself...
fun prepareBeverage(name: String): Beverage? = when (name) {
"Water" -> Beverage.WATER
"Soda" -> Beverage.SODA
else -> null
}
// ... but needs the chef to prepare an entree
fun prepareEntree(name: String): Entree? = chef.prepareEntree(name)
fun acceptPayment(money: Int) = println("Thank you for paying for your meal")
}
enum class Entree { TOSSED_SALAD, SALMON_ON_RICE }
enum class Beverage { WATER, SODA }
Listing 13.1
In this code, the prepareEntree()
function in Waiter
simply calls the same function on the chef
object, sending along the same name
argument that it received. This is manual delegation.
The word delegate can be either a noun or a verb, with a slight difference in pronunciation. In the listing above, the chef
object here is called a delegate (noun), because the Waiter
delegates (verb) the prepareEntree()
call to it.
Now we’ve got a class for the waiter and the chef. However, we won’t bother creating a class for the customer. Instead, the code in the next listing will act like a customer, calling functions on a Waiter
object.
val waiter = Waiter(Chef())
val beverage = waiter.prepareBeverage("Soda")
val entree = waiter.prepareEntree("Salmon on Rice")
Listing 13.2
Congratulations! You’ve already created a simple, manual delegation relationship between the Waiter
and the Chef
. As we’ll see in a moment, delegation can be even easier than this!
Before we move on, though, you might have noticed that both Waiter
and Chef
have a function called prepareEntree()
, and those two functions have the same parameter types and return type.
Points out that the two functions have the same signature.classChef {funprepareEntree(name: String): Entree? =when(name) {"Caesar Salad"Entree.CAESAR_SALAD"Salmon on Rice"Entree.SALMON_ON_RICEelsenull}}classWaiter(private valchef: Chef) {The waiter can prepare a beverage by himselffunprepareBeverage(name: String): Beverage? =when(name) {"Water"Beverage.WATER"Soda"Beverage.SODAelsenull}but needs the chef to prepare an entreefunprepareEntree(name: String): Entree? =chef.prepareEntree(name)funacceptPayment(money: Int) = println("Thank you for paying for your meal")}same function name, parameters, and return typefunprepareEntree(name: String): Entree?sfunprepareEntree(name: String): Entree?
As we saw in the last chapter, when this happens, we can create an interface. Let’s update our code from Listing 13.1 so that Waiter
and Chef
both implement the same interface. Remember, when we do this, we also need to mark the prepareEntree()
function as override
in those two classes.
interface KitchenStaff {
fun prepareEntree(name: String): Entree?
}
class Chef : KitchenStaff {
override fun prepareEntree(name: String): Entree? = when (name) {
"Tossed Salad" -> Entree.TOSSED_SALAD
"Salmon on Rice" -> Entree.SALMON_ON_RICE
else -> null
}
}
class Waiter(private val chef: Chef) : KitchenStaff {
// The waiter can prepare beverages by himself...
fun prepareBeverage(name: String): Beverage? = when (name) {
"Water" -> Beverage.WATER
"Soda" -> Beverage.SODA
else -> null
}
// ... but needs the chef to prepare the entrees
override fun prepareEntree(name: String): Entree? = chef.prepareEntree(name)
fun acceptPayment(money: Int) = println("Thank you for paying for your meal")
}
Listing 13.3
Great! We’ve now got a delgation relationship between the Waiter
and the Chef
, plus we’ve created an interface for prepareEntree()
function that they both share.
Delegating More Function Calls
Now, writing one function to manually call chef.prepareEntree()
isn’t too bad, but there are plenty of other cases when the waiter might delegate to the chef. For example:
- Getting a list of the chef’s specials for the day
- Preparing an appetizer
- Preparing a dessert
- Sending the customer’s compliments along to the chef
It’s easy to update the interface to include these things:
interface KitchenStaff {
val specials: List<String>
fun prepareEntree(name: String): Entree?
fun prepareAppetizer(name: String): Appetizer?fun prepareDessert(name: String): Dessert?fun receiveCompliment(message: String)
}
Listing 13.4
However, we also have to update the Chef
and Waiter
classes to implement the new property and functions. As for how the Chef
class might implement those functions… we can leave that to our imaginations. We can easily see how this affects the Waiter
class, though:
class Waiter(private val chef: Chef) : KitchenStaff {
// These first two functions are the same as before
fun prepareBeverage(name: String): Beverage? = when (name) {
"Water" -> Beverage.WATER
"Soda" -> Beverage.SODA
else -> null
}
fun acceptPayment(money: Int) = println("Thank you for paying for your meal")
// Manually delegating to the chef for all of these things:
override val specials: List<String> get() = chef.specialsoverride fun prepareEntree(name: String) = chef.prepareEntree(name)override fun prepareAppetizer(name: String) = chef.prepareAppetizer(name)override fun prepareDessert(name: String) = chef.prepareDessert(name)override fun receiveCompliment(message: String) = chef.receiveCompliment(message)
}
Listing 13.5
As more and more properties and functions are added to the KitchenStaff
interface, manually delegating from the Waiter
to the Chef
becomes tedious to write, tedious to read, and makes it more likely that we could accidentally get something wrong.
When you look at override
functions above, you’ll notice a lot of repeated text in the code:
- The
override fun
- The name of the function on the left and the right side of the line
- The parameter names on the left and also on the right
The pattern is the same on each line. Code that has the same repeated pattern like this is called boilerplate.1 Even using Kotlin’s expression body functions, as we’re doing here, the boilerplate is really starting to pile up.
Lots of boilerplateoverride valspecials: List<String>get() =chef.specialsoverride funprepareEntree(name: String) =chef.prepareEntree(name)override funprepareAppetizer(name: String) =chef.prepareAppetizer(name)override funprepareDessert(name: String) =chef.prepareDessert(name)override funreceiveCompliment(message: String) =chef.receiveCompliment(message)Lots of boilerplate!
Thankfully, Kotlin makes it easy to do this kind of delegation, without having to write it all by hand!
Easy Delegation, the Kotlin Way
Instead of writing all of the boilerplate for delegation manually, we can just use Kotlin’s class delegation feature. To do this, we just need to do two things:
- Indicate which functions and properties to delegate
- Indicate which object they should be delegated to
The functions and properties are specified by the name of the interface that contains them, and the delegate is specified using the by
keyword. Once you do this, you can remove the manual delegation.
For example, to delegate all of the properties and functions in the KitchenStaff
to the chef
object, we can simply write this:
class Waiter(private val chef: Chef) : KitchenStaff by chef {
fun prepareBeverage(name: String): Beverage? = when (name) {
"Water" -> Beverage.WATER
"Soda" -> Beverage.SODA
else -> null
}
fun acceptPayment(money: Int) = println("Thank you for paying for your meal")
}
Listing 13.6
Easy, right? This code works the same as Listing 13.5; it’s just written differently. With this change, all of the properties and functions that were manually delegated to chef
are completely omitted from the Waiter
class body, but it’s still delegating them.
The only functions left inside this class are prepareBeverage()
and acceptPayment()
- the two functions that Waiter
handles himself. In other words, when you look at this class, you can really focus on what’s unique about the Waiter
class instead of having to see everything that it delegates to chef
.
By simply including by chef
, Kotlin does a lot of work for us. When you look at the first line of Listing 13.6, you can read it like:
Waiter
implementsKitchenStaff
by
delegating tochef
.
Or, illustrated:
Annotated code: `Waiter` implements `KitchenStaff` `by` delegating to the `chef` object. (point to each thing)classWaiter(private valchef: Chef) : KitchenStaffbychefimplementsdelegating to
By the way...
Note that since we no longer needed a member instance ofchef
, we could even remove private val
from the constructor. However, we’re going to need it again later in this chapter, so let’s leave it there for now.
Multiple Delegates
Good news - the restaurant just opened up its new beverage bar, offering fancy drinks like peach iced tea and tea-lemonade. Instead of preparing the beverages himself, the waiter will now delegate beverage preparation to the bartender.
To start with, let’s add a new interface and class for the bartender. We’ll also update the Beverage
enum class with the new beverage options.
interface BarStaff {
fun prepareBeverage(name: String): Beverage?
}
class Bartender: BarStaff {
override fun prepareBeverage(name: String): Beverage? = when(name) {
"Water" -> Beverage.WATER
"Soda" -> Beverage.SODA
"Peach Tea" -> Beverage.PEACH_ICED_TEA
"Tea-Lemonade" -> Beverage.TEA_LEMONADE
else -> null
}
}
enum class Beverage { WATER, SODA, PEACH_ICED_TEA, TEA_LEMONADE }
Listing 13.7
Now, we want the Waiter
to delegate the beverage preparation to the bartender. To start, let’s add a bartender
property to the Waiter
, and manually delegate to it, just like we did previously with the chef
.
class Waiter(
private val chef: Chef,
private val bartender: Bartender
) : KitchenStaff by chef, BarStaff {
override fun prepareBeverage(name: String) = bartender.prepareBeverage(name)
fun acceptPayment(money: Int) = println("Thank you for paying for your meal")
}
Listing 13.8
Here, we’ve got Kotlin automatically delegating everything in KitchenStaff
to the chef
, and we’re manually delegating prepareBeverage()
to the Bartender
. This is fine, but Kotlin also allows us to use by
to delegate to more than one object at a time, and it works just like you’d expect. Simply add by bartender
after BarStaff
, like this:
class Waiter(
private val chef: Chef,
private val bartender: Bartender
) : KitchenStaff by chef, BarStaff by bartender {
fun acceptPayment(money: Int) = println("Thank you for paying for your meal")
}
Listing 13.9
With this, we now have a class that delegates to two different classes. As in Listing 13.8, the body of the Waiter
class shows only what’s unique to the Waiter
class. All of the delegation boilerplate is tucked away behind those simple by
declarations!
Overriding a Delegated Call
As it’s getting later, the restaurant is getting crowded, and the chef is busy trying to prepare food for all of the customers. So, the chef says to the waiter, “The salad is easy to prepare. From now on, if you get an order for a tossed salad, just take care of it yourself. I’ll still handle the fancier meals.”
As we’ve seen, when we use Kotlin’s class delegation, all of the properties and functions from the interface are automatically sent to the designated object. However, you can also choose not to automatically delegate one or more particular properties or functions from the interface.
Let’s update our Kotlin code so that the Waiter
can prepare the salad by himself. To do this, we can include the prepareEntree()
function in Waiter
again, and manually delegate it to the chef
only when needed. Here’s how that looks:
class Waiter(
private val chef: Chef,
private val bartender: Bartender
) : KitchenStaff by chef, BarStaff by bartender {
override fun prepareEntree(name: String): Entree? =if (name == "Tossed Salad") Entree.TOSSED_SALAD else chef.prepareEntree(name)
fun acceptPayment(money: Int) = println("Thank you for paying for your meal")
}
Listing 13.10
When a function from KitchenStaff
is included in the Waiter
class, Kotlin will use that function instead of the sending it to the delegate. However, we can still choose to manually delegate to it, as we’re doing here in Listing 13.10. When the customer orders anything other than a tossed salad, we manually delegate to the chef
.
So, we can combine Kotlin’s class delegation with manual delegation.
Managing Conflicts
When the meal is fantastic, sometimes the customer tells the waiter to send along compliments to the chef. The KitchenStaff
has a function called receiveCompliment()
. So, when this function is called on a Waiter
object, it’s sent along to its chef
object. Inside the Chef
class, we can simply print out that the compliment was received, like this:
interface KitchenStaff {
// ... disregarding other properties and functions ...
fun receiveCompliment(message: String)
}
class Chef : KitchenStaff
// ... disregarding other properties and functions ...
override fun receiveCompliment(message: String) =
println("Chef received a compliment: $message")
}
Listing 13.11
And of course, calling this function on the waiter
will send it along to the chef
.
val waiter = Waiter(Chef(), Bartender())
val beverage = waiter.prepareBeverage("Water")
val entree = waiter.prepareEntree("Salmon on Rice")
waiter.receiveCompliment("The salmon entree was fantastic!")
Listing 13.12
The chef isn’t the only one who might receive a compliment, though. The bartender makes an excellent peach iced tea, and sometimes the customers want to send along their compliments to the bartender, too! Let’s update the BarStaff
interface and the Bartender
class so that he can also receive a compliment.
interface BarStaff {
fun prepareBeverage(name: String): Beverage
fun receiveCompliment(message: String)
}
class Bartender: BarStaff {
// ... disregarding other properties and functions ...
override fun receiveCompliment(message: String) =println("Bartender received a compliment: $message")
}
Listing 13.13
Now, both Chef
and Bartender
include a function called receiveCompliment()
- and they both have the same parameter types and return type.
Show how they both have the same thing.interfaceKitchenStaff {valspecials: List<String>funprepareEntree(name: String): Entree?funprepareAppetizer(name: String): Appetizer?funprepareDessert(name: String): Dessert?funreceiveCompliment(message: String)}interfaceBarStaff {funprepareBeverage(name: String): BeveragefunreceiveCompliment(message: String)}same function name, parameter, and return type
So, when our code calls waiter.receiveCompliment()
, what should Kotlin do with it? Should it send the compliment to the chef
object, or to the bartender
object? Or maybe both?
It’s up to you as the programmer to decide. In fact, in a situation like this, where you try to use class delegation for two interfaces that have the same property or function, you’ll get a compiler error like this:
Class ‘Waiter’ must override public open fun receiveCompliment(message: String): Unit defined in Waiter because it inherits many implementations of it.
To resolve this, we’ll need to include this function in the Waiter
class. For example, we could check to see if the message
includes the word “entree” or “beverage”, and manually delegate to the chef or bartender accordingly. Then, as a last resort, the waiter can receive the compliment himself.
class Waiter(
private val chef: Chef,
private val bartender: Bartender
) : KitchenStaff by chef, BarStaff by bartender {
override fun receiveCompliment(message: String) = when {
message.contains("entree") -> chef.receiveCompliment(message)
message.contains("beverage") -> bartender.receiveCompliment(message)
else -> println("Waiter received compliment: $message")
}
override fun prepareEntree(name: String): Entree? =
if (name == "Tossed Salad") Entree.TOSSED_SALAD else chef.prepareEntree(name)
fun acceptPayment(money: Int) = println("Thank you for paying for your meal")
}
Listing 13.14
And now, the customer can send compliments along to the chef, the bartender, or the waiter!
val waiter = Waiter(Chef(), Bartender())
waiter.receiveCompliment("The salmon entree was fantastic!")
waiter.receiveCompliment("The peach tea beverage was fantastic!")
waiter.receiveCompliment("The service was fantastic!")
Listing 13.15
Chef received a compliment: The salmon entree was fantastic!
Bartender received a compliment: The peach tea beverage was fantastic!
Waiter received compliment: The service was fantastic!
So far, we’ve used class delegation to easily forward property calls and function calls from a waiter
object to the chef
and bartender
objects. Although waiters, chefs, and bartenders illustrate the concept of delegation with concrete examples, delegation is also often used for a different purpose - to share general code across multiple specific types. So before we wrap up this chapter, let’s see how that works!
Delegation for General and Specific Types
As we saw in the last chapter, interfaces often represent general types like FarmAnimal
, and classes that implement interfaces often represent more specific types like Chicken
, Pig
, and Cow
. In some cases, you might have some general code that you want to share among many different specific types.
For example, all farm animals will want to eat, although the particular food that they eat might be a little different depending on what kind of animal they are. In this next code listing, we have three farm animals, each with its own eat()
function.
class Cow {
fun eat() = println("Eating grass - munch, munch, munch!")
}
class Chicken {
fun eat() = println("Eating bugs - munch, munch, munch!")
}
class Pig {
fun eat() = println("Eating corn - munch, munch, munch!")
}
Cow().eat() // Eating grass - munch, munch, munch!
Chicken().eat() // Eating bugs - munch, munch, munch!
Pig().eat() // Eating corn - munch, munch, munch!
Listing 13.16
You probably noticed some boilerplate code here again. In fact, other than their names, the only difference between each of these classes is the food in the string. The eat()
function is general, and we can share it across these specific classes with delegation. It’s quick and easy, so let’s start with a simple interface for someone who eats.
interface Eater {
fun eat()
}
Listing 13.17
Let’s implement this interface with a class that makes that munching sound.
class Muncher(private val food: String) : Eater {
override fun eat() = println("Eating $food - munch, munch, munch!")
}
Listing 13.18
By using class delegation, we can make it so that all of the animals share this implementation of the eat()
function. Notice that there’s a lot less repeated code now:
class Cow : Eater by Muncher("grass")
class Chicken : Eater by Muncher("bugs")
class Pig : Eater by Muncher("corn")
Cow().eat() // Eating grass - munch, munch, munch!
Chicken().eat() // Eating bugs - munch, munch, munch!
Pig().eat() // Eating corn - munch, munch, munch!
Listing 13.19
As it turns out, the pigs on this farm like to scarf down their dinner much faster than the cows and chickens, who usually just graze. So instead of sharing the Mucher
code with them, we can just implement the eat()
function directly in Pig
, like this:
class Cow : Eater by Muncher("grass")
class Chicken : Eater by Muncher("bugs")
class Pig : Eater {
override fun eat() = println("Scarfing down corn - NOM NOM NOM!!!")
}
Cow().eat() // Eating grass - munch, munch, munch!
Chicken().eat() // Eating bugs - munch, munch, munch!
Pig().eat() // Scarfing down corn - NOM NOM NOM!!!
Listing 13.20
Alternatively, we can accomplish the same thing by creating another class, and using it as the Pig
’s delegate. Here’s how that looks:
class Scarfer(private val food: String) : Eater {
override fun eat() = println("Scarfing down $food - NOM NOM NOM!!!")
}
class Cow : Eater by Muncher("grass")
class Chicken : Eater by Muncher("bugs")
class Pig : Eater by Scarfer("corn")
Listing 13.21
So, class delegation can be used to share some general code - such as how to eat()
- across different specific types - like Cow
, Chicken
, and Pig
classes.
This is only one way of sharing code across different classes. In the next chapter, we’ll look at abstract and open classes, which can also be used to share code.
Summary
In this chapter, you learned:
- What delegation is.
- How to manually delegate from one object to another.
- How to use Kotlin’s class delegation to automatically delegate.
- How to override particular functions that would otherwise be delegated.
- How to resolve conflicts when two delegates provide an implementation for the same function.
- How to use delegation for general and specific types.
As mentioned above, the next chapter will cover abstract and open classes, which can also be used to share code among multiple classes. See you then!
- The term boilerplate comes from the old days of the printed newspaper industry, when publishing syndicates would send local newspapers stories on metal plates that were ready for them to print. Those metal plates looked like the plating used when producing steam boilers. Those stories were also often fluff articles that lacked original content, which is how the term got its meaning. (“boilerplate,” Merriam-Webster.com Dictionary, https://www.merriam-webster.com/dictionary/boilerplate. Accessed 1/23/2023). [return]
Share this article:
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK