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

唐子玄發表於2019-07-16

如果用程式碼實現擇偶標準的判斷邏輯,會很容易寫出又臭又長的程式碼。本文通過 Kotin 獨有的語法特性“約定”來增加程式碼的可讀性、複用性。

這是 Kotlin 系列的第七篇,目錄詳見本文末尾。

業務場景

假設女生的擇偶標準如下:未婚且歲數比我大,如果對方是本地帥哥則對收入降低標準(年薪>10萬),如果對方非本地則要求歲數不能超過40歲,且年薪在40萬以上。(BMI 在 20 到 25 之間的定義為帥哥)

業務分析

將候選人組織成列表,在候選人列表物件上呼叫filter()將篩選標準傳入即可。

  1. 將候選人抽象成data類:
data class Human(
        val age:Int, //年齡
        val annualSalary:Int,//年薪 
        val nativePlace:String, //祖籍
        val married:Boolean, //婚否
        val height:Int,//身高
        val weight:Int, //體重
        val gender:String//性別
)
複製程式碼
  1. 定義篩選函式
fun filterCandidate(man: List<Human>, women: Human, predicate: (Human, Human) -> Boolean) {
    man.filter { predicate.invoke(it, women) }.forEach {
        Log.v(“ttaylor”, “man = $it”)
    }
}
複製程式碼

函式接收三個引數:

  1. man表示一組候選人
  2. women表示客戶
  3. predicate表示該客戶的篩選標準。

其中第三個引數的型別是函式型別,用一個 lambda (Human, Human) -> Boolean來描述,它表示該函式接收兩個 Human 型別的輸入並輸出 Boolean。

函式體中呼叫了系統預定義的filter(),它的定義如下:

/**
 * Returns a list containing only elements matching the given [predicate].
 */
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    //'構建空列表例項'
    return filterTo(ArrayList<T>(), predicate)
}

/**
 * Appends all elements matching the given [predicate] to the given [destination].
 */
public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
    //'遍歷集合向列表中新增符合條件的元素'
    for (element in this) if (predicate(element)) destination.add(element)
    return destination
}
複製程式碼

filter()接收一個函式型別引數predicate,即篩選標準,該型別用 lambda 描述為(T) -> Boolean,即函式接收一個列表物件並返回一個 Boolean。

filter()遍歷原列表並將滿足條件的元素新增到新列表來完成篩選。在應用條件的時候用到了如下這種語法:

if (predicate(element))
複製程式碼

這種語法在 Java 中沒有,即變數(引數),就好像呼叫函式一樣呼叫變數,這是一個特殊的變數,裡面存放著一個函式,所以這種語法的效果就是將引數傳遞給變數中的函式並執行它。在 Kotlin 中,稱為叫約定

約定

plus約定

先看一個更簡單的約定:

data class Point( val x: Int, val y: Int){
    //'宣告plus函式'
    operator fun plus(other: Point): Point{
        return Point(x + other.x, y + other.y)
    }
}

val p1 = Point(1, 0)
val p2 = Point(2, 1)
//'將Point物件相加'
println(p1 + p2)
複製程式碼

上述程式碼的輸出是 Point(x=3, y=1)

Point類使用operator關鍵詞宣告瞭plus()函式,並在其中定義了相加演算法,這使得Point物件之間可以使用+來做加法運算,即原本的p1.plus(p2)可以簡寫成p1+p2

這個 case 中的約定可以描述成:通過operator關鍵詞的宣告,將plus()函式和+建立了一一對應的關係。Kotlin 中定了很多這樣的對應關係,比如times()對應*equals()對應==

約定將函式呼叫轉換成運算子呼叫,以讓程式碼更簡潔的同時也更具表現力。

invoke約定

在這些約定中有一個叫 invoke約定如果類使用operator宣告瞭invoke(),則該類的物件就可以當做函式一樣呼叫,即在變數後加上()

Kotlin 中 lambda 都會被編譯成實現了FunctionN介面的類,比如filter()中的predicate被定義成(T) -> Boolean,編譯時,它會變成這樣:

