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

测试之後,我们就可以发现案例通过了!


<<:  我们的基因体时代-AI, Data和生物资讯 Day24- 使用tidyverse观念来分析基因资料:plyranges

>>:  Swift纯Code之旅 Day14. 「TableView(5) - 点击TableViewCell」

[Day 30] 应用三:脸部追踪

今天就来看一下人脸辨识 + 物件追踪可以迸出什麽样的火花吧! 本文开始 在applications目...

DAY 1 系列文章启文

近年来 ROS (Robot Operating System,机器人作业系统) 目前已成熟应用於智...

Day 14 : 案例分享(4.3) 签核与费用模组 - 签核关卡及条件设定

案例说明及适用场景 签核与流程及人员组织架构为正相关 一般简易的判断包含金额,商品类型,申请人或由申...

Day14-旧网站重写成Vue_5_多图片切换

昨天PO完文重看一下旧文才发现前天说要讲json做轮播,结果昨天先讲了tab…. 希望今天能讲完轮播...

Ruby on Rails 模组(Module)

如果我有一个小猫类别,我想要这个小猫类别有飞行功能,你会怎麽做? 直接写一个有飞行功能的小鸟类别,然...