代码重构:坏味道识别、重构手法、Kotlin特有的重构技巧

代码重构这件事,我做了十几年,越做越觉得有意思。很多人觉得重构就是「把代码改好看点」,其实远不止如此。重构的核心是——在不改变外部行为的前提下,改善内部结构。说白了,就是让代码更容易理解、更容易修改、更容易扩展。

我见过太多项目,前期赶进度堆代码,后期维护成本高得吓人。你想想看,一个函数写了300行,里面嵌套了5层if-else,谁看了不头疼?今天我们就来聊聊,怎么识别这些「坏味道」,怎么用正确的手法重构,以及Kotlin给了我们哪些独特的武器。

核心观点:重构不是一次性的「大扫除」,而是日常开发中的「随手整理」。好的代码是改出来的,不是写出来的。

一、坏味道识别:你的代码在「报警」

坏味道这个词,最早出自《重构》这本书。它不是bug,但暗示着代码可能出了问题。我总结了几种最常见的坏味道,你在项目中肯定遇到过。

1. 过长函数

一个函数超过20行,就该警惕了。超过50行,基本可以断定需要拆分。我在项目中见过一个函数写了800行,里面还有递归调用……嗯,那是我接手的一个遗留系统,改一个bug花了三天。

2. 过大的类

一个类做了太多事,违反了单一职责原则。判断标准很简单:如果你无法用一句话说清楚这个类是干什么的,那它大概率太大了。

3. 重复代码

这是最明显的坏味道。同一个逻辑出现在多个地方,改一个地方就得改所有地方。我曾经因为漏改了一处重复代码,导致线上出了个严重bug……从那以后,我对重复代码零容忍。

4. 过长参数列表

函数参数超过3个,调用方就会觉得痛苦。超过5个,基本可以确定设计有问题。

5. 数据泥团

几个数据项总是同时出现,比如姓名、年龄、地址。这时候就该考虑封装成类了。

6. 依恋情结

一个方法对另一个类的数据特别感兴趣,频繁调用getter/setter。这说明这个方法放错了地方。

坏味道类型 识别特征 常见后果
过长函数 函数超过20行,逻辑复杂 难以理解、难以测试
过大的类 类职责不清晰,方法过多 高耦合、低内聚
重复代码 相同逻辑出现在多处 修改遗漏、维护成本高
过长参数列表 参数超过3个 调用复杂、易出错
数据泥团 多个数据项总是同时出现 代码冗余、缺乏抽象
依恋情结 方法频繁访问其他类的数据 职责错位、耦合度高

我的习惯:每次code review时,我都会先扫一遍这些坏味道。如果发现3个以上,我就会建议先重构再合入。磨刀不误砍柴工。

二、重构手法:从坏到好的「手术刀」

识别出坏味道之后,怎么动手改?这里介绍几种最常用的重构手法,都是我在实战中反复用到的。

1. 提取函数

把一段逻辑独立出来,变成一个单独的函数。这是最基础也最常用的手法。

// 重构前
fun processOrder(order: Order) {
    // 计算折扣
    var discount = 0.0
    if (order.total > 1000) {
        discount = order.total * 0.1
    } else if (order.total > 500) {
        discount = order.total * 0.05
    }
    // 其他逻辑...
}

// 重构后
fun processOrder(order: Order) {
    val discount = calculateDiscount(order.total)
    // 其他逻辑...
}

private fun calculateDiscount(total: Double): Double {
    return when {
        total > 1000 -> total * 0.1
        total > 500 -> total * 0.05
        else -> 0.0
    }
}

2. 移动函数

把函数放到它真正该在的地方。如果一个方法频繁使用另一个类的数据,那就把它移过去。

3. 引入参数对象

把多个参数封装成一个对象。这样不仅减少了参数数量,还让语义更清晰。

// 重构前
fun searchUsers(name: String, age: Int, city: String, page: Int, size: Int)

// 重构后
data class SearchCriteria(
    val name: String? = null,
    val age: Int? = null,
    val city: String? = null,
    val page: Int = 1,
    val size: Int = 20
)

fun searchUsers(criteria: SearchCriteria)

4. 分解条件表达式

复杂的if-else逻辑,拆成多个小函数,每个函数只处理一种情况。

// 重构前
fun getDiscount(user: User): Double {
    return if (user.isVip && user.level > 3) {
        if (user.totalSpent > 10000) 0.3 else 0.2
    } else if (user.isVip) {
        0.1
    } else {
        0.0
    }
}

// 重构后
fun getDiscount(user: User): Double = when {
    user.isHighLevelVip() -> user.getHighLevelDiscount()
    user.isVip -> 0.1
    else -> 0.0
}

