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

执行之後,确认我们的测试都会通过,今天的重构就大功告成罗!


<<:  用React刻自己的投资Dashboard Day10 - 用useCallback hook帮你记住函式

>>:  DOM 节点选取

3面向谈ML产品与软件产品的相异处

在过去的5-7年当中,ML已经不再只限於研究人员能够接触、使用,越来越多的AI/ML工具以及产品出现...

Day 13: 时间管理、预估、压力 (待改进中... )

CH9: 时间管理 「专业开发人员同样清楚会议的高昂成本,他们同样清楚自己的时间是宝贵的。所以,如果...

[DAY 10] Pytorch 简介

前言 在开始进入我们各式各样的深度模型之前,我们要先来介绍一个非常 Powerful 的 Deep ...

Day 15. 来了解Data Persistence in Unity

我的文章系列快要变成每天30分钟,学一点新的东西了。 一样是继续跟着教程走,Implement da...

[Matplotlib] tight_layout()

With tight_layout() import numpy as np import mat...