多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

编程尚未被解决:复杂度管理才是真正的挑战

编程尚未被解决:复杂度管理才是真正的挑战 1. 为什么“编程尚未被解决”不是一句空话第一次看到“编程尚未被解决”这个说法很多人会觉得这是一句博眼球的标题党。毕竟我们已经有Python、Java、Rust、Go这么多语言有VS Code、JetBrains全家桶有Copilot、Cursor这类AI编程助手GitHub上每天新增几十万个仓库各种低代码平台也在宣称“人人都是开发者”。从表面看编程这件事已经被工具链武装到了牙齿怎么还说“尚未被解决”但如果你真正在一线写过几年代码尤其是参与过大型项目的维护、重构和跨团队协作就会慢慢意识到我们解决的只是“把代码敲出来”这个环节而编程真正难的部分——需求理解、抽象建模、状态管理、并发控制、错误处理、可维护性、团队协作——几乎没有一个被彻底解决。工具在进步但问题的复杂度也在同步膨胀。这就像城市修了更多高架桥结果车也更多了拥堵并没有消失只是换了个地方出现。“编程尚未被解决”这个判断核心指向的是编程的本质困难不在于语法而在于复杂度管理。语法可以学API可以查但如何在十万行代码里找到一个bug的根因如何设计一个半年后还能看懂的模块如何让五个人的代码风格不打架这些才是真正消耗开发者精力的地方。本文会从几个具体维度拆解这个判断结合我自己的踩坑经历聊聊为什么这些困难至今没有通用解以及在实际工作中我们能做些什么来缓解。这篇文章适合所有写过真实项目的人——不管你是刚入行一两年、正在被遗留代码折磨的新人还是带了几年团队、被架构决策反复打脸的老手。我不会给你一套“银弹”因为银弹不存在但我会把问题拆得足够细让你知道哪些坑是结构性的、哪些是可以绕过去的。2. 复杂度才是编程的真正对手2.1 代码量增长带来的非线性维护成本很多人对编程难度的直觉是线性的写100行代码花1小时写1000行就花10小时。但真实项目里维护成本和代码量之间是超线性关系。我参与过一个中型后端系统初期只有三个模块、大约八千行代码那时候改一个功能平均半天就能搞定。两年后系统膨胀到十二万行模块间依赖像蜘蛛网一样改一个接口要同时动五六个文件还要跑三套集成测试一个看似简单的需求排期直接变成两周。为什么会这样因为代码量增长带来的不是简单的“更多代码”而是依赖关系数量的爆炸。假设有N个模块模块间可能的依赖路径数量在最坏情况下接近N的阶乘级别。每增加一个模块它和已有模块之间就可能产生新的调用、新的数据流、新的时序约束。这些关系不会自动整理只会不断堆积直到某一天你发现改A会崩B改B会崩C而C和A之间隔了四层调用你根本想不到它们有关系。这就是“编程尚未被解决”的第一个硬核证据我们至今没有一种可靠的方法能在代码规模增长时自动控制依赖复杂度。模块化、微服务、领域驱动设计都是缓解手段但它们本身也引入了新的复杂度——分布式事务、服务间通信、版本兼容。换句话说我们是在用新的复杂度置换旧的复杂度而不是消除复杂度。2.2 状态管理并发场景下的永恒难题如果说依赖复杂度是慢性病那状态管理就是急性病发作起来能让人通宵。我印象最深的一次事故是一个订单系统在促销期间出现重复扣款。排查了两天才定位到两个异步任务同时读取了同一个订单状态都判断为“未支付”然后各自发起了一次扣款。代码逻辑单独看都没问题但并发时序一交错就出了大问题。这类问题的根源在于程序的状态空间随并发数指数级膨胀。单线程程序里你只需要考虑一条执行路径两个线程并发可能的交错路径就变成几十种十个线程并发路径数量已经超出人类直觉能覆盖的范围。我们用的锁、信号量、原子操作、事务隔离级别本质上都是在用“限制并发”来换取“状态可控”但限制本身又带来死锁、性能下降、隔离级别选择困难等新问题。更麻烦的是很多状态问题在测试环境根本复现不了。测试环境并发低、数据量小、网络延迟稳定而生产环境是另一回事。我见过一个bug只在周五下午流量高峰出现因为那时候某个定时任务和用户请求恰好撞在一起。这种问题没有通用解法只能靠日志、监控、链路追踪一点点逼近真相。编程在这里“尚未被解决”是因为我们还没有一种既能充分利用并发性能、又能让状态完全可预测的通用模型。2.3 需求模糊性与抽象泄漏程序员最怕听到的一句话是“这个需求很简单你先做细节后面再对”。因为“后面再对”往往意味着返工。需求模糊是编程的上游难题用户说的“订单列表”可能包含筛选、排序、分页、导出、权限过滤、状态流转而他在提需求时脑子里只有“一个列表”。你按自己的理解做了交付时他说“不是这个意思”于是重来。这背后是抽象泄漏在作祟。所有非平凡的抽象都有泄漏这是软件工程里的一个经典观察。你以为“订单”是一个清晰的概念但一旦涉及退款、拆单、合并支付、跨境税费这个概念就裂变成十几个子状态和边界条件。你在代码里建的模型永远只是真实业务的一个近似而近似就会在边界处出错。我自己的经验是在动手写代码之前花在需求澄清和边界梳理上的时间至少应该占总时间的百分之三十。很多人觉得这是浪费时间但返工一次的成本远高于前期多问几个问题。可惜的是这个道理大家都懂真正执行时还是会被排期压力逼着往前赶然后在下游付出代价。编程尚未被解决很大程度上是因为“理解问题”这一步至今没有可自动化的方法。3. 工具链的繁荣掩盖了哪些真实痛点3.1 编辑器与IDE补全很快理解很慢现在的IDE确实强大。自动补全、跳转定义、重构重命名、实时错误提示这些功能把“写代码”的门槛降得很低。我刚开始写代码时还在用文本编辑器加命令行编译现在的新人直接上手就是智能提示拉满的环境。但工具解决的是“输入效率”不是“理解效率”。一个典型场景你接手一个陌生模块IDE能告诉你每个函数在哪、被谁调用但它不能告诉你“为什么这个函数要这么设计”“这个参数为什么不能传空”“这段逻辑为什么不能删”。这些信息藏在代码注释、提交历史、甚至前同事的脑子里。我见过太多项目代码本身能跑但没人敢改因为没人知道改了会触发什么隐藏逻辑。IDE的跳转功能在这里帮不上忙它只能展示结构不能传递意图。所以工具链的繁荣实际上把痛点从“写不出来”转移到了“看不懂、不敢改”。这恰恰是编程尚未被解决的核心区域代码的可理解性至今没有可靠的自动化保障。命名规范、注释、文档都是人工手段依赖写代码的人当时的自觉性而人的自觉性是最不稳定的东西。3.2 测试覆盖率数字背后的虚假安全感很多团队把测试覆盖率当作质量指标要求达到百分之八十甚至九十。我待过的一个团队就定过百分之八十五的硬指标结果大家为了凑数字写了一堆断言极弱的测试调一下函数断言不抛异常就算过。覆盖率上去了bug一个没少。问题在于覆盖率衡量的是“代码被执行过”不是“逻辑被验证过”。一个if分支被走到不代表分支里的边界条件被覆盖一个异常被捕获不代表异常处理逻辑正确。真正有价值的测试是那些针对边界条件、异常路径、并发时序设计的用例而这些恰恰最难写、最花时间也最容易被覆盖率指标忽略。我的做法是覆盖率只看趋势不看绝对值。更重要的是看关键模块有没有针对性的边界测试。比如支付金额计算我会要求对零值、负值、最大值、精度丢失、货币单位混用这些情况都有用例。这些用例可能只占代码量的百分之十但能挡住百分之八十的严重bug。工具能统计覆盖率但不能替你判断哪些测试有价值这个判断仍然依赖人。3.3 依赖管理版本冲突的连锁反应现代项目很少从零写所有东西大量依赖第三方库。这本来是为了提效但依赖管理本身成了新的复杂度来源。我遇到过一次线上故障根因是某个间接依赖的小版本升级改了一个默认超时时间导致下游服务批量超时。这个依赖不是我们直接引入的是依赖的依赖藏在依赖树的第四层。依赖冲突的麻烦在于你无法控制别人怎么改他们的库。语义化版本是个约定但遵守的人不多。一个小版本号变动可能只是修了个文档也可能悄悄改了行为。你锁版本能避免自动升级但锁太久又会错过安全补丁。这个权衡没有标准答案只能靠持续关注和及时升级来平衡。更隐蔽的是依赖带来的“知识负债”。你用了某个库就得理解它的行为、限制、坑。库越多需要理解的东西越多。有时候为了省两天开发时间引入一个库结果花了两周去排查它引发的诡异问题。这种账很难算清楚但每个有经验的开发者都吃过亏。4. 人的因素协作与认知的硬约束4.1 代码风格之争背后的沟通成本团队里只要有超过三个人就一定会出现代码风格分歧有人喜欢早返回有人喜欢嵌套if有人用长变量名有人用缩写有人写注释有人觉得好代码不需要注释。这些分歧单独看都是小事但累积起来会显著拖慢协作效率。代码评审时花大量时间讨论风格而不是逻辑这是极大的浪费。我后来学到的经验是风格问题必须用工具强制统一不要靠人自觉。格式化工具、静态检查工具能自动处理百分之九十的风格分歧剩下的百分之十写进团队规范评审时只关注逻辑和边界。把人的精力从“这个空格该不该有”转移到“这个边界条件处理对了吗”才是有效的协作。但工具能统一的只是表面风格更深层的设计风格很难统一。比如有人习惯把逻辑写在服务层有人喜欢放在模型层有人倾向贫血模型有人坚持充血模型。这些分歧涉及架构理念不是格式化工具能解决的。最终还是要靠团队在项目初期达成共识并在实践中不断对齐。这个过程没有捷径只能靠沟通和磨合。4.2 知识传递的断层与遗留代码每个团队都有“只有某个人能改”的模块。这个人一旦离职或转岗模块就变成黑盒。我经历过一次核心模块的交接原负责人走得很急文档只有半页注释几乎没有。接手后第一个需求就卡住了因为有一段逻辑看起来毫无意义但删了会出问题。最后靠翻半年的提交记录和日志才推断出那段逻辑是为了绕过某个上游系统的历史bug。这就是知识传递的断层也是编程尚未被解决的另一个硬伤。代码可以复制但代码背后的决策上下文很难复制。为什么用这个算法不用那个为什么这里要重试三次为什么这个字段不能为空这些信息在写代码的人脑子里如果不主动记录就会随人流失。我的应对方法是强制要求关键决策写进代码注释或设计文档并且把“能否向新人解释清楚”作为代码评审的一项。如果一个模块没人能解释清楚那它就不算完成。这个要求听起来简单执行起来很难因为赶进度时最先牺牲的就是文档。但长期看这是唯一能减少知识断层的办法。4.3 估算与排期为什么总是延期软件开发延期是常态不延期才是新闻。原因很多需求变更、技术难点超预期、依赖方延迟、人员变动。但更深层的原因是软件开发的复杂度本质上不可精确估算。你可以估算“写这个函数要多久”但你很难估算“调试这个并发bug要多久”因为后者取决于问题什么时候暴露、暴露时你能不能复现、复现后能不能定位。我见过最离谱的一次延期是一个“简单”的数据迁移脚本原计划三天实际做了三周。因为迁移过程中发现源数据有大量脏数据格式不统一、字段缺失、重复记录每处理一种脏数据就要加一段逻辑加完还要验证。这种问题在动手之前根本看不到只有真正跑起来才会暴露。所以我现在做排期时会刻意留出“未知问题缓冲”通常是预估时间的百分之三十到五十。这不是偷懒是对复杂度不确定性的尊重。同时我会把任务拆得足够细细到每个子任务都能独立验证这样即使某个子任务超时也不会拖垮整个排期。5. 面对“尚未解决”实际工作中能做什么5.1 把可读性当作第一优先级既然编程的核心困难是复杂度管理而复杂度很大程度上体现在“人能不能看懂”那可读性就应该是写代码时的第一优先级而不是“能跑就行”。我自己的标准是如果一个函数需要超过三行注释才能解释清楚那它大概率应该被拆开。命名要能表达意图不要用data、info、handle这种万能词。变量名长一点没关系清晰比简短重要。具体做法上我会坚持几条函数尽量短一个函数只做一件事嵌套不超过三层超过就用早返回或提取函数避免全局可变状态状态尽量局部化错误处理要明确不要吞异常。这些规则不新鲜但坚持执行的人不多。我的体会是写的时候多花百分之二十的时间让代码清晰后面维护时能省百分之八十的时间。5.2 用自动化守住底线把精力留给判断工具能做的事尽量交给工具。格式化、静态检查、类型检查、单元测试、持续集成这些都应该自动化不要靠人肉保证。我现在的习惯是提交代码前本地跑一遍格式化和静态检查提交后持续集成自动跑测试和构建有问题直接拦住。这样评审时就不用讨论格式问题可以专注在逻辑和设计上。但自动化只能守住底线不能替代判断。比如“这个抽象是否合理”“这个边界条件是否覆盖全”“这个设计半年后会不会成为负担”这些仍然需要人来判断。所以我的策略是把机械性工作全部自动化把省下来的时间用在设计评审和边界梳理上。工具是放大器不是替代品。5.3 建立团队级的复杂度管理习惯个人习惯能解决一部分问题但团队协作中的复杂度需要团队级机制。我们团队现在坚持几条每个模块必须有负责人负责人负责回答关于这个模块的任何问题关键设计决策必须写进文档文档和代码一起评审每季度做一次依赖梳理清理不再使用的库和过时的抽象新成员入职第一个任务是给某个模块补文档补的过程就是理解的过程。这些机制不能消除复杂度但能让复杂度可见、可追踪、可管理。编程尚未被解决不代表我们只能躺平。在现有条件下把能控制的控制住把能记录的记录好把能自动化的自动化就已经比大多数团队做得好了。6. 我踩过的几个典型坑与应对6.1 过度设计抽象层数太多反而难维护刚工作那几年我特别迷恋设计模式觉得抽象层次越多越“高级”。有一次做一个配置读取功能我设计了接口层、工厂层、策略层、缓存层一共七层调用。结果一个简单的读取配置追代码要跳七次新人完全看不懂。后来重构时砍到三层代码量少了一半可读性反而上去了。这个坑的本质是抽象是为了隔离变化但如果变化本身不复杂抽象就是纯负担。判断标准很简单如果两个实现类有百分之八十的代码相同那抽象是合理的如果只是为了“以后可能扩展”那大概率是过度设计。我现在遵循的原则是不到第三次重复不抽象。等真正出现第三个相似场景时再提取公共部分这时候你对问题的理解也更准确。6.2 忽视错误处理异常吞掉后患无穷早期写代码时我习惯用try-catch把异常包起来然后打一行日志就完事。觉得这样“程序不会崩”。但后来发现吞掉异常比让程序崩溃更危险。因为崩溃至少能暴露问题而吞掉异常会让程序带着错误状态继续跑产生更难排查的脏数据。我遇到过一次数据不一致排查很久才发现是某个异步任务里异常被吞了任务失败但状态没回滚导致后续流程基于错误状态继续执行。修复方式很简单异常要么处理要么往上抛绝不静默吞掉。如果确实需要捕获必须记录足够的上下文并且明确恢复策略。错误处理不是可选项是代码质量的核心部分。6.3 低估数据迁移和兼容性任何涉及数据格式变更、接口版本升级、依赖替换的操作实际工作量都远超预期。我参与过一次接口版本升级原以为改改调用地址就行结果发现下游有十几个服务在用旧版本每个服务的升级节奏不同还要保证新旧版本并行期间数据一致。最后花了两个月才完全切换。经验是兼容性问题的成本取决于依赖你的下游有多少。在动手之前先画一张依赖图把所有受影响的方列出来评估每方的升级成本和意愿。如果下游太多且不可控那就考虑用适配层做过渡而不是强制统一升级。适配层会增加短期复杂度但能避免长期阻塞。7. 编程的未来不是被解决而是被重新定义“编程尚未被解决”这个判断并不意味着悲观。它只是提醒我们不要被工具链的繁荣迷惑以为所有问题都有现成答案。事实上每一代工具都在解决上一代的问题同时制造下一代的问题。汇编解决了机器码难写的问题高级语言解决了汇编难维护的问题框架解决了重复造轮子的问题云服务解决了部署运维的问题而每一个解决方案都带来了新的复杂度。AI编程助手是当下最热的方向它确实能提升写代码的效率但它解决的是“生成代码”这个环节而编程的核心困难——理解需求、管理复杂度、保证正确性、维护可读性——仍然需要人来判断。AI可以帮你写一个函数但它不能替你决定这个函数该不该存在。所以我的判断是编程不会被“解决”它会被不断“重新定义”。每一代开发者面对的困难不同但困难的本质——在不确定中构建可靠系统——不会消失。对我们这些一线开发者来说与其等待一个终极解决方案不如接受这个现实编程就是一件需要持续对抗复杂度的事情。把可读性做好把边界理清把自动化建起来把知识传递好这些基本功永远不会过时。工具会变语言会变但这些原则不会变。我在实际工作中最大的体会是那些看起来“笨”的基本功往往是最可靠的护城河。
返回列表