
接手过别人留的烂摊子吗就是那种十几个文件互相循环引用、一个方法写了八百行、变量名从a1排到a99、注释写着别动这段动了就炸的祖传代码。我接过还不止一次。最狠的一次线上系统每天凌晨三点定时崩溃全组轮班盯了一周日志才发现罪魁祸首是一个藏在工具类里的静态变量——它被三个不同模块共享其中一个模块在特定日期格式下会往里塞 null。修掉那行代码只用了三十秒找出它用了我整整五天。那段经历让我彻底想明白了一件事代码这玩意写的时候爽不爽是一回事交出去之后别人能不能看懂、能不能改、敢不敢改是另一回事。从屎山代码到优雅艺术品之间的距离不是天赋不是智商而是一套可以刻意训练的习惯和方法论。这篇文章我想把这些年踩过坑换来的心得从头到尾捋一遍。1. 屎山是怎么堆起来的每一个烂代码背后都有合理的理由先说结论没有人早上起来对着键盘发誓今天我要写一座屎山。每一座屎山都是无数个小的、看起来特别合理的决策累加出来的。理解这一点很重要因为只有承认屎山是系统性问题不是某个人的人品问题你才能真正找到治理它的办法。1.1 需求变更是屎山的第一推动力我观察过一个很有意思的现象很多项目的代码结构刻着产品需求的演化史。产品经理第一天说要一个用户列表你就写了个getUserList()第二天说要按部门筛选你在函数里加了两个参数第三天说导出 Excel你复制粘贴了整个方法改成getUserList2()第四天说要支持多选导出、还要带权限校验你已经不知道该改哪个版本了于是在调用入口处套了三层 if-else。这不是段子是每天都在发生的日常。需求变更本身不可怕可怕的是每次变更都直接在原有代码上打补丁而不是回头重新审视函数边界。补丁越打越多函数越来越长参数越来越复杂直到谁也说不清这个函数到底在干嘛。1.2 赶工压力让能跑就行成为政治正确先上线再说这个需求就做一次临时顶一下后面再重构——这些话是屎山的最佳养料。我见过最离谱的一个案例同事为了赶一个报表功能把 SQL 写在 JSP 页面里循环里套循环数据库连接手动管理异常全部吞掉只打印一行 log。上线确实很快但第二个月这张报表的查询把数据库 CPU 打满了三次。赶工的本质问题是让开发者在短期交付和长期质量之间被迫二选一。而人在压力下几乎总会选择前者因为负债是可以往后拖的deadline 不行。所以每次说后面再重构基本上就等于说不会重构了。1.3 陌生代码恐惧症带来的恶性循环屎山还有一个特别隐蔽的推手就是人对陌生代码的天然恐惧。看到一个 800 行的方法、一个只有四个字母的类名你会本能地想我别乱动万一把别的逻辑搞坏了怎么办。于是新需求来了你的最优策略变成了不动旧逻辑在旁边新写一段通过某种胶水代码接上去。这个策略聪明吗短期看非常聪明——改动最小、风险最小。但它有一个致命的副作用每次采用这种策略都在给屎山添砖加瓦。旧结构没人敢碰新逻辑越来越多地堆在边缘循环依赖加重模块边界被侵蚀。等到后来的人接手看到这堆相互缠绕的东西恐惧感更强就更不敢重构了。这就是一个完美的恶性循环。1.4 团队缺乏共同标准的各行其是最后还有一个经常被忽略的因素团队内部没有形成一致的代码风格和架构约定。有人喜欢写工具类什么逻辑都往里塞有人习惯在 Controller 里堆业务有人钟爱全局变量有人从来不给函数写注释。每个人单独看都是还说得过去的代码拼在一起就成了四不像。这个问题在人员流动大的团队尤其严重。一套代码三种风格接手的同学要想搞清楚某个数据是怎么流转的得把三个人的习惯都研究一遍。这种认知成本比代码本身的复杂度还要致命。2. 屎山的代价它怎么吃掉你的时间、团队和产品人们在讨论烂代码的时候经常说到技术债这个词。但我想强调一个更直观的说法屎山不是欠债它是个黑洞——它会匀速、无情地吸走本应该花在产品上的时间和精力。2.1 新增功能的成本是指数增长的刚写出来的代码加一个新功能可能只需要几十分钟。如果这个模块已经迭代了半年加同样的功能可能需要一天。迭代两年之后可能一周都搞不定——因为你需要搞清楚一堆历史逻辑之间错综复杂的耦合关系改一个地方可能引发三处连锁故障。这不是感觉是可以量化的。我见过最典型的一个例子一个简单的列表页增加排序字段需求评估工时从最初的 2 小时涨到了后期的 3 天。3 天里 2 天半在梳理现有逻辑、跑通数据链路真正写代码只用了半天。这种损耗老板看不见KPI 体现不出来但它真实地发生在每一个屎山项目的日常里。2.2 更可怕的隐性成本团队士气与人员流失比时间成本更隐蔽的是士气成本。程序员也是人人对在一团乱麻里小心翼翼缝补丁的工作会产生本能的抵触。长期困在屎山项目里的人会越来越没有成就感越来越不愿意主动思考最后要么变成提线木偶式地机械接需求要么直接想离职换个环境。我带过的一个项目就经历过这种低谷。交接期我统计过半年内核心开发换了三拨新同学平均熟悉代码的时间超过一个月。人一直在走代码一直在堆这项目一度进入谁在谁倒霉的怪圈。2.3 线上事故的定时炸弹如果说时间和士气是慢性病那线上事故就是急性发作。屎山代码最大的问题不是丑而是不可预测——你不知道改动某个看似无关的地方会不会引爆哪里。全局变量、隐藏的共享状态、隐式的执行顺序依赖、吞了异常但没做兜底逻辑……这些都是埋在地下的雷。我之前那个凌晨崩溃的案例就是典型。写那个静态变量的同事绝对没有恶意他甚至觉得自己写得挺规整——变量名虽然不是最优但也算语义清晰。他只是没意识到这个变量会被另外两个毫不相关的模块悄悄修改。这种问题在设计清晰的代码里几乎不可能出现在屎山里面却防不胜防。3. 拆山第一步重构之前先看清楚你面对的是什么好现在进入核心部分。假设你已经接手了一座屎山或者你意识到自己在亲手堆屎山接下来该怎么办我的答案可能和很多人预想的不一样不要一上来就撸起袖子改代码。重构最忌讳的就是感觉哪里不对就开干。你连地图都没有怎么拆迷宫3.1 先画地图理解现状比动手改代码重要一百倍拿到一个陌生项目我建议你先花几天时间做一件事把模块关系图画出来。用工具或者手绘都行核心是回答这几个问题系统有哪些大的模块它们之间的调用关系是什么有没有循环依赖哪里有明显的上帝类什么都干的那个和黑洞方法所有人都调用的那个数据是怎么流转的从入口请求到数据库中间经过哪些层哪些地方做了隐式状态修改哪些代码是可以删掉的哪些是历史遗留的死代码哪些是虽然没人知道但还在跑的定时任务这一步不需要一上来就精确到每个函数。先抓住主干把最粗的那几条依赖线画出来。等你有了图很多问题自己就浮现了。3.2 用代码体检指标客观评估烂的程度感觉不重要数据才重要。有几个特别好用的客观指标建议你在重构之前先跑一遍指标含义警示阈值循环复杂度方法里独立路径的数量越高越难测难懂单方法超过 15方法长度单个方法源码行数超过 100 行就要警惕类行数类是否承担过多职责超过 1000 行通常是上帝类依赖方向是否出现循环依赖、底层依赖上层出现环就是大麻烦重复代码率相同或相似的代码片段占比超过 5% 就值得处理注释密度复杂逻辑是否配了说明关键方法无注释要补跑这些指标用什么工具后端 Java 项目用 SonarQube前端 TS 项目可以用 ESLint 内置复杂度规则加上 Code ClimatePython 项目用 Radon都挺成熟的。注意指标不是拿来批判人的是拿来定位重灾区的。不要试图一口气全部清理先从指标最辣眼的那个文件开始。3.3 确立重构的行为不变原则重构的定义里有一条铁律重构不改变代码的可观察行为。也就是说重构前后同一个输入必须得到同一个输出系统对外的接口、返回结果、异常类型都不能变。这条原则听起来像废话但做起来非常容易破功。很多人在重构过程中顺手优化了一个逻辑——我觉得这里应该提前返回这个判断应该挪到前端去做。停。这已经不是重构了这是改功能。改功能不是不行但两个事情混在一起做会出大事。我的实操建议是每次重构只做一件事。要么纯结构调整要么功能变更不要趁乱夹带私货。夹带私货的后果就是出了 bug 你根本分不清是结构调整引入的还是功能变更引入的排查成本直接翻倍。3.4 安全网重构前先把测试补上我知道很多人对补测试这件事非常抗拒特别是面对屎山的时候——整个模块就没有一个测试你要我怎么补但请相信我没有测试的重构就像没系安全带的杂技表演危险且不负责任。补测试的策略不能贪大。挑一个重灾区模块从外部入口开始写几个针对输入-输出的用例先把核心行为锁定住。你不需要做到 100% 分支覆盖你需要的是如果我重构改坏了测试能第一时间响。实际操作中我常用的做法是特征测试Characterization Test给一个已知输入记录当前系统的实际输出写进测试断言里。这样就算这段代码的行为看着很奇怪、甚至像 bug测试也能把它锁住。等重构完成后你再决定要保留这个奇怪行为还是修复它——那是下一步的问题。4. 科学拆山从最臭的角落开始用绞杀者模式逐步替换地图画完指标跑完测试网兜住了接下来才真正开始动刀。这里我要先纠正一个很危险的误判很多人觉得重构就是把整个项目推翻重写。听起来很爽但这是最坏的决定——你会在毫无保护的情况下撞上之前提到的所有历史逻辑而且新代码和旧代码要在同一个系统里共存复杂度只会更高。我更推荐绞杀者模式Strangler Fig Pattern。形象点说就像榕树慢慢绞杀宿主一样你一点一点地用新结构替代旧结构每次替换一小块系统始终保持可运行、可上线。整座屎山不是在某个伟大时刻被推翻的它是在一次次不引人注目的迭代中悄悄消失的。4.1 第一步挑一个收益最大、风险最小的入口从哪里开始拆我的经验是从流量入口开始而不是从核心逻辑开始。比如一个老系统是传统的三层结构Controller 里堆了乱七八糟的业务代码。那你可以先做一件事——把 Controller 里的代码拆出来放到独立的 Service 类里面。这一步风险极低因为只是搬代码不改变任何逻辑。但收益非常大Controller 瘦身之后你立刻能在入口看到清晰的业务动作边界后续所有新需求都往新的 Service 里写没人再往 Controller 里堆垃圾。我给个简化版的例子重构前可能是这样的// 重构前一个 Controller 方法干了所有事 async function createOrder(req, res) { const user await db.findUser(req.body.userId); if (!user) { return res.status(400).json({ error: user not found }); } if (!req.body.items || req.body.items.length 0) { return res.status(400).json({ error: items empty }); } let total 0; for (const item of req.body.items) { const product await db.findProduct(item.productId); if (!product) { return res.status(400).json({ error: product not found }); } if (product.stock item.quantity) { return res.status(400).json({ error: stock insufficient }); } total product.price * item.quantity; } const order await db.createOrder(req.body.userId, req.body.items, total); res.json(order); }重构后Controller 只负责 HTTP 层的事业务搬到 Service校验逻辑独立成方法// 重构后Controller 只需要做参数收口和响应 async function createOrder(req, res) { const dto buildCreateOrderDto(req.body); try { const order await orderService.createOrder(dto); res.json(order); } catch (err) { // errorHandler 统一映射 400/404/500 errorHandler.handle(err, res); } }代码量没有大幅减少但结构清晰度完全不同。后续任何人加需求都知道去 Service 找业务逻辑而不是在 Controller 里再堆一组 if。4.2 第二步消除上帝类和黑洞方法Controller 瘦身之后下一个目标指向系统里最臭的那坨那个三五千行、几十个方法、谁都在调用的上帝类。处理它的办法不是重写而是按职责分家。做法很简单你把上帝类里所有的方法列出来然后按它们操作的数据和它们表达的业务概念分组。比如一个经典的UserService里既有登录、注册、改密码又有查订单、发优惠券、弄积分那你至少可以拆成AuthService、UserOrderService、UserPointService三个类。注意一个关键技巧拆的时候先建新类把方法搬过去然后让老类的方法变成一行委托调用。这样每个拆分步骤都保留原有调用路径哪怕搬错了也能立刻回滚。4.3 第三步处理循环依赖和隐式状态拆完上帝类你大概率会在依赖关系图上看到一些环A 调 BB 又调 A或者 A 调 BB 调 CC 又调 A。循环依赖是屎山最典型的结构腐烂信号。处理循环依赖最常用的手法是引入接口/抽象层。比如 A 和 B 相互依赖你可以在中间插一个接口IB让 A 依赖IBB 实现IB把编译期/运行期的环切断。另一个手法是提取共享依赖把 A 和 B 共用的那部分逻辑抽到一个独立的模块 C让 A、B 都只依赖 C谁也不依赖谁。至于隐式状态也就是全局变量、静态类缓存、线程局部变量这些东西处理思路是显式化。把隐藏的共享状态改成显式的参数传递、依赖注入或者上下文对象。这一步经常需要动到调用链上的所有相关方所以务必配合前面说的特征测试。4.4 每一步都独立上线不要让重构变成大爆炸绞杀者模式的核心是增量。每拆完一个模块都要保证系统能编译、能测试、能上线。哪怕只是把一个类从 2000 行减到 1800 行这也是一次完整的、可独立验收的迭代。为什么这么强调逐步上线两个原因。第一改动越小风险越可控出了问题排查范围越小。第二也是更重要的你是在给团队建立信心。当你一次次用小改动、零故障完成重构团队对重构这件事的恐惧会逐渐消散。大家会开始觉得哦原来改代码不是走钢丝是有方法有节奏的。5. 给代码装上护栏让优雅不再依赖个人自觉说到这可能有同学会想我现在明白了重构的方法但我挡不住同事继续写烂代码啊。我一个人把代码保持得很优雅旁边的人呼啦啦堆屎山怎么办这个问题非常真实。事实上一个团队里如果只有一个人追求代码质量那个人通常活不过三个月——要么被需求淹死要么被同事当成事儿多。真正的解法不是靠个人英雄主义而是靠流程工具和团队共识把优雅变成系统的默认行为而不是某几个人的自觉。5.1 用工具强制统一风格不要给我觉得留空间风格问题是最容易吵架也最没必要吵架的。变量命名、缩进、引号、排序、导入顺序……这些事一旦交给人工检查既浪费 code review 时间又容易引发毫无价值的争论。我的建议是把所有能自动化的检查全部交给工具。ESLint Prettier前端、CheckstyleJava、Black RuffPython、golangci-lintGo该上就上并且要接进 CI 强制卡口。不通过就不让合并没有任何豁免。一开始团队肯定有人抵触但习惯之后都会真香——因为省下来的时间是真金白银。这就像汽车的安全带系着不舒服但关键时刻救命慢慢也就习惯了。关键点在于工具规则是公开的、中立的、对所有人生效的它把你的审美和我的习惯之争变成了我们一起商定的规则。5.2 把代码审查从找茬变成结对共建Code Review 是最被低估也最常被用砸的环节。用砸的方式有两种一种是不 review合代码全凭自觉另一种是 review 时只盯着风格问题把 CR 变成了互怼现场。我的建议是 review 的重点要按优先级排逻辑正确性这个改动会不会引入 bug边界条件考虑了吗结构合理性有没有在错误的地方写逻辑有没有引入循环依赖职责是否清晰可读性与可维护性下一个接手的人能看懂吗变量名是否如实反映含义有没有没必要的复杂性命名和风格放到最后而且如果工具已经卡了风格这块基本不用人看。另外强烈建议用提问式 review而不是命令式 review。你这里为什么不返回 400比你必须改这里要好得多。前者是在讨论后者是在对抗。5.3 提高测试覆盖率优雅代码要有测试兜底我可以很坦率地讲一个没有测试的项目哪怕结构再整洁也是一个定时炸弹。重构出一套漂亮的分层架构当然好但如果没有测试保证行为不变明天一个粗心的改动就能让所有优雅瞬间崩塌。所以要对覆盖率的刚性目标做个务实规划。新代码必须有测试这是底线。Core 模块的测试覆盖率要优先提上去先到 60% 再到 80%。测试的取舍上优先保证核心业务链路的覆盖而不是追分支覆盖率的数字游戏。实际经验告诉我测试最大的价值不是证明代码对而是让你敢改。当你敢改代码了你的重构意愿和重构能力才会进入正向循环。5.4 定期代码卫生日让治山成为一种制度代码质量的维护不是一次性的工程而是持续性的投入。我见过几种做法最有效的是每个迭代拨出固定比例的时间专门处理技术债比如每个两周的迭代里抽出半天专门干和业务需求无关的事删死代码、补测试、给复杂方法加注释、升级依赖库版本。这半天看着浪费产能但它带来的回报是指数级的。它向团队传递了一个清晰信号代码质量不是嘴上说的优先级是实际上时间的优先级。有了这个制度屎山的增长速率就会被踩住刹车至少不会继续恶化。6. 从会写到会维护程序员的真正修养藏在哪聊完方法论最后我想谈谈修养这个词。标题里说这是程序员的最高修养我觉得没有夸张。很多人对代码写得好的理解是数据结构用得溜、算法题刷得转、新框架上手快。这些当然是一个程序员的技术功底但修养是另一码事。我用一句话总结我的理解修养就是你在写代码的时候心里装着那个三个月后坐在屏幕前的陌生人——他可能是你自己。6.1 换位思考六个月后的你也是别人说个真实经历。有一次我接手自己半年前写的代码盯着一个函数看了十分钟完全没想起来当初为什么这么写。最后看了一眼 git log才发现那个函数是为一个已经下线了的活动临时加的补丁。这事儿给我的冲击很大——我以为自己写的代码自己能懂但现实是记忆是会衰退的代码不会。所以每次你想省掉注释、想用聪明但隐晦的写法、想把临时逻辑塞进一个不相关的方法时请记住你不是在给别人添麻烦你是在给未来的自己埋雷。反过来如果你每次写代码都预设会有另一个我来 review会有陌生同事来维护很多当时爽一下的冲动就会自然消失。6.2 追求删掉代码而不是堆叠代码我见过很多刚入行的同学特别容易把实现功能等同于增加代码。需求一来潜意识里就想我该在哪加一个 if、我该新写一个方法。但代码质量最高的时刻往往不是你加了什么而是你删掉了什么。一个优秀工程师的日常很大一部分时间在做减法发现一个没用的参数删掉发现一段重复的逻辑抽取合并发现一个绕了三层才找到的取值方式改成直接传递甚至发现整个模块都没有存在的必要和产品沟通之后连根拔掉。减法的核心价值在于降低系统的总复杂度。复杂度和代码行数不完全是正比关系但很大程度上代码越少需要维护的路径越少出错的机会就越少。修养体现在你不以写了多少行为荣而以这个系统少了多少不需要的东西为傲。6.3 把怪罪换成归因从人身上转移到系统上最后这点我认为是最难但最值钱的修养。在屎山项目里最常见的心态是甩锅这段代码是 XXX 写的不关我事。 说真的我年轻时也这么想过。后来看得多了我发现一个残酷的事实绝大多数烂代码不是某个人蠢而是某个环境逼出来的。复杂的代码背后往往是混乱的需求流程、不合理的时间压力、缺失的代码评审机制。所以当新人把代码写烂了比起骂他一顿不如帮他看看是不是设计文档没给清楚、是不是测试资源没配好、是不是他觉得这个逻辑只活一周没人告诉他它要活三年。当你开始从系统层面找原因你会发现同样的烂人在不同的环境里表现完全不同。这也是为什么我在前几节花那么多篇幅强调流程、工具、制度——因为个人的修养再高也对抗不了系统性的熵增。只有把优雅固化到流程里个人的修为才能真正生根发芽而不是随着某个人的离职而烟消云散。最后说一个我自己一直在用的收尾技巧每次提交代码之前我都会把这次改动重新读一遍模拟成一个第一次看到这段代码的人。如果发现有不看注释就完全不懂的地方或者读了三遍还是觉得绕的逻辑我就停一停先把它改清楚再合。可能有人觉得这样做很慢。但这么多年下来我的经验是慢就是快。花在消歧义上的每一分钟都会在未来的某一天为你或者你的同事省下整整一个通宵。这就是我从屎山里爬出来之后学到的最值钱的一件事。