Roger 在餐厅吃晚餐时,服务员走过来问道:”先生,我能先给您上点喝的吗?”
”我想要一杯汽水,谢谢,”Roger 说。
”请稍等,先生。”服务员走回厨房的柜台,倒了一杯汽水,放在桌上。”先生,您准备好点餐了吗?”
Roger 回答说:”好的,我想要三文鱼配米饭,谢谢。”
“我会帮您下单,”服务员说道。他再次走回厨房。但这一次,他没有亲自准备订单,而是将它交给厨师,由厨师巧妙地准备美味的晚餐。准备完成后,厨师把餐点交给服务员,服务员回来后将餐点放在Roger的桌上。
餐厅和代码中的委托
正如这个故事所示,有时候服务员无需厨师参与就能完成订单——比如Roger点饮料时,但在其他情况下,比如当Roger点主菜时,服务员必须将订单交给厨师来完成。
类似地,在 Kotlin 中,有时候一个对象能够完全独立地完成请求(比如函数调用),但在其他情况下,它可能需要将请求转发给另一个对象。
无论这发生在现实生活中还是 Kotlin 中,这都称为委托。
手动委托
让我们回顾一下顾客、服务员和厨师之间的关系。
- 顾客向服务员下订单。
- 当订单是主菜时,服务员将主菜准备工作委托给厨师。
请注意,顾客从不直接与厨师交流——他们只与服务员互动,服务员将代表顾客与厨师交流。
我们可以在 Kotlin 中对这个顾客-服务员-厨师关系进行建模。首先,让我们把上面的插图替换成方框,这可以将我们的插图转换成简单的 UML 类图。
现在我们准备创建代表服务员和厨师的类,以及一些用于饮料和主菜的枚举类。
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 }在上面这段代码中,Waiter中的 prepareEntree() 函数只是调用 chef对象上的相同函数,并传递它接收到的相同 name参数。这就是手动委托。
delegate 这个词可以是名词也可以是动词,发音略有不同。在上面的代码中,chef 对象被称为 delegate(委托对象),因为 Waiter delegates(委托)prepareEntree() 调用给它。
现在我们有了服务员和厨师的类。但是,我们不会麻烦地为顾客创建一个类。相反,下一段代码将扮演顾客的角色,在 Waiter 对象上调用函数。
val waiter = Waiter(Chef())
val beverage = waiter.prepareBeverage("Soda")
val entree = waiter.prepareEntree("Salmon on Rice")恭喜你!你已经创建了 Waiter 和 Chef 之间的一个简单的手动委托关系。稍后我们将看到,委托可以比这更容易!
不过,在我们继续之前,你可能已经注意到 Waiter 和 Chef 都有一个名为 prepareEntree() 的函数,而且这两个函数具有相同的参数类型和返回类型。
正如我们在上一章中看到的,当发生这种情况时,我们可以创建一个接口。让我们更新 代码清单 13.1 中的代码,使 Waiter 和 Chef 都实现相同的接口。请记住,当我们这样做时,我们还需要在这两个类中将 prepareEntree() 函数标记为 override。
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 {
fun prepareBeverage(name: String): Beverage? = when (name) {
"Water" -> Beverage.WATER
"Soda" -> Beverage.SODA
else -> null
}
override fun prepareEntree(name: String): Entree? = chef.prepareEntree(name)
fun acceptPayment(money: Int) = println("Thank you for paying for your meal")
}好极了!现在我们在 Waiter 和 Chef 之间有了委托关系,此外我们还为他们共享的 prepareEntree() 函数创建了一个接口。
委托更多的函数调用
现在,手动编写一个函数来调用 chef.prepareEntree() 还算可以,但还有很多其他情况服务员可能需要委托给厨师。例如:
- 获取厨师当天的特色菜列表
- 准备开胃菜
- 准备甜点
- 将顾客的称赞转达给厨师
更新接口以包含这些内容很容易:
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)
}但是,我们还必须更新 Chef 和 Waiter 类来实现新的属性和函数。至于 Chef 类如何实现这些函数……我们可以发挥想象力。不过,我们很容易看出这如何影响 Waiter 类:
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.specials
override 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)
}随着越来越多的属性和函数被添加到 KitchenStaff 接口中,手动从 Waiter 委托给 Chef 变得繁琐、难读,而且更容易出错。
当你看上面的 override 函数时,你会发现代码中有很多重复的文字:
override fun- 函数名在代码行的左侧和右侧
- 参数名在左侧和右侧
每行的模式都是相同的。具有这种重复模式的代码称为样板代码。1 即使使用 Kotlin 的表达式函数,就像我们在这里所做的,样板代码真的开始堆积如山。
谢天谢地,Kotlin 让这种委托变得简单,无需手动编写所有代码!
简单的委托,Kotlin 方式
我们不必手动编写所有委托的样板代码,只需使用 Kotlin 的类委托功能。要做到这一点,我们只需要做两件事:
- 指明要委托哪些函数和属性
- 指明它们应该被委托给哪个对象
函数和属性由包含它们的接口名称指定,委托对象使用 by 关键字指定。一旦你这样做了,你就可以删除手动委托。
例如,要将 KitchenStaff 中的所有属性和函数委托给 chef 对象,我们只需这样写:
class Waiter(private val chef: Chef) : KitchenStaff by chef {
fun prepareBeverage(name: String): Beverage? = when (name) { /* ... */ }
fun acceptPayment(money: Int) = println("Thank you for paying for your meal")
}简单吧?此代码的工作方式与代码清单 13.5相同,只是写法不同。通过此更改,所有手动委托给 chef 的属性和函数都完全省略了,不出现在 Waiter 类体中,但它仍在委托它们。
这个类中唯一剩下的函数是 prepareBeverage() 和 acceptPayment()——这两个函数是 Waiter 自己处理的。换句话说,当你查看这个类时,你真的可以专注于 Waiter 类的独特之处,而不必看到它委托给 chef 的所有内容。
只需包含 by chef,Kotlin 就会为我们做很多工作。当你看代码清单 13.6的第一行时,你可以这样理解:
Waiter实现KitchenStaffby将其委托给chef。
或者,图示:
多个委托对象
好消息——餐厅刚刚开设了新的饮料吧,提供桃子冰茶和柠檬茶等精致饮品。服务员不再自己准备饮料,而是将饮料准备工作委托给调酒师。
首先,让我们为调酒师添加一个新的接口和类。我们还将用新的饮料选项更新 Beverage 枚举类。
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 }现在,我们希望 Waiter 将饮料准备委托给调酒师。首先,让我们给 Waiter 添加一个 bartender 属性,并手动委托给它,就像我们之前对 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")
}在这里,我们让 Kotlin 自动将 KitchenStaff 中的所有内容委托给 chef,而我们手动将 prepareBeverage() 委托给 Bartender。这很好,但 Kotlin 还允许我们使用 by 同时委托给多个对象,它的作用就像你期望的那样。只需在 BarStaff 后面添加 by bartender,如下所示:
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")
}通过这种方式,我们现在有了一个委托给两个不同类的类。就像代码清单 13.8一样,Waiter 类的类体只显示 Waiter 类的独特之处。所有委托样板代码都隐藏在那些简单的 by 声明后面!
重写委托调用
随着时间越来越晚,餐厅越来越拥挤,厨师正忙着为所有顾客准备食物。所以,厨师对服务员说:”沙拉很容易准备。从现在开始,如果你收到沙拉订单,就自己处理。我仍然会处理更精致的餐点。”
正如我们所见,当我们使用 Kotlin 的类委托时,接口中的所有属性和函数都会自动发送到指定的对象。但是,你也可以选择不自动委托接口中的一个或多个特定属性或函数。
让我们更新 Kotlin 代码,使 Waiter 可以自己准备沙拉。为此,我们可以在 Waiter 中再次包含 prepareEntree() 函数,并仅在需要时手动委托给 chef。如下所示:
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")
}当来自 KitchenStaff 的函数被包含在 Waiter 类中时,Kotlin 将使用该函数而不是将其发送给委托对象。但是,我们仍然可以选择手动委托给它,正如我们在代码清单 13.10中所做的那样。当顾客点除沙拉以外的任何东西时,我们手动委托给 chef。
所以,我们可以将 Kotlin 的类委托与手动委托结合起来。
处理冲突
当用餐很棒时,有时顾客会告诉服务员向厨师转达赞美。KitchenStaff 有一个名为 receiveCompliment() 的函数。所以,当这个函数在 Waiter 对象上被调用时,它会被发送给它的 chef 对象。在 Chef 类中,我们可以简单地打印出收到了赞美,如下所示:
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")
}当然,在 waiter 上调用这个函数会将其发送给 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!")不过,厨师并不是唯一可能收到赞美的人。调酒师泡的桃子冰茶非常棒,有时顾客也想向调酒师转达赞美!让我们更新 BarStaff 接口和 Bartender 类,以便他也能收到赞美。
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")
}现在,both Chef 和 Bartender 都包含一个名为 receiveCompliment() 的函数——而且它们具有相同的参数类型和返回类型。
所以,当我们的代码调用 waiter.receiveCompliment() 时,Kotlin 应该怎么做?它应该把赞美发送给 chef 对象,还是发送给 bartender 对象?或者两者都发?
这取决于你作为程序员的决定。事实上,在这种情况下,当你尝试对两个具有相同属性或函数的接口使用类委托时,你会收到一个类似这样的编译器错误:
类 ‘Waiter’ 必须重写在 Waiter 中定义的公共 open fun receiveCompliment(message: String): Unit,因为它继承了许多实现。
要解决这个问题,我们需要在 Waiter 类中包含这个函数。例如,我们可以检查 message 是否包含”entree”或”beverage”字样,并相应地手动委托给厨师或调酒师。然后,作为最后的手段,服务员可以直接接收赞美。
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")
}现在,顾客可以将赞美发送给厨师、调酒师或服务员!
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!")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!
到目前为止,我们已经使用类委托将属性调用和函数调用从 waiter 对象轻松转发到 chef 和 bartender 对象。虽然服务员、厨师和调酒师用具体例子说明了委托的概念,但委托也经常用于不同的目的——在多个特定类型之间共享通用代码。所以,在结束本章之前,让我们看看它是如何工作的!
用于通用和特定类型的委托
例如,所有的农场动物都想吃东西,尽管它们吃的具体食物可能因动物种类而异。在下一个代码清单中,我们有三个农场动物,每个都有自己 的 eat() 函数。
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!你可能再次注意到这里有一些样板代码。事实上,除了它们的名称之外,这些类之间唯一的区别就是字符串中的食物。eat() 函数是通用的,我们可以通过委托在这些特定的类之间共享它。这既快速又简单,所以我们从一个简单的"能吃东西"的接口开始吧。
interface Eater {
fun eat()
}让我们用一个发出咀嚼声的类来实现这个接口。
class Muncher(private val food: String) : Eater {
override fun eat() = println("Eating $food - munch, munch, munch!")
}通过使用类委托,我们可以让所有动物共享这个 eat() 函数实现。注意,现在重复的代码少了很多:
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!事实证明,这个农场里的猪比通常只是吃草的奶牛和鸡吃晚餐的速度快得多。所以,我们不用与它们共享 Muncher 代码,而可以直接在 Pig 中实现 eat() 函数,如下所示:
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!!!另外,我们可以通过创建另一个类并使用它作为 Pig 的委托对象来实现相同的效果。如下所示:
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")所以,类委托可用于在不同特定类型(如 Cow、Chicken 和 Pig 类)之间共享一些通用代码——比如如何 eat()。
这只是在不同类之间共享代码的一种方式。在下一章中,我们将介绍抽象和开放类,它们也可用于在多个类之间共享代码。
总结
在本章中,你学到了:
- 委托是什么。
- 如何从一个对象手动委托到另一个对象。
- 如何使用 Kotlin 的类委托自动委托。
- 如何重写特定函数,这些函数本来会被委托。
- 当两个委托为同一函数提供实现时,如何解决冲突。
- 如何将委托用于通用和特定类型。
如上所述,下一章将介绍抽象和开放类,它们也可用于在多个类之间共享代码。回头见!
-
样板代码一词来自印刷报纸行业的旧时代,当时出版集团会当地报纸发送已经准备好的金属印版上的故事。这些金属板看起来就像用于生产蒸汽锅炉的 plating。这些故事也常常是缺乏原创内容的 fluff 文章,这就是该术语获得其含义的方式。(”boilerplate,” Merriam-Webster.com Dictionary, https://www.merriam-webster.com/dictionary/boilerplate. 访问于 2023/1/23)。 ↩︎