十年开发告诉你什么是“烂代码”

"烂代码"这个说法在代码评审和网络论坛中一直被频繁提及。但它究竟是什么意思呢?这是一个典型的模糊术语例子,这类术语偶尔在开发者圈子中流传,却没有太多明确的界定。

甚至经常只是在评论区说对方的代码是烂代码!

我们收集了关于这个问题的各种观点、衡量标准以及代码风险,你可以借助这些内容来探讨对你的团队而言,什么样的代码质量算是"烂"的,以及如何去改进。

什么是烂代码

"烂代码" 是一个通用术语,开发人员、项目经理和其他利益相关者用它来笼统地指代一系列编码问题 ------ Bug、异味、安全问题等,这些问题会导致代码的可读性、可维护性和可扩展性较差。它并非仅仅指那些存在问题或不能正常运行的代码库。

在不同情况下,烂代码涵盖了任何未针对性能和维护进行优化的代码,从令人困惑的变量名到占用过多内存进行循环操作的函数都包括在内。

如果你的代码存在以下情况,就可能被描述为烂代码:

  • 在代码审查期间他人难以理解。
  • 随着时间推移难以维护或扩展。
  • 编写时没有正确的样式和格式。
  • 没有详细的注释说明。
  • 容易出现漏洞和故障。
  • 存在安全隐患,易遭受攻击。

我们将更深入地探讨上述每一点,但必须承认,没有哪个代码库是完美无缺的。有些时候,许可证的兼容性问题要优先于样式更改,或者安全漏洞比其他问题更为重要。烂代码的定义是因情况而异的,会随着环境变化而变化。尽管如此,还是有一些常见警示信号表明代码质量欠佳。

下面让我们来看看其中比较重要的一些:

难以理解

Kotlin 复制代码
fun magicMethod(x: Int, y: String, z: Boolean) = when {
    x > 10 && y.contains("test") -> if (z) "A" else "B"
    x < 0 || y.isEmpty() -> "C"
    else -> { globalCounter += x; "D" }
}

这段代码有什么作用?

无论是同事对你的代码进行审核,还是要对代码进行修改,他们都必须清楚代码中每个部分的含义。像使用 x1yzdata 这类变量名,会让你的代码库难以解读。同样,一个名为 xyzService 的文件,如果混用了多种缩进风格,且所有功能都写在一个名为 doStuff、长达 500 行的函数里,这样的代码既难以阅读,也不便处理。

难以维护

Kotlin 复制代码
class DataHandler {
    fun process(userId: String, callback: (String) -> Unit) {
        // 网络请求 + 数据库 + 格式化混合
        val request = Request.Builder().url("https://api.com/user/$userId").build()
        httpClient.newCall(request).enqueue(object : Callback {
            override fun onResponse(call: Call, response: Response) {
                val data = response.body?.string() ?: ""
                // 数据格式化逻辑
                val formatted = data.replace("\n", "\\n").let { 
                    // ...复杂的JSON处理和业务逻辑
                }
                // 直接操作数据库
                db.insertData(mapOf("user" to userId, "data" to formatted))
                // ...返回格式化结果
                callback(formatForDisplay(formatted, userId))
            }
            override fun onFailure(call: Call, e: IOException) {
                // ...错误处理和数据库记录
                db.insertError(mapOf("error" to e.message, "user" to userId))
                callback("Error: ${e.message}")
            }
        })
    }
}

想象一下如果让你在这些代码中更改bug!

当代码难以理解时,维护起来也会困难重重。例如,若有一个函数既要处理网络请求,又要负责数据库逻辑以及数据格式化,那么对代码进行任何细微改动都可能导致其无法正常运行。此外,由于原始工作中偷工减料,当需要对代码进行重构时,编写质量欠佳的遗留代码库会导致高昂的技术债。

难以扩展

Kotlin 复制代码
class DataProcessor {
    private val cache = arrayOfNulls<String>(100) // 固定大小
    
    fun process(data: String): String {
        if (data.length > 50) return data.substring(0, 50)
        for (i in 0..3) { // 固定4个线程
            // ...处理逻辑
        }
        // 硬编码数据库地址
        val connection = DriverManager.getConnection("jdbc:sqlite:fixed.db")
        return "processed: $data"
    }
    
    companion object {
        const val MAX_SIZE = 100
        const val THREAD_COUNT = 4
    }
}

一旦有新的变化,上面这段代码很难扩展!

优质的代码应具备适应性,能够在功能上实现扩展,在不影响性能的前提下处理更多流量或数据。难以扩展的代码可能是烂代码,因为随着时间推移,它会引发性能问题,还可能导致维护流程变长。

不安全

