iT邦幫忙

2021 iThome 鐵人賽

DAY 20
1
Modern Web

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

[Day 20] 調整一下我們的函數架構,談擴充函數和流暢介面

上次我們提到,我們只需要實作

  • filterAdminTag()
  • filterAuthorTag()
  • filterRegistered()
  • filterCustomer()

就可以再之後透過組合的方式,達成我們想要的邏輯

當然觀察一下之後,我們就會發現這段程式碼重複度很高,可以做些許的調整:

fun removeTag(tags: List<Tag>, name: String): List<Tag> =  
    tags.filterNot { it.name == name }  
  
fun filterAdminTag(tags: List<Tag>): List<Tag> =  
    removeTag(tags, "Admin")  
  
fun filterAuthorTag(tags: List<Tag>): List<Tag> =  
    removeTag(tags, "Author")  
  
fun filterRegisteredTag(tags: List<Tag>): List<Tag> =  
    removeTag(tags, "Registered")  
  
fun filterCustomerTag(tags: List<Tag>): List<Tag> =  
    removeTag(tags, "Customer")

除了這個很好改善的問題之外,還有一個大問題,那就是我們組合過濾條件的方式

val filter: (List<Tag>) -> List<Tag> = {filterAuthorTag(filterAdminTag(it))}

只有兩個條件還好,如果四個條件都要加上去

val filter: (List<Tag>) -> List<Tag> = {filterCustomerTag(filterRegisteredTag(filterAuthorTag(filterAdminTag(it))))}

這實在是太醜了!沒有別的寫法嗎?

這邊我們就要介紹到流暢介面(Fluent Interface)

以及 Kotlin 的另一個觀念:擴充函數了!

流暢介面的目標

什麼是流暢介面呢?

如果我們將函數的定義方式,從 函數名稱(某物件) -> 某物件 的宣告方式,改成 某物件.函數名稱() -> 某物件 的話,我們的邏輯就會從

函數C(函數B(函數A(某物件)))

變成

某物件.函數A()
.函數B()
.函數C()

如果邏輯合理的話,後者的設計看起來會更加簡潔。

並且執行順序從原本的由內而外,改成由上自下,更不容易搞混執行的順序。

要讓程式不需要套用 {filterCustomerTag(filterRegisteredTag(filterAuthorTag(filterAdminTag(it))))} 的結構,我們會希望過濾的條件寫成類似

it
    .filterAdminTag()
    .filterAuthorTag()

的流程,看起來比較簡潔,也不用一直去數最後的 ) 有幾個

要達成這個目標,我們要將 filterAdminTag() 從一個

(List<Tag>) -> List<Tag> 的函數

變成一個 List<Tag>.() -> List<Tag> 的函數

雖然我們不可能直接去改 List<Tag> 的程式碼,來加入這個函數。

不過 Kotlin 允許我們直接擴充這個類別,宣告的方式也非常簡潔

fun List<Tag>.filterAdminTag(): List<Tag> =  
    this.filterNot { it.name == "Admin" }  
  
fun List<Tag>.filterAuthorTag(): List<Tag> =  
    this.filterNot { it.name == "Author" }  
  
fun List<Tag>.filterRegisteredTag(): List<Tag> =  
    this.filterNot { it.name == "Registered" }  
  
fun List<Tag>.filterCustomerTag(): List<Tag> =  
    this.filterNot { it.name == "Customer" }

當然,我們的 updateUsersTags() 也要做對應的調整

filter 的型態變成 List<Tag>.()->List<Tag>

預設值變成了 {this},代表這個 lambda 直接回傳原本的 List<Tag>

最後,呼叫方式從 filter(tags) 改成 tags.filter()

fun updateUsersTags(users: List<User>, tags: List<Tag>, filter: List<Tag>.()->List<Tag> = {this}) {  
    transaction {  
 users.forEach {  
 it.tags = SizedCollection(tags.filter())  
        }  
 }}

接著我們來看看測試怎麼調整。

我們的 測試更新標籤時如過濾Admin,結果應不出現Admin

可以變成

@Test
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)) {
			this.filterAdminTag()
		}
		assertThat(testUser.tags.toList(), not(hasItem(admin)))
	}
}

測試更新標籤時如過濾Admin和Author,結果應不出現Admin和Author

則變成了

@Test
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" }
		updateUsersTags(
			listOf(testUser),
			listOf(testTag, admin, author)
		) {
			this
				.filterAdminTag()
				.filterAuthorTag()
		}
		assertThat(testUser.tags.toList(), not(hasItem(admin)))
		assertThat(testUser.tags.toList(), not(hasItem(author)))
	}
}

運作測試後,確認原本的邏輯都成功通過,我們就可以休息一下了!

再次重構?

休息一下過後,我們看到剛剛被修改後,沒有用到的 filterTag()

fun removeTag(tags: List<Tag>, name: String): List<Tag> =  
    tags.filterNot { it.name == name }  
  
fun List<Tag>.filterAdminTag(): List<Tag> =  
    this.filterNot { it.name == "Admin" }  
  
fun List<Tag>.filterAuthorTag(): List<Tag> =  
    this.filterNot { it.name == "Author" }  
  
fun List<Tag>.filterRegisteredTag(): List<Tag> =  
    this.filterNot { it.name == "Registered" }  
  
fun List<Tag>.filterCustomerTag(): List<Tag> =  
    this.filterNot { it.name == "Customer" }

突然發現:不對呀!我們還可以這樣改

fun List<Tag>.removeTag(name: String): List<Tag> =  
    this.filterNot { it.name == name } 

這樣程式就可以寫成類似

this
	.removeTag("Admin")
	.removeTag("Author")

不僅未來彈性變高,而且程式碼還更少了!

我們來改看看 測試更新標籤時如過濾Admin,結果應不出現Admin

@Test
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)) {
			this.removeTag("Admin")
		}
		assertThat(testUser.tags.toList(), not(hasItem(admin)))
	}
}

測試更新標籤時如過濾Admin和Author,結果應不出現Admin和Author

再改成

@Test
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" }
		updateUsersTags(
			listOf(testUser),
			listOf(testTag, admin, author)
		) {
			this
				.removeTag("Admin")
				.removeTag("Author")
		}
		assertThat(testUser.tags.toList(), not(hasItem(admin)))
		assertThat(testUser.tags.toList(), not(hasItem(author)))
	}
}

執行之後,確認我們的測試都會通過,今天的重構就大功告成囉!


上一篇
[Day 19] 突如其來的需求變更!來聊函數式編程
下一篇
[Day 21] 測試的型態調整,談單元測試與整合測試
系列文
Kotlin 怎麼操作資料庫?談談 Kotlin Exposed 框架30

尚未有邦友留言

立即登入留言