iT邦幫忙

2021 iThome 鐵人賽

DAY 19
2
Modern Web

Kotlin 怎麼操作資料庫?談談 Kotlin Exposed 框架系列 第 19

[Day 19] 突如其來的需求變更!來聊函數式編程

我們好不容易寫了 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

細心的讀者可能會發現:那如果我們要同時過濾 AdminAuthor 標籤的話,該怎麼辦呢?

別擔心!我們已經提供了足夠的材料給之後的工程師,之後維護的工程師一定可以組合出他所需要的邏輯。

之後的工程師只要這樣寫

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)))
	}
}

測試之後,我們就可以發現案例通過了!


上一篇
[Day 18] 重構我們的測試程式碼
下一篇
[Day 20] 調整一下我們的函數架構,談擴充函數和流暢介面
系列文
Kotlin 怎麼操作資料庫?談談 Kotlin Exposed 框架30

尚未有邦友留言

立即登入留言