美文网首页
Kotlin最佳工程实践

Kotlin最佳工程实践

作者: InnerNight | 来源:发表于2020-05-27 10:26 被阅读0次

    写在前面

    自从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 表达式

    相关文章

      网友评论

          本文标题:Kotlin最佳工程实践

          本文链接:https://www.haomeiwen.com/subject/lwhvrqtx.html