Kotlin实战:构建DSL简化代码

作者: 唐子玄 | 来源:发表于2019-07-02 12:25 被阅读10次

    这是该系列的第四篇,系列文章目录如下:

    1. Kotlin基础:白话文转文言文般的Kotlin常识

    2. Kotlin基础:望文生义的Kotlin集合操作

    3. Kotlin实战:用实战代码更深入地理解预定义扩展函数

    4. Kotlin实战:构建DSL简化代码

    这篇将在上一篇的代码基础上新增功能,并利用自定义的 DSL 来简化代码。

    引子

    上篇中利用apply()语法来简化组合动画的构建过程,代码如下:

    val span = 300
    AnimatorSet().apply {
        playTogether(
                ObjectAnimator.ofPropertyValuesHolder(
                        tvTitle,
                        PropertyValuesHolder.ofFloat("alpha", 0f, 1.0f),
                        PropertyValuesHolder.ofFloat("translationY", 0f, 100f)).apply {
                    interpolator = AccelerateInterpolator()
                    duration = span
                },
                ObjectAnimator.ofPropertyValuesHolder(
                        ivAvatar,
                        PropertyValuesHolder.ofFloat("alpha", 1.0f, 0f),
                        PropertyValuesHolder.ofFloat("translationY", 0f,100f)).apply {
                    interpolator = AccelerateInterpolator()
                    duration = span
                }
        )
        start()
    }
    

    如果动画的时间被拉长,需要在其暂停时显示 toast 提示,并且在结束时展示视图A,代码需做如下修改:

    val span = 5000
    AnimatorSet().apply {
        playTogether(
                ObjectAnimator.ofPropertyValuesHolder(
                        tvTitle,
                        PropertyValuesHolder.ofFloat("alpha", 0f, 1.0f),
                        PropertyValuesHolder.ofFloat("translationY", 0f, 100f)).apply {
                    interpolator = AccelerateInterpolator()
                    duration = span
                },
                ObjectAnimator.ofPropertyValuesHolder(
                        ivAvatar,
                        PropertyValuesHolder.ofFloat("alpha", 1.0f, 0f),
                        PropertyValuesHolder.ofFloat("translationY", 0f,100f)).apply {
                    interpolator = AccelerateInterpolator()
                    duration = span
                }
        )
        addPauseListener(object :Animator.AnimatorPauseListener{
            override fun onAnimationPause(animation: Animator?) {
                Toast.makeText(context,"pause",Toast.LENGTH_SHORT).show()
            }
    
            override fun onAnimationResume(animation: Animator?) {
            }
    
        })
        addListener(object : Animator.AnimatorListener{
            override fun onAnimationRepeat(animation: Animator?) {
            }
    
            override fun onAnimationEnd(animation: Animator?) {
                showA()
            }
    
            override fun onAnimationCancel(animation: Animator?) {
            }
    
            override fun onAnimationStart(animation: Animator?) {
            }
        })
        start()
    }
    

    这一段apply()有点过长了,严重降低了它的可读性。罪魁祸首是 java 接口。虽然只用到接口中的一个方法,但却必须将其余的方法保留空实现。

    有没有什么办法只实现想要的方法,去掉不用的方法?

    利用 kotlin 的自定义 DSL 就可以实现。

    DSL

    DSL = domain specific language,即“特定领域语言”,与它对应的一个概念叫“通用编程语言”,通用编程语言有一系列完善的能力来解决几乎所有能被计算机解决的问题,像 Java 就属于这种类型。而特定领域语言只专注于特定的任务,比如 SQL 只专注于操纵数据库,HTML 只专注于表述超文本。

    既然通用编程语言能够解决所有的问题,那为啥还需要特定领域语言?因为它可以使用比通用编程语言中等价代码更紧凑的语法来表达特定领域的操作。比如当执行一条 SQL 语句时,不需要从声明一个类及其方法开始。

    更紧凑的语法意味着更简洁的 API。应用程序中每个类都提供了其他类与之交互的可能性,确保这些交互易于理解并可以简洁地表达,对于软件的可维护性至关重要。

    DSL 有一个普通API不具备特征:DSL 具有结构。而接收者的lambda使得构建结构化的 API 变得容易

    带接收者的 lambda

    它是一种特殊的 lambda,是 kotlin 中特有的。可以把它理解成“为接收者声明的一个匿名的扩展函数”。(扩展函数是一种在类体外为类添加功能的特性)

    带接收者的lambda的函数体除了能访问其所在类的成员外,还能访问接收者的所有非私有成员,这个特性是它能够轻松的构建结构。

    当带接收者的 lambda 配合高阶函数时,构建结构化的 API 就变得易如反掌。

    高阶函数

    它是一种特殊的函数,它的参数或者返回值是另一个函数。

    比如集合的扩展函数filter()就是一个高阶函数:

    //'filter的参数是一个带接收的lambda'
    public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
        return filterTo(ArrayList<T>(), predicate)
    }
    

    可以使用它来过滤集合中的元素:

    students.filter { age > 18 }
    

    这样就是一种结构化 API 的调用(在 java 中看不到),虽然这种结构得益于 kotlin 的一个约定(如果函数只有一个参数且它是 lambda,则可以省略函数参数列表的括号)。但更关键的是 lambda 的内部,得益于带接收者的lambdaage > 18运行在一个和其调用方不同的上下文中,在这个上下文中,可以轻松的访问到Student的成员Student.age

    让我们使用这样的技巧来解决“必须实现java所有接口”的问题

    构建 DSL 解决 java 接口问题

    1. 新建类用于存放各个接口的实现
    class AnimatorListenerImpl {
        var onRepeat: ((Animator) -> Unit)? = null
        var onEnd: ((Animator) -> Unit)? = null
        var onCancel: ((Animator) -> Unit)? = null
        var onStart: ((Animator) -> Unit)? = null
    }
    

    它包含四个成员,每个成员的类型都是函数类型。看一下Animator.AnimatorListener的定义就能理解AnimatorListenerImpl的用意:

    public static interface AnimatorListener {
        void onAnimationStart(Animator animation);
        void onAnimationEnd(Animator animation);
        void onAnimationCancel(Animator animation);
        void onAnimationRepeat(Animator animation);
    }
    

    该接口中的每个方法都接收一个Animator参数并返回空值,用 lambda 可以表达成 (Animator) -> Unit。所以AnimatorListenerImpl将接口中的四个方法的实现都保存在函数变量中,并且实现是可空的。

    1. 为 Animator 定义一个高阶扩展函数
    fun AnimatorSet.addListener(action: AnimatorListenerImpl.() -> Unit) {
        AnimatorListenerImpl().apply { action }.let { builder ->
            //'将回调实现委托给AnimatorListenerImpl的函数类型变量'
            addListener(object : Animator.AnimatorListener {
                override fun onAnimationRepeat(animation: Animator?) {
                    animation?.let { builder.onRepeat?.invoke(animation) }
                }
    
                override fun onAnimationEnd(animation: Animator?) {
                    animation?.let { builder.onEnd?.invoke(animation) }
                }
    
                override fun onAnimationCancel(animation: Animator?) {
                    animation?.let { builder.onCancel?.invoke(animation) }
                }
    
                override fun onAnimationStart(animation: Animator?) {
                    animation?.let { builder.onStart?.invoke(animation) }
                }
            })
        }
    }
    

    Animator定义了扩展函数addListener(),该函数接收一个带接收者的lambdaaction

    扩展函数体中构建了AnimatorListenerImpl实例并紧接着应用了action,最后为Animator设置动画监听器并将其中态回调的实现委托给AnimatorListenerImpl实例。

    1. 使用自定义的 DSL
      然后就可以将本文开头的代码改写成这个样子:
    val span = 5000
    AnimatorSet().apply {
        playTogether(
                ObjectAnimator.ofPropertyValuesHolder(
                        tvTitle,
                        PropertyValuesHolder.ofFloat("alpha", 0f, 1.0f),
                        PropertyValuesHolder.ofFloat("translationY", 0f, 100f)).apply {
                    interpolator = AccelerateInterpolator()
                    duration = span
                },
                ObjectAnimator.ofPropertyValuesHolder(
                        ivAvatar,
                        PropertyValuesHolder.ofFloat("alpha", 1.0f, 0f),
                        PropertyValuesHolder.ofFloat("translationY", 0f,100f)).apply {
                    interpolator = AccelerateInterpolator()
                    duration = span
                }
        )
        addPauseListener{
            onPause = { Toast.makeText(context,"pause",Toast.LENGTH_SHORT).show() }
        }
        addListener { 
            onEnd = { showA() } 
        }
        start()
    }
    

    (省略了扩展函数addPauseListener()的定义,它和addListener()是类似的。)

    得益于带接收者的lambda,可以轻松地为AnimatorListenerImpl的成员onEnd赋值,这段逻辑会在动画结束时被调用。

    这段调用拥有自己独特的结构,它解决了“必须实现全部 java 接口”这个特定的问题,所以它可以称得上是一个自定义 DSL 。(当然和 SQL 相比,它显得太简单了)

    相关文章

      网友评论

        本文标题:Kotlin实战:构建DSL简化代码

        本文链接:https://www.haomeiwen.com/subject/hrrgcctx.html