写更易懂的代码,Kotlin 是这样隐藏复杂度的(一)

简介: 写更易懂的代码,Kotlin 是这样隐藏复杂度的(一)

引子


代码是一种表达,凝聚了程序员的想法,得先保证表达的正确性,以免执行时报错。除此之外,表达的简洁性也值得关注,以免日后因看不懂而难以维护。代码不仅是用来执行的,也是用来读或修改的,读懂是修改的前提。


这一系列的主题是“复杂度”。复杂度是软件开发过程中最大的敌人。高复杂度影响着理解成本,维护难度甚至是迭代节奏和交付质量。系列文章目录如下:


  1. Kotlin 基础 | 拒绝语法噪音


  1. Kotlin 源码 | 降低代码复杂度的法宝


Kotlin 是降低复杂度的大师,这一篇将挑选几个 Kotlin 的特性结合实战代码分析下它降低复杂度之道。


隐藏 try-catch-finally


假设有下面这个方法:


public Result write(String id, byte[] bytes) {
    Result result = new Result();
    FileOutputStream fileOutputStream = null;
    BufferedOutputStream bufferedOutputStream = null;
    try {
        lock.writeLock().lock();
        fileOutputStream = new FileOutputStream(new File("xxx"));
        bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
        bufferedOutputStream.write(bytes,0,bytes.length);
        boolean success = new Dao().insert(id);
        if(success) return result.success();
        else return result.error("failed");
    } catch (IOException e) {
        return result.error("file error");
    } catch (SQLiteException e){
        return result.error("db error") ;
    }finally {
        lock.writeLock().unlock();
        try {
            if (bufferedOutputStream != null) {
                bufferedOutputStream.close();
            }
        } catch (IOException e) {
        }
    }
}


方法中分别向文件和数据库输出内容,使用了 ReentrantReadWriteLock 保证线程安全,使用 try-catch 捕获异常,并且在 finally 中释放锁和 io 流。


若使用 kotlin 可以大幅降低这段代码的复杂度:


public fun write(id: String, bytes: ByteArray) =
    return runCatching {
        lock.write {
            File("xxx").outputStream().buffered().use { it.write(bytes, 0, bytes.size) }
            if (Dao().insert(id)) Result().success()
            else Result().error("failed")
        }
    }.getOrElse {
        when (it) {
            is SQLiteException -> Result().error("db error")
            is IOException -> Result().error("file error")
            else -> Result().error("other error")
        }
    }


kotlin 仅用了一半的代码量就表达出相同的语义。


runCatching() + getOrElse()


其中runCatching(),是一个扩展法方法:


public inline fun <T, R> T.runCatching(block: T.() -> R): Result<R> {
    return try {
        Result.success(block())
    } catch (e: Throwable) {
        Result.failure(e)
    }
}


该方法的参数是一个带返回值的 lambda,它会被嵌入 try-catch 执行。


该方法的返回值是 Result,lambda 执行的结果会被包装成 Result。


Kotlin 中try-catch是一个表达式,它是有值的,等于每个分支最后一条语句的值。这个特性使得不必多声明一个局部变量:


Result result = new Result();
try {
    result.success();
} catch (Exception e) {
    result.failure(e);
}
return result;


除此之外,Kotlin 中的 if-else 和 when 都是表达式,它们的值等于命中分支中最后一个表达式的值。上述代码借用了这个特性,消除了用于记录返回值的局部变量,将整个方法的返回值内聚在一个表达式中:


return runCatching { // 该方法返回值 = lambda的值
    lock.write { // 该方法返回值 = lambda的值 = if-else 表达式的值
        if (Dao().insert(id)) Result().success()
        else Result().error("failed")
    }
}.getOrElse { // 该方法返回值 = lambda的值 = when 表达式的值
    when (it) {
        is SQLiteException -> Result().error("db error")
        is IOException -> Result().error("file error")
        else -> Result().error("other error")
    }
}


getOrElse()是 Result 的扩展方法:


public inline fun <R, T : R> Result<T>.getOrElse(onFailure: (exception: Throwable) -> R): R {
    contract {
        callsInPlace(onFailure, InvocationKind.AT_MOST_ONCE)
    }
    return when (val exception = exceptionOrNull()) {
        null -> value as T
        else -> onFailure(exception)
    }
}