Kotlin 复制代码
fun login(username: String, password: String) {
    // 直接拼接SQL,易受注入攻击
    val query = "SELECT * FROM users WHERE name='$username' AND pass='$password'"
    val result = db.rawQuery(query, null)
    
    // 密码明文存储
    if (result.count > 0) {
        savePassword(username, password) // 危险!
    }
    
    // 未验证输入
    val file = File("/data/$username") // 路径穿越漏洞
    file.writeText(password)
}

代码中一些简单的错误和问题可能会导致存在漏洞,进而引发安全隐患。代码不安全的原因可能多种多样,例如将用户输入直接插入 SQL 查询语句中,却没有进行数据净化处理。在这种情况下,恶意用户通过简单的输入操作就可能获取所有用户数据。

实际项目中,除了重视代码质量之外,我们也应该关注安全问题,因为不安全的代码就是烂代码,尤其是在人工智能蓬勃发展的当下。

容易出错

Kotlin 复制代码
fun removeInvalidItems(items: MutableList<String>): List<String> {
    for (i in items.indices) {
        if (items[i].isEmpty()) {
            items.removeAt(i) // 删除元素后索引失效
        }
    }
    
    // 或者使用迭代器删除
    items.iterator().forEach { item ->
        if (item.length < 3) {
            items.remove(item) // 迭代过程中修改集合
        }
    }
    
    return items
}

多数 Java/Kotlin 开发者一定碰到过在循环中删除元素的问题。

如果你的代码没有考虑到潜在问题,那它可能就是烂代码。任何没有考虑到无效用户输入、循环、数组或其他可能问题的代码库,在不可避免地出现故障时都可能引发问题,尤其是当问题在 "潜伏" 一段时间后才出现的时候。

效率不高

Kotlin 复制代码
fun findDuplicates(list: List<Int>): List<Int> {
    val result = mutableListOf<Int>()
    for (i in list.indices) {
        for (j in list.indices) { // O(n²) 复杂度
            if (i != j && list[i] == list[j] && !result.contains(list[i])) {
                result.add(list[i])
            }
        }
    }
    return result
}

fun inefficientSort(list: List<Int>): List<Int> {
    return list.sorted().reversed().sorted() // 多余的排序操作
}

对了,如果你对集合的处理特别多,可以看看这里

性能问题会导致代码质量不佳,比如在 for 循环中,对其中的每一项都进行数据库调用。这可能会增加页面加载所需的时间,甚至导致超时。

华盛顿大学的一项研究展示了低效数据库查询的影响。该研究发现,修复这类问题可使网页响应时间缩短 98%

不遵守团队约定

Kotlin 复制代码
fun processData(input:String){
var result= mutableListOf<String>()
        for(item in input.split(",")){
if(item.isNotEmpty()){
result.add(item.trim())
                }
        }
val output= result.joinToString(",")//变量命名不一致
return output
}// 缩进混乱,行长度超限,缺少空行分隔

你可能会说现在的代码格式化不可能出现这种问题,我举得例子确实是个极端,但是你一定碰到过不同开发者不同的格式化风格的代码!

当代码不遵循公认的工作流程和风格规范时,即使代码能够运行,也会迫使你的团队成员放慢速度,梳理差异并猜测代码意图。这可能会导致大量的技术债务。例如,选择使用水平缩进还是垂直缩进、设定代码行的长度、或是尽早确定使用变量的方法。

从实践中定义烂代码

定义烂代码和好代码,有助于提高输出的质量和效率。它确保代码符合预期用途,有助于团队更轻松地协作,减少技术债务,并推动开发出以性能为导向、兼顾长期使用和可扩展性的代码。

对于参与项目的每个人来说,了解在特定情况下烂代码的样子至关重要。在一个项目中被视为烂代码的情况,在另一个项目中可能被认为是可以接受的。能够区分两者可以使预期更加清晰,让开发人员更好地理解什么是烂代码。

为烂代码下定义,还有助于开发人员在需要提高敏捷性时,明白应该避免哪些情况。例如,当需要考虑时间限制时,他们应该明确避开哪些操作以保持较高的代码质量?

烂代码的影响

我们稍微谈到了烂代码是如何影响你扩展产品、维护代码库以及让团队正常工作的能力。但现在让我们更详细地探讨一下烂代码会对你的业务产生哪些重大影响:

  • 降低可扩展性:在测试期间看似没问题的未优化循环或阻塞调用,当用户流量增加时可能会导致系统崩溃。在产品上线前还能正常运行的代码,恰恰在你最需要它的时候突然出问题了。

  • 技术债 :烂代码会使得修复问题或对代码库进行未来更改变得困难且成本更高。2022 年开展的一项研究发现,修复技术债问题消耗的时间多达开发总时间的 25%

  • 损害生产力:烂代码会浪费宝贵的时间,影响生产力。这体现在它会导致开发人员花费更多时间去解读代码,或者在更新和维护那些复杂代码时耗费更多时间。此外,由于代码复杂性增加,质量保证周期可能会变长,从长远来看,修复代码问题也会变得更加复杂、耗时。

  • 损害用户信任:烂代码会直接影响用户体验。例如,一个导致个人数据泄露或在结账时导致程序崩溃的漏洞,可能会让你数月来获取客户的努力付诸东流。用户对糟糕经历的记忆远比美好经历深刻得多。

  • 人才流失:留住优秀的开发人才也与代码库的质量直接相关。顶尖的工程师不愿意处理满是问题的代码库,因为在这样的代码库中,打开每个文件都可能会发现新的一堆问题。

