Kotlin實戰:使用DSL構建結構化API去掉冗餘的介面方法

唐子玄發表於2019-07-02

這是該系列的第四篇,系列文章目錄如下:

  1. Kotlin基礎:白話文轉文言文般的Kotlin常識

  2. Kotlin基礎:望文生義的Kotlin集合操作

  3. Kotlin實戰:用實戰程式碼更深入地理解預定義擴充套件函式

  4. Kotlin實戰:使用DSL構建結構化API去掉冗餘的介面方法

  5. Kotlin基礎:屬性也可以是抽象的

  6. Kotlin進階:動畫程式碼太醜,用DSL動畫庫拯救,像說話一樣寫程式碼喲!

  7. Kotlin基礎:用約定簡化相親

  8. Kotlin基礎 | 2 = 12 ?泛型、類委託、過載運算子綜合應用

即使不需要 Java 介面中的某些方法,也必須將其implements,然後保持其為空實現,傻傻地處在那。利用 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( 指向 age 時可以省略 this )

讓我們使用這樣的技巧來解決“必須實現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 相比,它顯得太簡單了)。

下一篇會進一步使用 DSL 的思想將 Android 整套構建動畫的介面重構成結構化的程式碼。到時候就可以使用這樣的程式碼來構建動畫:

animSet {
    objectAnim {
        target = textView
        scaleX = floatArrayOf(1.0f,1.3f)
        scaleY = scaleX
        duration = 300L
        interpolator = LinearInterpolator()
    } with objectAnim {
        target = button
        translationX = floatArrayOf(0f,100f)
        duration = 300
        interpolator = LinearInterpolator()
    } before anim{
        values = intArrayOf(ivRight,screenWidth)
        action = { value -> imageView.right = value as Int }
        duration = 400
        interpolator = LinearInterpolator()
    }
    onEnd = Toast.makeText(activity,"animation end",Toast.LENGTH_SHORT).show()
    start()
}
複製程式碼

相關文章