Logo
Section 1 / 9

装饰器设计模式

学习如何在 Kotlin 中实现装饰器设计模式和 Elvis 运算符。

导入

装饰器设计模式

学习如何在 Kotlin 中实现装饰器设计模式和 Elvis 运算符。

在原型设计模式中,我们可以创建具有略微(或明显)不同数据的类实例。但这引发了一个问题:

如果我们希望创建一组行为略有不同的类,该怎么办?

由于 Kotlin 中的函数是一等公民,我们可以使用原型设计模式来实现这一目标。毕竟,JavaScript 就成功地通过这种方式实现了具有轻微差异行为的一组类。但针对同样的问题,还有另一种解决方法。毕竟,设计模式本质上是关于解决问题的方法。

通过实现 装饰器 设计模式,我们可以让代码的使用者自行指定他们希望添加的功能。

增强一个类

假设我们有一个非常简单的类,用于记录《星际迷航》宇宙中的所有舰长及其所属飞船:

一个开放的《星际迷航》仓库类,包含获取和添加飞船舰长的方法

1open class StarTrekRepository { 2 private val starshipCaptains = mutableMapOf("USS Enterprise" to "Jean-Luc Picard") 3 4 open fun getCaptain(starshipName: String): String { 5 return starshipCaptains[starshipName] ?: "Unknown" 6 } 7 8 open fun addCaptain(starshipName: String, captainName: String) { 9 starshipCaptains[starshipName] = captainName 10 } 11}

某天,你的负责人——抱歉,应该是 Scrum 主管——向你提出一个紧急需求:从现在起,每次有人查询舰长时,我们都必须将该操作记录到控制台。但有个限制:你不能直接修改 StarTrekRepository 类。因为还有其他使用者在使用这个类,而他们并不需要这种日志功能。

但在深入探讨这个问题之前,让我们先关注一下这个类中的一个特殊之处——也就是 getCaptain 函数中出现的一个奇怪操作符。

Elvis 操作符

我们知道,Kotlin 不仅是强类型语言,还是一种空安全语言。

如果像我们的例子中那样,某个键在映射中没有对应的值,会发生什么?

在处理映射时,一种选择是使用 Kotlin 提供的 getOrDefault 方法。这在当前情况下可能是可行的,但在需要处理可能为 null 的值时,这种方法并不适用。另一种选择是使用 Elvis 操作符(?:)。

如果你好奇这个操作符为何叫“Elvis”,它确实有点像猫王埃尔维斯·普雷斯利的发型。 Elvis 操作符的作用是在接收到 null 值时提供一个默认值。再看一遍 `getCaptain` 函数,就能明白它是如何实现的。

...
Elvis operator

同一函数的“去糖化”形式如下:

如果映射中存在该飞船的舰长,则返回其姓名

1return if (starshipCaptains[starshipName] == null) 2 "Unknown" 3else starshipCaptains[starshipName]

由此可见,这个操作符大大减少了重复代码的编写量。

继承的问题

回到当前任务。由于我们的类及其方法被声明为 open,我们可以继承该类并重写所需的方法:

继承自 StarTrekRepository 并在调用 getCaptain 前记录日志的子类

1class LoggingGetCaptainStarTrekRepository : StarTrekRepository() { 2 override fun getCaptain(starshipName: String): String { 3 println("Getting captain for $starshipName") 4 return super.getCaptain(starshipName) 5 } 6}

这非常简单!尽管这个类的名字变得越来越长了。

注意这里我们通过 super 关键字将调用委托给父类的实现。然而第二天,你的老板(抱歉,Scrum 主管)又提出了另一个需求:添加舰长时,必须检查其姓名长度不超过 15 个字符。顺便说一句,这个功能不应与之前的日志功能相关联。有时我们只需要日志,有时只需要验证。因此,新的类看起来如下:

验证添加舰长的子类

1class ValidatingAddCaptainStarTrekRepository : StarTrekRepository() { 2 override fun addCaptain(starshipName: String, captainName: String) { 3 if (captainName.length > 15) { 4 throw RuntimeException("$captainName is longer than 15 characters!") 5 } 6 super.addCaptain(starshipName, captainName) 7 } 8}

又完成了一项任务。

但第二天,又出现了新需求:在某些情况下,我们需要 StarTrekRepository 同时启用日志记录和验证功能。看来我们得把它命名为 LoggingGetCaptainValidatingAddCaptainStarTrekRepository 了。

这类问题非常常见,也清楚地表明设计模式或许能帮我们解决。

装饰器设计模式的目的,正是动态地为对象添加新行为。在我们的例子中,日志记录和验证是两种我们有时希望应用、有时不希望应用的行为。

我们将从把 StarTrekRepository 转换为接口开始:

声明获取和添加飞船舰长的方法

1interface StarTrekRepository { 2 fun getCaptain(starshipName: String): String 3 fun addCaptain(starshipName: String, captainName: String) 4}

然后,我们使用之前相同的逻辑来实现这个接口:

StarTrekRepository 接口的实现