上述烂代码的影响中,前三条是最直观的,也是能立即反映出来的。而后面两条影响,可能不仅仅是烂代码导致的。

如何少写烂代码

有很多方法可以帮助你少写烂代码,包括:

  • 明确任务:先设计,不要马上开始编码。退一步,多提问题,确保你完全理解任务要求。撰写问题总结也有助于梳理预期的输入、输出和限制条件,将抽象概念转化为切实的目标。

  • 选择简单方案:今天看似省时的巧妙算法,明天可能会耗费更多时间。如果你不能在一分钟内解释清楚你的代码,那就考虑与他人协作重写。后续有优化的时间再慢慢优化代码,不要急于求成。不要过早优化。

  • 避免重复:避免重复代码。当你复制代码行时,也复制了其中的错误和其他问题,这可能导致需要在多个地方花费大量时间进行修复。遵循 "三次原则" ------ 如果你编写相似代码超过两次,就将其转化为一个可复用的函数。

  • 编写短小精悍的函数 :确保你的函数实现其预期功能,不要承担过多职责。小段代码更易于理解和测试。函数行数尽量控制在 30 行以内,每个函数只做一件事。

  • 用有意义的命名:代码库中的命名要易于理解。由于代码阅读过程中人们经常看到这些名称,所以变量、函数和其他标识符的命名要让任何阅读代码的人都能清楚其含义和用途。

  • 定期测试:编写检查整体行为的测试用例,以及针对关键算法的针对性测试。定期测试使你能够放心地重构代码,因为你知道如果出现问题,可以回滚到上一个版本。

  • 使用静态代码分析工具和同行评审:静态代码分析工具可以检查代码中的问题,例如未使用的变量、无法触及的分支或安全风险。将这些检查与同行评审相结合,形成一个更严格的评审体系,让每个人都对代码质量负责。

  • 遵循统一的代码风格:就内部代码风格指南达成共识,并严格遵守。代码风格一致可以使开发人员专注于代码功能,而不必纠结于代码外观。

  • 优先考虑安全性:验证输入,对输出进行编码,妥善处理 API 密钥和密码等机密信息,可使用环境变量或机密管理工具安全存储。代码评审时,要留意注入攻击和竞争条件等问题。

  • 记录决策依据:模棱两可的地方容易产生烂代码。记录采用某种方法的理由。为什么选择乐观锁而非悲观锁?为什么服务要异步而非同步处理事件?

  • 逐步重构:从零开始重建可能会引入新的错误,导致生产延迟。诸如代码清理、替换遗留模式以及优化缓慢的数据库查询等逐步重构过程通常干扰较小,同时也更容易在过程中发现编码问题。

总结

烂代码是一个问题,可能会给开发团队、项目乃至更广泛的商业领域带来代价高昂的问题。消除烂代码语义上的模糊性,了解其表现形式,有助于你更早发现问题、更轻松地修复问题,并降低烂代码对项目的影响。

相关推荐
Java烘焙师2 小时前
架构师必备:限流方案选型(原理篇)
架构·限流·源码分析
爱吃牛肉的大老虎8 小时前
网络传输架构之GraphQL讲解
后端·架构·graphql
Curvatureflight12 小时前
GPT-4o Realtime 之后:全双工语音大模型如何改变下一代人机交互?
人工智能·语言模型·架构·人机交互
t***D26414 小时前
云原生架构
云原生·架构
jinxinyuuuus14 小时前
局域网文件传输:P2P架构中NAT穿透、打洞与数据安全协议
网络协议·架构·p2p
六行神算API-天璇16 小时前
架构实战:打造基于大模型的“混合搜索”系统,兼顾关键词与语义
人工智能·架构
顾林海17 小时前
从0到1搭建Android网络框架:别再让你的请求在"路上迷路"了
android·面试·架构
语落心生18 小时前
Apache Geaflow推理框架Geaflow-infer 解析系列(四)依赖管理
架构
云渠道商yunshuguoji18 小时前
亚马逊云渠道商:如何用 EC2 Auto Scaling 轻松应对流量洪峰?
架构