减少Kotlin when 对字段的嵌套方案思考

1,750 阅读3分钟

为什么会有这样的想法?

最近开发新项目时遇到一个问题,项目中对与许多资源的字段嵌套层级很多,但是字段又都从一个接口返回,并且存在与一个JSON对象中。带来的问题:

  • when 嵌套层级过多
  • when 语句会有else得情况,代码不够安全
  • 后期维护成本很高

虽然使用kotlin,语法上已经能精简很多,但是这种需求难免让代码的可读性,维护性很差

实际的需求场景

对于用户资源的展示。首先资源类型分为:Image,Video,然后资源的类型分为normal,red,burning,redBurning,状态分为read,noRead。而服务器返回的相关字段都存储在一个JSON对象中,并且都属于同一级的属性。那么可以得到这样的一个data class

    data class ResBean(val url: String, val format: String, val type: String, val viewStatus: String){
            companion object { // 在伴生对象中定义一些常量来表示其中的字段类型
            const val RES_FORMAT_VIDEO = "video"
            const val RES_FORMAT_IMAGE = "image"
            const val RES_RORMAT_UNKONW = "unKnow"
    
            const val RES_TYPE_NORMAL = "normal"
            const val RES_TYPE_RED = "red"
            const val RES_TYPE_RED_BURNING = "red_burning"
            const val RES_TYPE_BURNING = "burning"
        }
    }

使用时的需求场景,现在需要在一个ViewPager中展示这些资源,首先需要考虑到时Format,其次还需要考虑到Type,那么你就会得到一个when嵌套者when的代码块

fun showByFormatAndType(res: ResBean) {
    when (res.format) {
        ResBean.RES_FORMAT_IMAGE ->
            when (res.type) {
                ResBean.RES_TYPE_RED -> showRedImage()
                ResBean.RES_TYPE_BURNING -> showBurningImage()
                ResBean.RES_TYPE_RED_BURNING -> showRedBurningImage()
                ResBean.RES_TYPE_NORMAL -> showNormalImage()
            }
        ResBean.RES_FORMAT_VIDEO ->
            when (res.type) {
                ResBean.RES_TYPE_RED -> showRedVideo()
                ResBean.RES_TYPE_BURNING -> showBurningVideo()
                ResBean.RES_TYPE_RED_BURNING -> showRedBurningVideo()
                ResBean.RES_TYPE_NORMAL -> showNormalVideo()
            }
        else -> showUnKown()
    }
}

这样的代码带来的困扰其实主要时易读性后后期维护上困难,如果你前期没有做很好的提出封装,那么就更痛苦了。试想一下如果需要添加一个新的format,或者一个type都不太舒服。而且你还得考虑很多得else情况

实现方案

在层级嵌套上,可以考虑将fromat和type做一个积类型,也就合二为一来减少多层嵌套,代码如下:

fun byFormatAndTypeTodo(res: ResBean) {
    with(res) {
        when {
            format == ResBean.RES_FORMAT_IMAGE && type == ResBean.RES_TYPE_NORMAL -> showNormalImage()
            format == ResBean.RES_FORMAT_IMAGE && type == ResBean.RES_TYPE_RED -> showRedImage()
            format == ResBean.RES_FORMAT_IMAGE && type == ResBean.RES_TYPE_BURNING -> showBurningImage()
            format == ResBean.RES_FORMAT_IMAGE && type == ResBean.RES_TYPE_RED_BURNING -> showRedBurningImage()
            format == ResBean.RES_FORMAT_VIDEO && type == ResBean.RES_TYPE_NORMAL -> showNormalVideo()
            format == ResBean.RES_FORMAT_VIDEO && type == ResBean.RES_TYPE_RED -> showRedVideo()
            format == ResBean.RES_FORMAT_VIDEO && type == ResBean.RES_TYPE_BURNING -> showBurningVideo()
            format == ResBean.RES_FORMAT_VIDEO && type == ResBean.RES_TYPE_RED_BURNING -> showRedBurningVideo()
            else -> showUnKown()
        }
    }
}

这样一来减少了层级得嵌套,但是牺牲了代码得可读性,最后考虑结合密封来提升可读性。首先定义一个密封类

sealed class ResWithFormatAndType {
    data class NormalImageRes(val res: ResBean) : ResWithFormatAndType()
    data class RedImageRes(val res: ResBean) : ResWithFormatAndType()
    data class BurningImageRes(val res: ResBean) : ResWithFormatAndType()
    data class RedAndBurningImageRes(val res: ResBean) : ResWithFormatAndType()
    data class NormalVideoRes(val res: ResBean) : ResWithFormatAndType()
    data class RedVideoRes(val res: ResBean) : ResWithFormatAndType()
    data class BurningVideoRes(val res: ResBean) : ResWithFormatAndType()
    data class RedAndBurningVideoRes(val res: ResBean) : ResWithFormatAndType()
    object UnkownRes : ResWithFormatAndType()
}

之后封装一个方法来获取相应

fun getResByFormatWithType(res: ResBean): ResWithFormatAndType = with(res) {
    when {
        format == ResBean.RES_FORMAT_IMAGE && type == ResBean.RES_TYPE_NORMAL -> NormalImageRes(res)
        format == ResBean.RES_FORMAT_IMAGE && type == ResBean.RES_TYPE_RED -> RedImageRes(res)
        format == ResBean.RES_FORMAT_IMAGE && type == ResBean.RES_TYPE_BURNING -> BurningImageRes(res)
        format == ResBean.RES_FORMAT_IMAGE && type == ResBean.RES_TYPE_RED_BURNING -> RedAndBurningImageRes(res)
        format == ResBean.RES_FORMAT_VIDEO && type == ResBean.RES_TYPE_NORMAL -> NormalVideoRes(res)
        format == ResBean.RES_FORMAT_VIDEO && type == ResBean.RES_TYPE_RED -> RedVideoRes(res)
        format == ResBean.RES_FORMAT_VIDEO && type == ResBean.RES_TYPE_BURNING -> BurningVideoRes(res)
        format == ResBean.RES_FORMAT_VIDEO && type == ResBean.RES_TYPE_RED_BURNING -> RedAndBurningVideoRes(res)
        else -> ResWithFormatAndType.UnkownRes
    }
}

最后在使用得地方

fun test() {
    when (getResWithFormatAndType(resBean) {
        is ResWithFormatAndType.NormalImageRes -> showNormalImage()
        is ResWithFormatAndType.BurningImageRes -> showBurningImage()
        is ResWithFormatAndType.RedImageRes -> showRedImage()
        is ResWithFormatAndType.RedAndBurningImageRes -> showRedBurningImage()
        is ResWithFormatAndType.BurningVideoRes -> showBurningVideo()
        is ResWithFormatAndType.NormalVideoRes -> showNormalVideo()
        is ResWithFormatAndType.RedAndBurningVideoRes -> showRedBurningVideo()
        is ResWithFormatAndType.RedVideoRes -> showRedVideo()
    }
}

最终解决后的方案个人觉得还不够完善,以前看到过Scala有模式匹配这样的概念,但是在Kotlin中没有找到最终的解决方案,也期许一下Kotlin能早日支持模式匹配这样的概念。