我們好不容易寫了 userAddTag()
和 updateUsersTags()
的邏輯,突然又出現了新需求!
這次需求單位希望 updateUsersTags()
後面可以加上過濾的功能,在套用時可以避免某些 tag
被錯誤的套上。
比方說,在一般更新時,我們不會將 admin
標籤套用在一般使用者上,這應該是只能套用在程式管理員身上的標籤才對。
並且需求單位希望能根據情況,過濾掉以下一個或多個標籤:
admin
author
registered
customer
那麼,我們能不能調整看看 updateUsersTags()
來滿足這個需求呢?
一開始我們可能會想:這有什麼難的?再寫四個函數
updateUsersTagsWithoutAdmin
updateUsersTagsWithoutAuthor
updateUsersTagsWithoutRegistered
updateUsersTagsWithoutCustomer
不就好了嗎?
不過我們仔細一看,發現到需求內的一個小細節:「一個或多個標籤」
這下就不能這樣做了,不然豈不是變成了
updateUsersTagsWithoutAdminAndAuthor
updateUsersTagsWithoutAdminAndAuthorAndRegistered
這時我們就要換個思考方式,想想怎麼在一個函數內,滿足所有的過濾條件了!
這時候,我們就要善用函數式編程的概念,使用函數的思考領域來架構邏輯!
{:height="300px" width="400px"}
所謂函數式編程,簡單的說,就是將函數視為一個個體,可以當作參數傳到其他函數裏面去,也可以變成回傳值
首先我們想到,如果我們將過濾條件本身,設計成一個一個的函數。那麼我們原先的 updateUsersTags()
就不用修改太多,只需要在後面加入一個 過濾條件
的函數,並套用到要加入的 tags
上,不就可以了嗎?
我們來試著調整一下 updateUsersTags()
fun updateUsersTags(users: List<User>, tags: List<Tag>, filter: (List<Tag>) -> List<Tag>) {
transaction {
users.forEach {
it.tags = SizedCollection(filter(tags))
}
}
}
這樣改了之後,嘗試執行一下測試。我們就會發現一件很糟糕的事情:缺少 filter
參數的話,原先的測試都無法通過了!
我們可以加上一個預設值,如果我們沒有設定任何 filter
,那麼就不會濾除任何 tag
,全部都會通過
fun updateUsersTags(users: List<User>, tags: List<Tag>, filter: (List<Tag>) -> List<Tag> = { it }) {
transaction {
users.forEach {
it.tags = SizedCollection(filter(tags))
}
}
}
運作一下測試,確認全部都會通過
然後我們加上過濾掉 Admin
標籤的函數 filterAdminTag()
fun filterAdminTag(tags: List<Tag>): List<Tag> =
tags.filterNot { it.name == "Admin" }
再加上過濾掉 Author
標籤的函數 filterAuthorTag()
fun filterAdminTag(tags: List<Tag>): List<Tag> =
tags.filterNot { it.name == "Admin" }
接著我們就可以測試囉!
加上測試案例 測試更新標籤時如過濾Admin,結果應不出現Admin
這邊我們利用 ::
符號,直接呼叫 filterAdminTag()
函數
fun `測試更新標籤時如過濾Admin,結果應不出現Admin`() {
initDatabase()
transaction {
val (testUser) = makeTestUsers(1)
val (testTag) = makeTestTags(1)
val admin = Tag.new { name="Admin" }
updateUsersTags(listOf(testUser), listOf(testTag, admin), ::filterAdminTag)
assertThat(testUser.tags.toList(), not(hasItem(admin)))
}
}
運作一下測試,我們就會發現這段程式確實通過了,testUser.tags
內確實不包含 admin
!
細心的讀者可能會發現:那如果我們要同時過濾 Admin
和 Author
標籤的話,該怎麼辦呢?
別擔心!我們已經提供了足夠的材料給之後的工程師,之後維護的工程師一定可以組合出他所需要的邏輯。
之後的工程師只要這樣寫
val filter: (List<Tag>) -> List<Tag> = {filterAuthorTag(filterAdminTag(it))}
就可以將兩個函數組合成另一個新的函數
我們來嘗試看看測試案例 測試更新標籤時如過濾Admin和Author,結果應不出現Admin和Author
fun `測試更新標籤時如過濾Admin和Author,結果應不出現Admin和Author`() {
initDatabase()
transaction {
val (testUser) = makeTestUsers(1)
val (testTag) = makeTestTags(1)
val admin = Tag.new { name="Admin" }
val author = Tag.new { name="Author" }
val filter: (List<Tag>) -> List<Tag> = {filterAuthorTag(filterAdminTag(it))}
updateUsersTags(
listOf(testUser),
listOf(testTag, admin, author), filter)
assertThat(testUser.tags.toList(), not(hasItem(admin)))
assertThat(testUser.tags.toList(), not(hasItem(author)))
}
}
測試之後,我們就可以發現案例通過了!