Kotlin最佳工程实践

2020-05-27  本文已影响0人  InnerNight

写在前面

自从Kotlin被官宣为Android开发正式语言,这门语言也越来越流行。相信大家也对Kotlin这门语言有过了解或者学习,例如类型推断、空安全、lambda表达式、高阶函数,甚至Kotlin协程之类的。但听了很多大道理,还是过不好这一生;对很多java程序员来说,学了很多kotlin知识,但真到自己写的时候还是无从下手,写法还是照搬java那一套。
本文就是为了解决这个问题,希望提供在工程实践具体场景下如何去有效的利用kotlin的语言特性,接受kotlin的编程思想,来提高开发效率和优化代码结构。此外,本文中提到的工程实践限定在Java和Kotlin混合工程,对于纯Kotlin工程下一些最优实践不作讨论。最后,本文目标受众是学习过Kotlin的Java程序员,很多东西会建立在Java和kotlin语言基础上讨论,不会介绍kotlin语言的基础知识。

主要场景

如何实现懒汉式单例模式

众所周知,Kotlin中不在提供static关键字,而是提供了object关键字,可以更加方便的去声明一个对象,而Java中的单例模式也可以简化成一句简单的声明,示例代码:

object SimpleSington {
  fun test() {}
}
//在Kotlin里调用
SimpleSington.test()

//在Java中调用
SimpleSington.INSTANCE.test();

这种声明单例的方法,等价于如下java代码:

public final class SimpleSington {
   public static final SimpleSington INSTANCE;

   private SimpleSington() {
   }

   static {
      INSTANCE = new SimpleSington();
   }
}

而这是饿汉式的实现,有可能造成一定的性能浪费。如果对于性能开销没有特别严格的要求,这个也满足日常开发,但如果因为业务、性能要求需要实现一个懒汉式的加载,就需要改进我们的写法。这里可以用上kotlin的另一个语言特性:lazy。具体语法的介绍不是本文重点,如果需要了解可以查看:Kotlin的lateinit和lazy,下面是示例代码:

class LazySingleton private constructor() {
    companion object {
        val instance: LazySingleton by lazy { LazySingleton() }
    }
}

这个写法利用lazy关键字,非常简单的实现了一个懒汉式的单例模式。那么这段代码等效于哪种java的写法;以及lazy的更高级用法,包括3中LazyThreadSafetyMode的含义,读者感兴趣可以阅读上面的链接并自己探究。

使用顶层函数代替静态工具类

Java中static另一个典型应用场景,就是静态工具类,而在Kotlin中,我们应该如何去写一些工具类?对于Kotlin对static的替代,第一反应是object或者companion object,那是不是应该写成如下这样?

// Don't
object StringUtil {
    fun countAmountOfX(string: String): Int{
        return string.length - string.replace("x", "").length
    }
}
StringUtil.countAmountOfX("xFunxWithxKotlinx")

但这样的写法我们并不推荐,这里应该用Kotlin支持的顶层函数,写法如下:

// Do
fun countAmountOfX(str: String): Int {
    return str.length - str.replace("x", "").length
}
countAmountOfX("xFunxWithxKotlinx")

使用扩展函数代替工具方法

其实上面的代码还可以进一步优化,kotlin中可以使用扩展函数来扩展一个类的方法,这种写法运用在代码中,最终结果就成了这样:

// Do
fun String.countAmountOfX(): Int {
    return length - replace("x", "").length
}
"xFunxWithxKotlinx".countAmountOfX()

这样,这个函数就变成了像String自带的函数一样,用起来非常自然;但在Kotlin/Java混合工程中,这样用了顶层函数和扩展函数语言特性的方法,要如何在Java里中使用呢?这就需要加上一个注解,示例代码如下:

@file:JvmName("StringUtil")

package com.liye.utils

// In Kotlin, remove the unnecessary wrapping util class and use top-level functions instead
// Often, you can additionally use extension functions, which increases readability ("like a story").
fun String.countAmountOfX(): Int {
    return length - replace("x", "").length
}

// Java code
StringUtil.countAmountOfX("xFunxWithxKotlinx")

这里面涉及到两个知识点:1. 通过@file:JvmName注解可以让java中使用到kotlin的顶层函数;2. kotlin中的扩展函数在java中调用时,会把扩展对象作为第一个参数传入;
此外,根据Kotlin编码规约的推荐,应该把单行表达式的函数使用=来简化,同时省去了返回值声明,代码如下:

// Do
fun String.countAmountOfX() = length - replace("x", "").length

对于Kotlin的扩展函数,还需了解的是Kotlin的标准库中利用扩展函数提供了大量基于Java类的扩展,包括:String、Collection、Array等,这些都可以在实践中帮助我们更方便的编码;而且Kotlin开发中也推荐尽量使用这些方法,替代掉之前很多我们自己写的各种工具类,例如:

// Do
if (str.isNullOrEmpty()) {
    // ....
}
arrayOf(1, 2, 3, 4)