interface Function1<in T, out Boolean>{
    operator fun invoke(p1: T): Boolean
}
複製程式碼

Kotin 為所有的 lambda 實現了invoke約定,所以執行 lambda 有以下幾種方法:

//將 lambda 儲存在函式型別的變數中
val printx= {x: Int -> println(x)}
//'1. 使用invoke約定執行 lambda'
printx(1)
//'2. 呼叫invoke()函式執行 lambda'
printx.invoke(1)

//'3. 還有一種極端的方式:定義 lambda 的同時傳遞引數給它並執行'
{x: Int -> println(x)}(1)//輸出1
複製程式碼

回到剛才的業務函式filterTo()

fun filterCandidate(man: List<Human>, women: Human, predicate: (Human, Human) -> Boolean) {
    man.filter { predicate.invoke(it, women) }.forEach {
        Log.v(“ttaylor”, “man = $it”)
    }
}
複製程式碼

其實可以使用invoke約定來簡化程式碼如下:

fun filterCandidate(man: List<Human>, women: Human, predicate: (Human, Human) -> Boolean) {
    man.filter { predicate(it, women) }.forEach {
        Log.v(“ttaylor”, “man = $it”)
    }
}
複製程式碼

業務實現

來看下我們真正要簡化的東西:女生的篩選條件,即實現一個(Human, Human) -> Boolean)型別的 lambda :

{ man, women -> 
    !man.married && 
    man.age in women.age..30 &&
    man.nativePlace == woman.nativePlace &&
    man.annualSalary >= 10 && 
    (man.weight / ((man.height.toDouble() / 100)).pow(2)).toInt() in 20..25 
    || 
    !man.married && 
    man.age in women.age..40 &&
    man.nativePlace != woman.nativePlace && 
    man.annualSalary >= 40 
}
複製程式碼

通過合理換行和縮排,已經為這一長串邏輯表示式增加了些許可讀性,但一眼望去,腦袋還是暈的。而且運用了in約定來簡化程式碼:如果用operator宣告瞭contains()函式,則可以使用elment in list來簡化list.contains(elment)。所以在 Java 中,邏輯表示式會更加冗長。

其中有一些長且晦澀的表示式,增加了整體的理解難度。那就把它抽象成一個方法,然後取一個好名字,來降低一點理解難度,在所處的介面類(比如Activity)中定義兩個私有方法:

//'是否具有相同祖籍'
private fun isLocal(man1: Human, man2: Human): Boolean {
    return man1.nativePlace == man2.nativePlace
}

//'BMI 計算公式'
private fun bmi(man: Human): Int {
    return (man.weight / ((man.height.toDouble() / 100)).pow(2)).toInt()
}
複製程式碼

經過簡化之後程式碼如下:

{ man, women -> 
    !man.married && 
    man.age in women.age..30 &&
    isLocal(women, man) &&
    man.annualSalary >= 10 && 
    bmi(man) in 20..25 
    || 
    !man.married && 
    man.age in women.age..40 &&
    !isLocal(women, man) && 
    man.annualSalary >= 40 
}
複製程式碼

仔細一想女生的篩選標準其實可以概括成兩類男生:本地帥哥 或者 外地成功男士。所以可進一步抽象出兩個函式:

//'是否是本地帥哥'
private fun isLocalHandsome(man :Human, women: Human): Boolean{
    return (
        !man.married && 
        man.age in women.age..30 &&
        isLocal(women, man) &&
        man.annualSalary >= 10 && 
        bmi(man) in 20..25  
    )
}

//'是否是外地成功男士'
private fun isRemoteSuccessful(man :Human, women: Human): Boolean{
    return (
        !man.married && 
        man.age in women.age..40 &&
        !isLocal(women, man) && 
        man.annualSalary >= 40 
    )
}
複製程式碼

於是乎,程式碼簡化如下:

{ man, women -> isLocalHandsome(man, women) || isRemoteSuccessful(man, women) }
複製程式碼