1class DefaultStarTrekRepository : StarTrekRepository { 2 private val starshipCaptains = mutableMapOf("USS Enterprise" to "Jean-Luc Picard") 3 4 override fun getCaptain(starshipName: String): String { 5 return starshipCaptains[starshipName] ?: "Unknown" 6 } 7 8 override fun addCaptain(starshipName: String, captainName: String) { 9 starshipCaptains[starshipName] = captainName 10 } 11}

接下来,不再扩展具体实现类,而是实现接口,并使用另一个关键字 by:

在调用 getCaptain 前记录消息的装饰器类

1class LoggingGetCaptain(private val repository: StarTrekRepository) : StarTrekRepository by repository { 2 override fun getCaptain(starshipName: String): String { 3 println("Getting captain for $starshipName") 4 return repository.getCaptain(starshipName) 5 } 6}

by 关键字将接口的实现委托给另一个对象。因此,LoggingGetCaptain 类无需手动实现接口中声明的任何函数,它们都由被包装的对象自动提供。

在这个例子中,最难理解的部分是类签名。装饰器设计模式的核心要求如下:

  • 我们需要能够接收要装饰的对象;
  • 我们需要保留对该对象的引用;
  • 当装饰器被调用时,我们需要决定是否要改变所持对象的行为,还是将其调用委托出去;
  • 我们需要能够提取出一个接口,或者已有接口由库作者提供。

注意,我们不再使用 super 关键字。如果尝试使用,将无法工作,因为我们现在是在实现一个接口,而不是继承一个类。取而代之的是,我们使用对被包装对象的引用。

为了确保我们真正理解这一模式,我们来实现第二个装饰器:

在调用 addCaptain 前验证舰长姓名长度的装饰器类

1class ValidatingAdd(private val repository: StarTrekRepository) : StarTrekRepository by repository { 2 private val maxNameLength = 15 3 4 override fun addCaptain(starshipName: String, captainName: String) { 5 require(captainName.length < maxNameLength) { 6 "$captainName name is longer than or equal to the $maxNameLength characters!" 7 } 8 repository.addCaptain(starshipName, captainName) 9 } 10}

与之前的 ValidatingAddCaptainStarTrekRepository 实现相比,唯一的区别是我们使用了 require 函数而非 if 表达式。这通常更具可读性,并且当表达式为 false 时会抛出 IllegalArgumentException。

现在来看看它的运行效果:

创建 DefaultStarTrekRepository、ValidatingAdd 和 LoggingGetCaptain 的实例

1val starTrekRepository = DefaultStarTrekRepository() 2val withValidating = ValidatingAdd(starTrekRepository) 3val withLoggingAndValidating = LoggingGetCaptain(withValidating) 4 5withLoggingAndValidating.getCaptain("USS Enterprise") 6withLoggingAndValidating.addCaptain("USS Voyager", "Kathryn Janeway")

最后一行会抛出异常:

1> Kathryn Janeway name is longer than or equal to the 15 characters!

我们可以运行下面的代码进行练习:

1import java.lang.IllegalStateException 2 3fun main() { 4 val starTrekRepository = DefaultStarTrekRepository() 5 val withValidating = ValidatingAdd(starTrekRepository) 6 val withLoggingAndValidating = LoggingGetCaptain(withValidating) 7 8 withLoggingAndValidating["USS Enterprise"] 9 10 try { 11 // 抛出异常:Kathryn Janeway name is longer than 15 characters! 12 withLoggingAndValidating["USS Voyager"] = "Kathryn Janeway" 13 } catch (e: IllegalArgumentException) { 14 println(e) 15 } 16 17 //println(withLoggingAndValidating is LoggingGetCaptain) // 这是顶层装饰器,没问题 18 //println(withLoggingAndValidating is StarTrekRepository) // 这是我们实现的接口,也没问题 19 //println(withLoggingAndValidating is ValidatingAdd) // 我们包装了这个类,但编译器无法验证 20 //println(withLoggingAndValidating is DefaultStarTrekRepository) // 我们包装了这个类,但编译器无法验证 21} 22 23interface StarTrekRepository { 24 operator fun get(starshipName: String): String 25 operator fun set(starshipName: String, captainName: String) 26} 27 28class DefaultStarTrekRepository : StarTrekRepository { 29 private val starshipCaptains = mutableMapOf("USS Enterprise" to "Jean-Luc Picard") 30 31 override fun get(starshipName: String): String { 32 return starshipCaptains[starshipName] ?: "Unknown" 33 } 34 35 override fun set(starshipName: String, captainName: String) { 36 starshipCaptains[starshipName] = captainName 37 } 38} 39 40class LoggingGetCaptain(private val repository: StarTrekRepository) : StarTrekRepository by repository { 41 override fun get(starshipName: String): String { 42 println("Getting captain for $starshipName") 43 return repository[starshipName] 44 } 45} 46 47class ValidatingAdd(private val repository: StarTrekRepository) : StarTrekRepository by repository { 48 private val maxNameLength = 15 49 override fun set(starshipName: String, captainName: String) { 50 require(captainName.length < maxNameLength) { 51 "$captainName name is longer than or equal to the $maxNameLength characters!" 52 } 53 repository[starshipName] = captainName 54 } 55}

正如我们所见,这种模式允许我们组合行为,正符合我们的初衷。

使用创建型设计模式

操作符重载与装饰器设计模式的注意事项