它用于去除包装类 Result,返回真正的结果。


runCatching() + getOrElse() 的组合配合各种表达式使得“在 try-catch ”代码块中返回值变得更简洁、更有表现力、更具有函数式编程的风格。


write()


write() 是 ReentrantReadWriteLock 的扩展方法:


public inline fun <T> ReentrantReadWriteLock.write(action: () -> T): T {
    contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) }
    val rl = readLock()
    val readCount = if (writeHoldCount == 0) readHoldCount else 0
    repeat(readCount) { rl.unlock() }
    val wl = writeLock()
    wl.lock()
    try {
        return action()
    } finally {
        repeat(readCount) { rl.lock() }
        wl.unlock()
    }
}


它把“在 try-catch-finally 中加/释放锁”的细节隐藏在了内部,留给外部的只剩下简洁:


lock.write { // io 操作 }


外部代码不再会出现 finally 了,复杂度就这样被隐藏。


拒绝嵌套构造


在 java 中,io 相关类使用装饰者模式,代码通常如下:


BufferedOutputStream stream = new BufferedOutputStream(new FileOutputStream(new File("xxx")));


把这种构建对象的方式叫“嵌套式构建”。在 kotlin 中,有更好的表达方式:链式构建,代码如下:


File("xxx").outputStream().buffered()


其中 outputStream() 和 buffered() 都是扩展方法:


// File 的扩展方法
public inline fun File.outputStream(): FileOutputStream {
    return FileOutputStream(this)
}
// OutputStream 的扩展方法
public inline fun OutputStream.buffered(bufferSize: Int): BufferedOutputStream =
    if (this is BufferedOutputStream) this else BufferedOutputStream(this, bufferSize)


嵌套式构造被隐藏在方法内部分批进行。


use()


public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    var exception: Throwable? = null
    try {
        return block(this)
    } catch (e: Throwable) {
        exception = e
        throw e
    } finally {
        when {
            apiVersionIsAtLeast(1, 1, 0) -> this.closeFinally(exception)
            this == null -> {}
            exception == null -> close()
            else ->
                try {
                    close()
                } catch (closeException: Throwable) {
                    // cause.addSuppressed(closeException) // ignored here
                }
        }
    }
}


use() 方法是所有资源类的福音,它把“如何使用资源”、“如何捕获异常”以及“如何释放资源”内聚在一个方法内部。如此一来,外部代码中不会出现 try-catch-finally 代码块了。


用 DSL 隐藏不必要的接口


实现 Java 接口时,即使不需要其中的某些方法,也必须将其 implements 并保持其为空实现,傻傻地处在那:


AnimationSet animationSet = new AnimationSet(false);
animationSet.setAnimationListener(new Animation.AnimationListener() {
    @Override
    public void onAnimationStart(Animation animation) {
    }
    @Override
    public void onAnimationEnd(Animation animation) {
        showToast()
    }
    @Override
    public void onAnimationRepeat(Animation animation) {
    }
});


其实只是想在动画结束时展示一个 toast,另外两个回调对我来说没用。


利用 Kotlin 的语法糖可以只实现自己感兴趣的方法。


再介绍解决方案之前得引入一个概念:DSL


DSL


DSL = domain specific language,即“特定领域语言”,与它对应的一个概念叫“通用编程语言”,通用编程语言有一系列完善的能力来解决几乎所有能被计算机解决的问题,像 Java 就属于这种类型。而特定领域语言只专注于特定的任务,比如 SQL 只专注于操纵数据库,HTML 只专注于表述超文本。


既然通用编程语言能够解决所有的问题,那为啥还需要特定领域语言?因为它可以使用比通用编程语言中等价代码更紧凑的语法来表达特定领域的操作。比如当执行一条 SQL 语句时,不需要从声明一个类及其方法开始。


更紧凑的语法意味着更简洁的 API。应用程序中每个类都提供了其他类与之交互的可能性,确保这些交互易于理解并可以简洁地表达(低复杂度),对于软件的可维护性至关重要。


DSL 有一个普通API不具备特征:DSL 具有结构。而带接收者的lambda使得构建结构化的 API 变得容易。


带接收者的 lambda


它是一种特殊的 lambda,kotlin 中特有的。它的表达形式为:T.() -> R。即在常规的 lambda 前面声明了一个对象 T,该对象称为 lambda 的接收者。


