Kotlin 中的密封类 优于 带标签的类
这是 dhl 的第 33 篇原创文章
我相信很多小伙伴对 Sealed Classes 已经并不陌生了,在之前的文章中我也分析过 Sealed Classes 原理,以及 Google 和很多开源项目为什么都在大量的使用它,如果你对 Sealed Classes 还不是很了解,可以前往查看 Kotlin Sealed 是什么?为什么 Google 都在用 主要内容如下:
Sealed Classes 原理分析? 枚举和抽象类都有那些局限性? 为什么枚举可以作为单例?枚举作为单例有那些优点? 分别在什么情况下使用枚举和 Sealed Classes? Sealed Classes 究竟是什么? 为什么 Sealed Classes 用于表示受限制的类层次结构? 为什么说 Sealed Classes 是枚举类的扩展? Sealed Classes 的子类可以表示不同状态的实例,那么在项目中如何使用? 禁止在 Sealed Classes 所定义的文件外使用, Kotlin 是如何做到的呢?
而今天这篇文章,我们主要从类层次结构来讨论一下 Sealed Classes(密封类) 和 Tagged Classes(标记类)的优缺点。在开始分析之前,我们先介绍一下什么是 Tagged Classes(标记类)以及都有那些缺点。
Tagged Classes 是什么
在一个类中包含一个指示操作的标记字段或者特征,方便在它们之间切换的类称为 Tagged Classes(标记类),在 Effective Java 中也指出了 Tagged Classes 存在很多问题,这里引用 Effective Java Item 23 中的一个案例来分析 Tagged Classes 存在的问题,这里用 Kotlin 重写了。
class Figure(
// 这个标签字段:用来表示图形的形状
val shape: Shape,
// 这个字段用于圆形
val radius: Double = 0.0,
// 这两个字段用于矩形
val length: Double = 0.0,
val width: Double = 0.0
) {
// 定义了两个形状 矩形、圆形
enum class Shape {
RECTANGLE, CIRCLE
}
// 计算当前图形的面积
fun area(): Double = when (shape) {
Shape.RECTANGLE -> length * width
Shape.CIRCLE -> Math.PI * (radius * radius)
else -> throw AssertionError(shape)
}
companion object {
fun createRectangle(radius: Double) {
Figure(
shape = Shape.RECTANGLE,
radius = radius
)
}
fun createCircle(length: Double, width: Double = 0.0) {
Figure(
shape = Shape.CIRCLE,
length = length,
width = width
)
}
}
}
正如你所见,代码中包含了很多模板代码,包括标记字段、切换语句、枚举等等,在一个类中包含了很多不同的操作,如果以后增加新的操作,有需要增加新的标记,实际情况这样的代码在项目中非常的常见,主要存在以下几个问题:
增加了很多模板代码 内存是非常稀缺的资源,当我们创建圆形的时候,与它无关的字段也要保留,增加当前类所占用的内存 降低了代码的可读性,类中混合了很多操作例如枚举、切换语句等等,为了保证对象正确的创建,通常需要用到工厂模式等等设计模式 如果增加新的图形,不得不去修改原有的代码结构 ......
那么有没有很好的替换方案,可以解决以上所有的问题,而且还可以在不修改原有的代码结构基础上增加新的图形,这就需要用到类的层次结构。
类的层次结构
无论是 Java 还是 Kotlin 我们都会使用类的层次结构代替标记类,而在 Kotlin 中我们常用 Sealed Classes 表示受限制的类层次结构, 在之前的文章 Kotlin Sealed 是什么? 中已经详细分析过 Sealed Classes。接下来一起来看一下如何使用 Sealed Classes 优化上面的代码。
sealed class Figure {
abstract fun area(): Double
class Rectangle(val length: Double, val width: Double) : Figure() {
override fun area(): Double = length * width
}
class Circle(val radius: Double) : Figure() {
override fun area(): Double = Math.PI * (radius * radius)
}
}
正如你所见,代码简洁干净了很多,不包含模板代码,并且类之间的职责分明,提高了代码的灵活性,完美的解决了上述所有的缺点。每个类中不包含无关的字段,同时在类中添加新的参数,并不会影响其他类。
如果我们需要增加新的图形,只需要新增加一个类即可,并不会破坏原有的代码结构,例如这里我们增加一个球形。
class Ball(val radius: Double) : Figure() {
override fun area(): Double = 4.0 * Math.PI * Math.pow(radius, 2.0)
}
不仅仅如此,Sealed Classes 结合 when 表达式一起使用会更加的方便,when 语句下的所有分支可以通过快捷键 Mac/Win/Linux:Alt + Enter
自动生成,如下所示。
fun Figure.Valida() {
when (this) {
is Figure.Ball -> {
println("I am Ball")
area()
}
is Figure.Circle -> {
println("I am Circle")
area()
}
is Figure.Rectangle -> {
println("I am Rectangle")
area()
}
}
}
在 Effective Java 中也说明了 Tagged classes(标记类)很少有适合的场景,但是往往在开发过程中,为了快速的开发一个功能,往往会忽略它所带来的影响,但是我们在做优化的时候,遇到这种 Tagged classes 是否可以考虑使用类的层次结构来代替,如果是 Kotlin 建议使用 Sealed Classes。
参考文章
Effective Java Item 23: Prefer class hierarchies to tagged classes Effective Kotlin Item 40: Prefer class hierarchies to tagged classes
全文到这里就结束了,如果有帮助欢迎 在看 、点赞 、分享 就是对我最大的鼓励
代码不止,文章不停
欢迎点击下方卡片关注我,持续分享最新的技术
推荐阅读:
最后推荐我一直在更新维护的项目和网站:
最新的 AndroidX Jetpack 相关组件的实战项目 以及 原理分析的文章
https://github.com/hi-dhl/AndroidX-Jetpack-PracticeLeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解,语言 Java 和 kotlin,包含多种解法、解题思路、时间复杂度、空间复杂度分析
剑指 offer:https://offer.hi-dhl.com
LeetCode:https://leetcode.hi-dhl.com最新 Android 10 源码分析系列文章
https://github.com/hi-dhl/Android10-Source-Analysis一系列国外的技术文章,每篇文章都会有译者思考部分,对原文的更加深入的分析
https://github.com/hi-dhl/Technical-Article-Translation「为互联网人而设计,国内国外名站导航」涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android 开发等等网址
https://site.51git.cn