private fun User.isHighLevelVip() = isVip && level > 3
private fun User.getHighLevelDiscount() = if (totalSpent > 10000) 0.3 else 0.2

避坑指南:我曾经在重构时一次性改了太多东西,结果测试全挂了。后来我学乖了——每次只做一种重构手法,改完就跑测试,确保没问题再继续。小步快跑,才是重构的正确姿势。

三、Kotlin特有的重构技巧

Kotlin给了我们很多「独门武器」,用好了能让代码脱胎换骨。我个人觉得,Kotlin最厉害的地方就是——它让你用更少的代码,表达更清晰的意图。

1. 用扩展函数消除工具类

Java里到处都是Utils类,Kotlin完全不需要。扩展函数让代码更自然。

// Java风格
fun StringUtils.isEmail(email: String): Boolean {
    return email.contains("@")
}

// Kotlin风格
fun String.isEmail(): Boolean = contains("@")

// 使用
val result = "test@example.com".isEmail()

2. 用sealed class替代枚举+when

sealed class是Kotlin的「王牌」之一。它让状态管理变得异常清晰。

sealed class UiState {
    data class Loading(val progress: Int) : UiState()
    data class Success(val data: List<Item>) : UiState()
    data class Error(val message: String) : UiState()
    object Empty : UiState()
}

fun render(state: UiState) = when (state) {
    is UiState.Loading -> showLoading(state.progress)
    is UiState.Success -> showData(state.data)
    is UiState.Error -> showError(state.message)
    UiState.Empty -> showEmpty()
    // 不需要else分支,编译器会检查是否覆盖所有情况
}

3. 用委托简化样板代码

by关键字能帮你省掉大量重复代码。by lazy、by Delegates.observable、by map……都是好东西。

class UserViewModel : ViewModel() {
    // 懒加载,只在第一次访问时初始化
    private val repository by lazy { UserRepository() }
    
    // 可观察属性,值变化时自动触发
    var userName by Delegates.observable("") { _, old, new ->
        Log.d("UserViewModel", "Name changed: $old -> $new")
    }
    
    // 委托给Map,适合解析JSON
    class User(val map: Map<String, Any?>) {
        val name: String by map
        val age: Int by map
    }
}

4. 用内联函数消除高阶函数开销

Kotlin的高阶函数很强大,但lambda会创建对象。用inline可以消除这个开销。

inline fun <T> measureTime(block: () -> T): T {
    val start = System.currentTimeMillis()
    val result = block()
    val end = System.currentTimeMillis()
    println("耗时: ${end - start}ms")
    return result
}

// 使用
val result = measureTime {
    // 你的代码
    heavyComputation()
}

5. 用作用域函数简化对象配置

apply、let、run、with、also——这五个函数用好了,代码会变得非常「Kotlin风格」。

// 重构前
val dialog = AlertDialog.Builder(context)
dialog.setTitle("提示")
dialog.setMessage("确定要删除吗?")
dialog.setPositiveButton("确定") { _, _ -> delete() }
dialog.setNegativeButton("取消", null)
dialog.show()

// 重构后
AlertDialog.Builder(context).apply {
    setTitle("提示")
    setMessage("确定要删除吗?")
    setPositiveButton("确定") { _, _ -> delete() }
    setNegativeButton("取消", null)
    show()
}

我的建议:作用域函数虽好,但不要滥用。我见过有人在一个表达式里嵌套了3层apply/let,读起来反而更费劲。记住——重构的目的是让代码更容易理解,不是更「炫技」。

四、知识体系总览

下面这张图,是我对本章知识体系的总结。你可以把它当作一个「重构检查清单」。

代码重构知识体系 坏味道识别 重构手法 Kotlin特有技巧 过长函数 过大的类 重复代码 过长参数列表 数据泥团 依恋情结 提取函数 移动函数 引入参数对象 分解条件表达式 扩展函数 sealed class 委托 内联函数 作用域函数 核心原则:小步快跑,不改行为,持续改善

这张图把本章的内容分成了三个维度。从左到右,是一个完整的重构流程:先识别问题,再选择手法,最后用Kotlin的特性优雅地实现。我个人习惯在每次提交代码前,都对照这张图快速过一遍——有没有坏味道?有没有更好的写法?

最后说一句:重构不是一次性的工作,而是贯穿整个开发过程的习惯。每次看到代码有「不舒服」的地方,就顺手改一下。日积月累,你的代码质量会越来越好,维护成本会越来越低。嗯,这就是我做了十几年重构,最深的体会。

公众号:蓝海资料掘金营,微信deep3321