借着案例学Kotlin
代码块与基本代码的语法区别
创建文档信息的业务场景
Java 怎么写业务?
- 先声明一个待返回的空对象(没有被初始化完毕的中间状态)
- 中间业务逻辑,夹杂各种参数对象的getter,以及返回对象的setter
- 持久化
- 包装返回
public RestResult<DocumentDo> generate(Dto dto) {
RestResult<DocumentDo> result = new RestResult<>();
DocumentDo docDo = new DocumentDo();
// ...
// 检查重复
if(checkDuplicate(dto.getUsername())) {
throw new BizException("...");
}
// 各种 username 合法过滤, 关键词过滤, 可能涉及到修改username的
RpcResult<UsernameDto> usernameRes = xxxService.getUsername(username)
String finalUsername = null;
if(usernameRes != null &&
usernameRes.isSuccess() &&
usernameRes.getData() != null) {
finalUsername = usernameRes.getData().getUsername();
}
// 能不能看出来这段代码的问题?
docDo.setUsername(finalUsername);
// 取块 并开始处理
ContentDto richContentDto = dto.getContent();
if(richContent == null) {
throw new BizException("...");
}
List<BlockDto> blocks = richContentDto.getBlocks();
if(blocks == null) {
throw new BizException("...");
}
// encode & flattern images
List<BlockDo> blockDoList = blocks
.stream()
.parallel()
.flatmap(block -> {
if(!BlockUtils.isImage(block)) {
return Stream.of(block);
}
byte[] bytes = (bytes[]) block.getContent();
byte[][] bytess = ImageUtils.analysis(bytes);
return Arrays
.stream()
.map(bytes -> {
BlockDo blockDo = new BlockDo();
blockDo.setXXX(block.getXXX());
blockDo.setContent(bytes);
})
})
.collect(Collectors.toList());
// 假设整个文档流经历了黑盒一样的处理计算, 你会选择相信这个blockDoList吗
docDo.setBlocks(blockDoList);
DocumentDo docResDo = dockDoRepository.save(docDo);
result.setSuccess(true);
result.setDate(docResDo);
return result;
}Kotlin 写法
(先忽略语法设计细节审美偏好,如方法声明、类型后置声明等等)
fun generate(dto: Dto): RestResult<DocumentDo> {
// 检查重复 以及 username 合法过滤, 关键词过滤, 可能涉及到username变更的
// val 定义为常量, 同时类型为String不可空类型
val finalUsername: String = dto
.username
?.takeIf { checkDuplicate(it) }
?.let { xxxService.getUsername(it)}
?.takeIf { it.isSuccess }
?.data
?.username
?: throw new BizException("...")
finalUsername.substring(0, 5)
// encode & flattern images
// val 定义为常量, 同时类型为List<BlockDo>不可空类型
val blockDoList: List<BlockDo> = dto
.content
?.blocks
?.flatmap { block ->
if(!block.isImage) listOf(block)
else (block.content as? ByteArray)
.let { bytes.analysis() }
.map { bytes ->
BlockDo().apply { this ->
xxx = xxx
content = bytes
}
}
}
}
?: throw new BizException("...")
val blockDoList2: List<BlockDo?>?
return DocumentDo(
xxx = xxx,
block = blockDos,
username = finalUsername,
)
.run { this -> dockDoRepository.save(this) }
.let { it -> RestResult(true, it) }
// run & let 的语义上不同,
// run lambda 块表现为拥有上下文this, 预期返回一个对象, this是被chain对象, 语义 跑个save
// val anotherObj = obj.run { this -> objB }
// let lambda 块表现为捕获一个为it参数, 预期返回一个对象, it是被chain对象, 语义 让xx变换
// val anotherObj = obj.let { it -> objB }
// 另外还有apply lambda 块表现为拥有上下文this, this是被chain对象, 没有返回值, 语义apply 应用
// obj.apply { this -> this.username = "xxx" }
// also lambda 块表现为捕获一个为it参数, it是被chain对象, 语义also 同时
// obj.also { it -> it.username = "xxx" }
}通过Kotlin的转写, 其实已经"强制性"修复了Java版本遗留没有处理的NPE
- 常量
finalUsername,会被编译器以及IDE推导出val finalUsername: String的不可空字符串类型的 - 常量
blockDos,会被编译器以及IDE推导出val finalUsername: List<BlockDo>的不可空列表类型的
其中 blockDos,还会更甚推断出容器内部的元素也不可空,如果代码变换中有一处会返回空,又或者确实需要显式的空元素,相应的,其类型应该为 List<BlockDo?>,当然最坏的类型 List<BlockDo?>? 我猜一定不是你想要的。
这就是Kotlin的强制空安全,通过语法和编译器,强制性的在代码编写中对类型的可空与否进行选择、传递、处理的操作。
进阶:一个复杂的ViewModel转换
想象一下通过DB Query查询了一个 ResultSet:
| ID | username | information | 30DaysLiveTimes | 60DaysLiveTimes |
|---|---|---|---|---|
| 1 | 鲁迅 | 浙江周树人 | 123 | 60 |
| 2 | LO | ASC | 70 | 70 |
| 3 | ABC | ASD | 60 | 70 |
然后需要对这个结果集进行分裂、排序,输出成下面的展示格式:
{
"informationList": [
{
"information": "ASD",
"usernameList": ["LO", "ABC"]
}
],
"rangeList": [
{
"username": "ASD",
"liveTimes30Days": 60
}
],
"timesList": [
{
"username": "ASD",
"days": 30,
"times": 60
},
{
"username": "ASD",
"days": 60,
"times": 70
}
]
}Kotlin的写法很简单,同时针对于值可空的ResultSet、Map等容器,提供了便捷的空操作处理。
// ViewModel.kt
data class ViewModel(
val informationList: List<Information>,
val rangeList: List<Range>,
val timesList: List<Times>,
) {
data class Information(val information: String, val usernameList: List<String>)
data class Range(val username: String, val liveTimes30Days: Int)
data class Times(val username: String, val days: Int, val times: Int)
}
fun main() {
val resultSet: List<Map<String, Any?>> = queryFromDb()
val informationList: List<Information> = resultSet
.groupingBy { it["information"]?.toString() } // Group<String?, Map<String, Any?>>
.fold(listOf<String>()) { list, map ->
val username = map["username"]?.toString()
username?.let { list.add(username) } ?: list
if (username == null) list else list.add(username)
} // Map<String?, List<String>>
.mapNotNull { (key, value) -> if (key != null) key to value else null }
// Map<String, List<String>>
.map { (key, value) -> Information(key, value) }
// List<Information>
val rangeList: List<Range> = resultSet
.sortedByDescending { it["30DaysLiveTimes"] as? Int } // List<Map<String, Any?>>
.mapNotNull @label2{
// 空则丢弃
val username = it["username"]?.toString() ?: return@mapNotNull null
val liveTimes30Days = (it["30DaysLiveTimes"] as? Int) ?: return@mapNotNull null
username to liveTimes30Days
} // List<Pair<String, Int>>
.map { (key, value) -> Range(key, value) } // List<Range>
List<Range?>?
val timesList = resultSet
.flatMap { map ->
// 把小范围共用方法抽象成一个函数对象 generate
fun generate(days: Int): Triple<String, Int, Int>? {
val username = map["username"]?.toString() ?: return null
val times = (map["${days}DaysLiveTimes"] as? Int) ?: return null
return Triple(username, days, times)
}
// Stream
sequenceOf(
generate(30),
generate(60)
).filterNotNull()
}
.map { (first, second, third) -> Times(first, second, third) }
// 显式命名构造 xxx = xxx,针对参数多和调用不够明确的时候,帮助阅读性
val viewModel = ViewModel(
informationList = informationList,
rangeList = rangeList,
timesList = timesList,
)
}类
怎么编写一个Spring Bean Service?
Kotlin类的声明与Java相差不大,对于依赖注入的编写会更加简洁。
下面注入代码等同于通过构造器的 @Autowired 注入,通过构造器注入,保证了Bean的初始化一定是完整且正确的。
@Component
class AbcService(
@Qualified("TestBcdService")
private val bcdService: BcdService,
private val applicationContext: ApplicationContext,
) {
@Resource
private val bcdService: BcdService
@Resource
private var bcdService: BcdService?
@Resource
private lateinit var bcdService: BcdService
fun serviceAbc(request: Request): String {
val str =
return "Hello" + str
return "Hello ${bcdService.run(request.username)}"
}
}
// public final class AbcService()而对于 Configuration,众所周知 Configuration 的实现通过增强继承进行了实现,然而由于Kotlin Class默认为final级别,需要显式的Open。正如空安全一样,显式的反转更有助于程序的正确理解性。
@Configuration
open class AbcConfiguration(
private val properties: AbcConfigurationProperties,
) {
@Bean
open fun properties2(): Properties = Properties()
@Bean
open fun dataSource(properties2: Properties): DataSource = properties.run {
HikariDataSourceBuilder()
.host(host)
.database(database)
.username(username)
.password(password)
.build()
}
}
@ConstructorBinding
@ConfigurationProperties
data class AbcConfigurationProperties(
val host: String = "jdbc://127.0.0.1:3306",
val database: String = "information_schema",
val username: String = "admin",
val password: String = "admin",
)Utils? Converter?
在Java里面,很多时候想要根据业务场景对某个类进行扩充,比如 StringUtils.isNotBlack()、DateUtils.yymmdd(),又或许需要对两个不同层次的同一领域模型进行转换,如 DomainObjectConverter.convertToPo()。
大部分时候这些类的存在是没有意义的,但正如 public static void main() 一样,你必须假设一个类的存在作为这些方法的容器。
而Kotlin Extends Function的存在,可以让它们更为合理地存在与拓展。
// DateExtends.kt
package com.lohoknang.common
// uuuummdd
private val DATE_TIME_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("uuuummdd")
fun Date.formatDatabase(): String = this.format(databaseFormat)
fun Any?.formatDatabase(): String? = this
?.toString
?.let { LocalDate.parse(it, ISO_DATE) }
?.format(databaseFormat)
fun main() {
val dateTime = LocalDateTime.now()
// 为 LocalDateTime 拓展了 formatDatabase 方法
println(dateTime.formatDatabase())
val dateTime = "2021-03-07".formatDatabase()
}DSL进阶
Kotlin DSL其实是Kotlin里面Extend & Lambda & Function & Object的结合体,能够在你自身的领域中构建一个领域专属的声明式语言,从而减少胶水代码,以及提高表达性。
- Html kotlin
- android
举例一个场景,定义一个HTML对象:
html {
div {
class("card-box", "middle")
content("测试块")
}
div {
class("card-box", "middle")
span("行内文本")
icon { class = "arrow" }
span("行内文本")
}
div({
})
divExtend {
}
fun divExtend(divBlock: Div.() -> Unit) {
divBlock.apply {
class("card-box")
}
}
}这一段代码表达的是我定义了一系列函数对象的组合,单看顶层其实是 fun html(blockFunction: () -> Unit) 的函数调用,而 {} 内的内容其实是 blockFunction 的函数体。而具体你需要对 blockFunction 这个函数对象进行如何处理、拓展、执行、转换,取决于 html 这个函数是如何实现的。
这给予了Kotlin DSL无限的可能性,这种声明式的编程帮助你可以抽象通用代码,帮助复杂布局的编写,帮助函数式编程里面状态的维护,帮助定义自身领域模型。
