如果用程式碼實現擇偶標準的判斷邏輯,會很容易寫出又臭又長的程式碼。本文通過 Kotin 獨有的語法特性“約定”來增加程式碼的可讀性、複用性。
這是 Kotlin 系列的第七篇,目錄詳見本文末尾。
業務場景
假設女生的擇偶標準如下:未婚且歲數比我大,如果對方是本地帥哥則對收入降低標準(年薪>10萬),如果對方非本地則要求歲數不能超過40歲,且年薪在40萬以上。(BMI 在 20 到 25 之間的定義為帥哥)
業務分析
將候選人組織成列表,在候選人列表物件上呼叫filter()
將篩選標準傳入即可。
- 將候選人抽象成
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//性別
)
複製程式碼
- 定義篩選函式
fun filterCandidate(man: List<Human>, women: Human, predicate: (Human, Human) -> Boolean) {
man.filter { predicate.invoke(it, women) }.forEach {
Log.v(“ttaylor”, “man = $it”)
}
}
複製程式碼
函式接收三個引數:
man
表示一組候選人women
表示客戶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
}
複製程式碼
你更喜歡哪個版本?