可以把这种 lambda 理解成“为接收者声明的一个匿名扩展函数”。(扩展函数是一种在类体外为类添加功能的特性)


带接收者的 lambda 的函数体除了能访问其所在类的成员外,还能访问接收者的所有非私有成员,这个特性是它能够轻松地构建结构。


当带接收者的 lambda 配合高阶函数时,构建结构化的 API 就变得易如反掌。


高阶函数


它是一种特殊的函数,它的参数或者返回值是另一个函数。


比如启动协程的 launch() 方法就是一个高阶函数:


public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit // 该参数是另一个函数
): Job { ... }


launch() 的最后一个参数是“带接收者的 lambda”,接收者是 CoroutineScope,这样的设定使得在协程中构建子协程易如反掌:


scope.launch { // IDE 会提示这里有个隐含参数 this:CoroutineScope
    launch {...}
}


上述代码内部的 launch{},其实是 this.launch {},this 通常可以省略。又因为参数 block 被定义为CoroutineScope.() -> Unit,所以 this 的类型就是 CoroutineScope。

这样的代码就是有结构的。


下面运用 DSL 来解决当前的问题。


  1. 新建类用于存放接口中各个方法的实现


class AnimatorListenerImpl {
    var onRepeat: ((Animator) -> Unit)? = null
    var onEnd: ((Animator) -> Unit)? = null
    var onCancel: ((Animator) -> Unit)? = null
    var onStart: ((Animator) -> Unit)? = null
}


它包含四个成员,每个成员的类型都是函数类型。Kotlin 中函数也是一种类型,它可以存储在变量中。


这四个函数类型变量的声明参照了Animator.AnimatorListener的定义:


public static interface AnimatorListener {
    void onAnimationStart(Animator animation);
    void onAnimationEnd(Animator animation);
    void onAnimationCancel(Animator animation);
    void onAnimationRepeat(Animator animation);
}


该接口中的每个方法都接收一个 Animator 参数并返回空值,用 lambda 可以表达成 (Animator) -> Unit。所以 AnimatorListenerImpl 将接口中的四个方法的实现都保存在函数变量中,并且实现是可空的。


  1. 为 Animator 定义一个高阶扩展函数


fun AnimatorSet.addListener(action: AnimatorListenerImpl.() -> Unit) {
    AnimatorListenerImpl().apply { action }.let { builder ->
        //'将回调实现委托给AnimatorListenerImpl的函数类型变量'
        addListener(object : Animator.AnimatorListener {
            override fun onAnimationRepeat(animation: Animator?) {
                animation?.let { builder.onRepeat?.invoke(animation) }
            }
            override fun onAnimationEnd(animation: Animator?) {
                animation?.let { builder.onEnd?.invoke(animation) }
            }
            override fun onAnimationCancel(animation: Animator?) {
                animation?.let { builder.onCancel?.invoke(animation) }
            }
            override fun onAnimationStart(animation: Animator?) {
                animation?.let { builder.onStart?.invoke(animation) }
            }
        })
    }
}


为 Animator 定义了扩展函数 addListener(),该函数接收一个带接收者的lambdaaction


扩展函数体中构建了 AnimatorListenerImpl 实例并紧接着应用了 action ,最后为 Animator 设置动画监听器并将回调的实现委托给 AnimatorListenerImpl 中的函数类型变量。


将本节开头的代码改写:


AnimationSet().addListener {
    onEnd = { showToast() } 
}


这段调用拥有自己独特的结构,它解决了“必须实现全部 java 接口”这个特定的问题,所以它可以称得上是一个自定义 DSL (当然和 SQL 相比,它显得太简单了)。


关于 DSL 更广泛的应用可以点击 Android性能优化 | 把构建布局用时缩短 20 倍(下) - 掘金 (juejin.cn)


总结


高阶方法、带接收者的 lambda、扩展方法、函数类型、if-else/when/try-catch 表达式,综合运用这些语法糖,将啰嗦的语法隐藏在一个个扩展方法内部,把简洁留给上层,这就是 Kotlin 的化解复杂度之道。


推荐阅读












