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

若是用代碼實現擇偶標準的判斷邏輯,會很容易寫出又臭又長的代碼。本文經過 Kotin 獨有的語法特性「約定」來增長代碼的可讀性、複用性。算法

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

業務場景

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

業務分析

將候選人組織成列表,在候選人列表對象上調用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」)
    }
}
複製代碼

函數接收三個參數:post

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

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

函數體中調用了系統預約義的filter(),它的定義以下:ui

/**
 * 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。this

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

if (predicate(element))
複製代碼

這種語法在 Java 中沒有,即變量(參數),就好像調用函數同樣調用變量,這是一個特殊的變量,裏面存放着一個函數,因此這種語法的效果就是將參數傳遞給變量中的函數並執行它。在 Kotlin 中,稱爲叫約定code

約定

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基礎:用約定簡化相親

相關文章
相關標籤/搜索