使用默认字段代替重载

Kotlin中的函数支持默认参数,这在代码声明时可以替代掉原来需要靠重载编写的大量代码,示例如下:

// Don't
fun foo() = foo("a")
fun foo(a: String) { /*……*/ }

// Do
fun foo(a: String = "a") { /*……*/ }

但是Java并不支持默认参数,所以在混合工程中,Kotlin中使用了默认参数的方法必须使用@JvmOverloads注解来支持java调用:

// Do
@JvmOverloads
fun foo(a: String = "a") { /*……*/ }

使用命名字段来增加可读性

Kotlin支持在方法调用时,使用命名参数,示例如下:

val config2 = SearchConfig2(
       root = "~/folder",
       term = "game of thrones",
       recursive = true,
       followSymlinks = true
)

可以发现,命名字段结合默认字段,有点类似Java中使用了Builder模式或者流式调用的代码,如下:

// Don't
val config = SearchConfig()
       .setRoot("~/folder")
       .setTerm("game of thrones")
       .setRecursive(true)
       .setFollowSymlinks(true)

上述代码是不推荐的,在Kotlin中,合理使用命名字段和默认字段,可以省去大量的这样的set方法;

使用lambda替代callback

在java中,经常需要使用callback接口来实现回调、观察者模式等逻辑,在kotlin中更推荐使用lambda来替代callbcak,省去callback接口的声明与定义;

// Don't
interface GreeterCallback {
    fun greetName(name: String): Unit
}

fun sayHi(callback: GreeterCallback) = /* … */

// Do
fun sayHi(callback: (String) -> Unit) = /* … */

// Caller
greeter.sayHi { Log.d("Greeting", "Hello, $it!") }

其中关于lambda的使用以及简化可以参考文章:细说 Kotlin 的 Lambda 表达式

合理使用Kotlin的内置函数(let、apply等)

提到Kotlin的lambda,不得不提kotlin中内置了几个以lambda作为参数传入的内置函数,这几个函数结合kotlin的语言特性,可以大大简化我们的代码,下面给出几个工程实践中的例子。

替代if-null检测

在java中为了运行时避免NPE,我们经常需要做非空检测;而Kotlin一个重要的语言特性就是空安全,将非空检测作为了语言机制的一部分,那么如何在kotlin中优雅的去使用一些nullable对象呢?比如下面这样的代码:

// Java
if (data != null && data.publishTime != null && viewHolder != null && viewHolder.commentTimeTxt != null) {
    viewHolder.commentTimeTxt.text = DateUtil.getRecommentDate(data.publishTime/1000)
}

在kotlin中就不需要再写这么多if了,直接使用?.结合let即可搞定:

// Kotlin
data?.publishTime?.let {
            viewHolder?.commentTimeTxt?.text = DateUtil.getRecommentDate(it/1000)
        }

替代if-type检测

了解Kotlin的基础语法,就会知道kotlin中提供了is和as关键字来实现类似Java中经常会使用的instanceOf和类型转换。但很多时候,结合内置函数,我们应该更进一步简化我们的代码,例如:

// Java
for (Animal animal : animals) {
    if (animal instanceOf Dog) {
        ((Dog) animal).bark();
    }
}

转成Kotlin的话,可以写成如下这样:

// kotlin
animals.forEach {
    if (it is Dog) {
        it.bark()
    }
}

但其实,还可以通过使用as?关键字进一步简化成如下代码:

// kotlin
animals.forEach {
    (it as? Dog)?.bark()
}

整合对象初始化代码

//Don't
val dataSource = BasicDataSource()
dataSource.driverClassName = "com.mysql.jdbc.Driver"
dataSource.url = "jdbc:mysql://domain:3309/db"
dataSource.username = "username"
dataSource.password = "password"
dataSource.maxTotal = 40
dataSource.maxIdle = 40
dataSource.minIdle = 4

这种Java中的常见写法,在kotlin中就不推荐了,可以用apply来大大简化:

//Do
val dataSource = BasicDataSource().apply {
    driverClassName = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://domain:3309/db"
    username = "username"
    password = "password"
    maxTotal = 40
    maxIdle = 40
    minIdle = 4
}

结语

基于Kotlin的新语法和编程思想,还有很多的工程实践可以探讨如何从原来Java的写法之上演进,这里探讨的只是很少一部分,更不用说还有类似Kotlin-reflect,kotlin-coroutine, RxKotlin这样的库,以及越来越多的支持库。相信Kotlin作为新一代的安卓官方语言,会越来越摆脱Java的桎梏,发展的越来越好,也希望大家可以尽快应用到自己的工程中。最后,欢迎讨论与指正!

参考文献

Kotlin的lateinit和lazy
Kotlin-Java interop guide
Kotlin习惯用法
Idiomatic Kotlin. Best Practices.
简述Kotlin中let, apply, run, with的区别
细说 Kotlin 的 Lambda 表达式

上一篇下一篇

猜你喜欢

热点阅读