借着案例学Kotlin

# 代码块&基本代码的语法区别

创建文档信息的业务场景

# Java 怎么写业务?

  1. 先声明一个待返回的空对象. (没有被初始化完毕的中间状态)
  2. 中间业务逻辑, 夹杂各种参数对象的getter, 以及返回对象的setter
  3. 持久化
  4. 包装返回
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, liveTimes30Days: Int)
  data class Times(val username: String, days: Int, 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 lazyinit var bcdService: BcdService
  
	fun serviceAbc(request: Request): String {
    val str = 
    return "Hellow" + str;
    

		return "Hellow ${bcdService.run(request.username)}"
	}
}

// public final class AbcService()

而对于Configuration, 众周所知Configuration的实现通过增强继承进行了实现, 然而由于Kotlin Class默认为final级别, 需要显式的Open. 正如空安全一样, 显式的反转更有助于程序的正确理解性

@Configuration
open class AbcConfiguration(
	private properties: AbcConfigurationProperties,
) {
  
  @Bean
  open fun properties2(): Properties = Proerties()

	@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 = "infomation_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 & Lamda & 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无限的可能性, 这种声明式的编程帮助你可以抽象通用代码, 帮助复杂布局的编写, 帮助函数式变成里面状态的维护, 帮助定义自身领域模型