目录
相关文章
|
7月前
|
XML 编译器 Android开发
Kotlin DSL 实战:像 Compose 一样写代码
Kotlin DSL 实战:像 Compose 一样写代码
175 0
|
7月前
|
JSON 监控 数据挖掘
使用Kotlin代码简化局域网监控软件开发流程
使用Kotlin简化局域网监控软件开发,通过获取网络设备的IP和MAC地址,实现实时监控网络流量。示例代码展示了如何创建Kotlin项目,获取网络设备信息,监控网络流量以及进行数据分析和处理。此外,还演示了如何使用HTTP库将数据提交到网站,为网络管理提供高效支持。
159 0
|
3月前
|
自然语言处理 Java 网络架构
解锁跨平台微服务新纪元:Micronaut与Kotlin联袂打造的多语言兼容服务——代码、教程、实战一次打包奉送!
【9月更文挑战第6天】Micronaut是一款轻量级、高性能的Java框架,适用于微服务开发。它支持Java、Groovy和Kotlin等多种语言,提供灵活的多语言开发环境。本文通过创建一个简单的多语言兼容服务,展示如何使用Micronaut及其注解驱动特性实现REST接口,并引入国际化支持。无论是个人项目还是企业应用,Micronaut都能提供高效、一致的开发体验,成为跨平台开发的利器。通过简单的配置和代码编写,即可实现多语言支持,展现其强大的跨平台优势。
56 3
|
3月前
|
API 数据处理 数据库
掌握 Kotlin Flow 的艺术:让无限数据流处理变得优雅且高效 —— 实战教程揭秘如何在数据洪流中保持代码的健壮与灵活
Kotlin Flow 是一个强大的协程 API,专为处理异步数据流设计。它适合处理网络请求数据、监听数据库变化等场景。本文通过示例代码展示如何使用 Kotlin Flow 管理无限流,如实时数据流。首先定义了一个生成无限整数的流 `infiniteNumbers()`,然后结合多种操作符(如 `buffer`、`onEach`、`scan`、`filter`、`takeWhile` 和 `collectLatest`),实现对无限流的优雅处理,例如计算随机数的平均值并在超过阈值时停止接收新数据。这展示了 Flow 在资源管理和逻辑清晰性方面的优势。
75 0
|
4月前
|
安全 Java Android开发
Kotlin字符串秘籍:解锁高效处理与创意应用,让你的代码闪耀不凡!
【8月更文挑战第2天】Kotlin是一门现代化的静态类型语言,以简洁、安全及强互操作性著称,在Android及服务器端开发中广受好评。本文通过与其他语言对比,深入解析Kotlin中字符串的基础和高级用法。Kotlin简化了字符串拼接,支持直接使用`+`操作符,并引入了直观的字符串模板。它提供了丰富的字符串操作函数,如使用索引范围进行子字符串提取,增强了代码的可读性。Kotlin字符串的不可变性提升了程序稳定性。利用扩展函数特性,可以轻松定制字符串行为,提高代码的模块化和重用性。掌握这些技巧能显著提升开发效率和代码质量。
49 1
|
6月前
|
JavaScript 前端开发 Android开发
kotlin安卓在Jetpack Compose 框架下使用webview , 网页中的JavaScript代码如何与native交互
在Jetpack Compose中使用Kotlin创建Webview组件,设置JavaScript交互:`@Composable`函数`ComposableWebView`加载网页并启用JavaScript。通过`addJavascriptInterface`添加`WebAppInterface`类,允许JavaScript调用Android方法如播放音频。当页面加载完成时,执行`onWebViewReady`回调。
|
7月前
|
Java Kotlin
java调用kotlin代码编译报错“找不到符号”的问题
java调用kotlin代码编译报错“找不到符号”的问题
313 10
|
缓存 API Android开发
Kotlin 学习笔记(七)—— Flow 数据流学习实践指北(三)冷流转热流以及代码实例(下)
Kotlin 学习笔记(七)—— Flow 数据流学习实践指北(三)冷流转热流以及代码实例(下)
172 0
|
缓存 Java Kotlin
Kotlin 学习笔记(七)—— Flow 数据流学习实践指北(三)冷流转热流以及代码实例(上)
Kotlin 学习笔记(七)—— Flow 数据流学习实践指北(三)冷流转热流以及代码实例(上)
127 0
|
IDE 安全 Java
使用 kotlin.Deprecated,优雅废弃你的过时代码
使用 kotlin.Deprecated,优雅废弃你的过时代码
415 0