代码重构:坏味道识别、重构手法、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的特性优雅地实现。我个人习惯在每次提交代码前,都对照这张图快速过一遍——有没有坏味道?有没有更好的写法?
最后说一句:重构不是一次性的工作,而是贯穿整个开发过程的习惯。每次看到代码有「不舒服」的地方,就顺手改一下。日积月累,你的代码质量会越来越好,维护成本会越来越低。嗯,这就是我做了十几年重构,最深的体会。