為簡化程式碼付出的代價是在介面類中增加了 4 個私有函式。理論上介面中應該只包含View及對它的操作才對,這 4 個私有函式顯得格格不入。而且如果另一個女生還需要找本地帥哥,這段寫在介面中的邏輯如何複用?

那就把這四個方法都寫到Human類中,這其實是個不錯的辦法,但如果各式各樣的需求不斷增多,那Human類中的方法將膨脹。

其實更好的做法是用invoke約定來統籌篩選條件:

//'定義篩選標準類繼承自函式型別(Human)->Boolean'
class HandsomeOrSuccessfulPredicate(val women: Human) : (Human) -> Boolean {
    //'定義invoke約定'
    override fun invoke(human: Human): Boolean = human.isLocalHandsome(women) || human.isRemoteSuccessful(women)

    //'為Human定義擴充套件函式計算BMI'
    private fun Human.bmi(): Int = (weight / ((height.toDouble() / 100)).pow(2)).toInt()

    //'為Human定義擴充套件函式判斷是否同一祖籍'
    private fun Human.isLocal(human: Human): Boolean = nativePlace == human.nativePlace

    //'為Human定義擴充套件函式判斷是否是本地帥哥'
    private fun Human.isLocalHandsome(human: Human): Boolean = (
            !married &&
            age in human.age..30 &&
            isLocal(human) &&
            annualSalary >= 10 &&
            bmi() in 20..25
    )

    //'為Human定義擴充套件函式判斷是否是外地成功人士'
    private fun Human.isRemoteSuccessful(human: Human): Boolean = (
            !married &&
            age in human.age..40 &&
            !isLocal(human) &&
            annualSalary >= 40
    )
}
複製程式碼

當定義類繼承自函式型別時,IDE 會提示你重寫invoke()方法,將女生篩選標準的完整邏輯寫在invoke()方法體內,將和篩選標準有關的細分邏輯都作為Human的擴充套件函式寫在類體內。

雖然新增了一個類,但是,它將複雜的判定條件拆分成多個語義更清晰的片段,使程式碼更容易理解和修改,並且將片段歸總在一個類中,這樣篩選標準就可以以一個類的身份到處使用。

為篩選準備一組候選人:

private val man = listOf(
        Human(age = 30, annualSalary = 40, nativePlace = "山東", married = false, height = 170, weight = 80, gender = "male"),
        Human(age = 22, annualSalary = 23, nativePlace = "浙江", married = true, height = 189, weight = 90, gender = "male"),
        Human(age = 40, annualSalary = 13, nativePlace = "上海", married = true, height = 181, weight = 70, gender = "male"),
        Human(age = 25, annualSalary = 70, nativePlace = "江蘇", married = false, height = 167, weight = 66, gender = "male"))
複製程式碼

然後開始篩選:

fun filterCandidate(man: List<Human>, predicate: (Human) -> Boolean) {
    man.filter (predicate).forEach { Log.v("ttaylor","man = $it") }
}

//'進行篩選'
filterCandidate(man,HandsomeOrSuccessfulPredicate(women))
複製程式碼

修改了下filterCandidate(),這次它變得更加簡潔了,只需要兩個引數。

將它和最開始的版本做一下對比:

fun filterCandidate(man: List<Human>, women: Human, predicate: (Human, Human) -> Boolean) {
    man.filter { predicate(it, women) }.forEach {
        Log.v("ttaylor", "man = $it")
    }
}

filterCandidate(man, women) { man, women -> 
    !man.married && 
    man.age in women.age..30 &&
    man.nativePlace == woman.nativePlace &&
    man.annualSalary >= 10 && 
    (man.weight / ((man.height.toDouble() / 100)).pow(2)).toInt() in 20..25 
    || 
    !man.married && 
    man.age in women.age..40 &&
    man.nativePlace != woman.nativePlace && 
    man.annualSalary >= 40 
}
複製程式碼

你更喜歡哪個版本?

推薦閱讀

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

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

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

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

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

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

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

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

相關文章