ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

屎山代码的12条反面教材:你中了几条?

屎山代码的12条反面教材:你中了几条? 说起“屎山代码”我相信每个写了几年代码的人都会心照不宣地笑一下。这东西不是一个人能造出来的而是一群人、一堆需求、无数个“先这样吧”“后面再改”“能跑就行”堆出来的庞然大物——它是一款由临时方案、紧急补丁、历史包袱组成的庞大沼泽。今天这篇东西其实是我不小心总结出来的12条反面教材。每一条我都亲手写过、接手过、或者被它狠狠坑过。为了把话说透我先声明一下这12条不是为了教人怎么写烂代码而是用反讽的方式把这些坑标出来。如果你发现自己中了三条以上恭喜你你也是合格的屎山艺术家了——但真的该收手了。1. 命名与注释让读代码变成一场猜谜游戏一个项目给人的第一印象往往不在架构设计而在最简单的命名和注释上。你打开一个文件扫一眼变量名和注释基本就能判断写这段代码的人当时是什么精神状态以及他后来有没有被这段代码反噬过。想堆出合格的屎山第一步就是把代码的可读性按在地上摩擦。1.1 技巧1变量命名全靠缘分变量名是代码里最基础的信息载体也是很多人最懒得花心思的地方。想写出屎山风味最经典的套路就是给变量取一堆毫无信息量的名字a、b、c、temp、data、res、list、obj一个函数里全用这堆名字走一遍读代码的人要时刻猜“这个data到底是哪个data”。更进阶一点的玩法是用拼音缩写比如登录写dl数据写sj用户写yh提交写tj。在中文团队里这种写法看上去好像“挺好懂的”但换个人来读或者你自己隔了三周再回来看那个dl到底是“登录”还是“队列”还是“电量”完全靠猜。我见过最离谱的一个例子有人在一个函数里声明了data1、data2、data3后来需要把其中两个拼在一起他没有新起名字直接叫了data11。这个data11的语义是“data1和data1的合体”你品品这是人能看明白的吗还有个更高阶的踩坑流派叫“同名不同义”。同一个函数里result一会儿存查询结果一会儿存状态码一会儿存拼接后的字符串一个名字从头用到尾。这种代码最可怕的地方在于它表面上有名字实际上和没有名字差不多你只能通过上下文逐行推理它的真实含义。命名这件事本质上是在给下一任维护者传递信息和信任。你要是把这条路堵死了后面的人就只能靠猜、靠抄、靠注释补锅靠山吃山靠水吃水靠猜吃土。1.2 技巧2魔法数字是代码里的“神秘符号”如果说变量命名是给代码蒙上了一层雾那魔法数字就是直接往代码里埋地雷。所谓魔法数字就是那些裸写在逻辑里、没有任何注释和常量定义的数字比如if (status 3) { // 执行某个操作 }这个3是什么意思是待审核、已通过、还是已删除没人知道。经验丰富一点的开发者可能会去查一下联调文档但大部分时候文档只有一句话“状态码见接口定义”接口定义又是从某个群里转发过来的表格截图而截图已经被压缩得看不清了。魔法数字的可怕之处在于它几乎没法检索。你要是把3定义成一个常量STATUS_APPROVED全局搜一下就能找到所有使用点。但如果你让3裸奔在代码里那未来想改状态枚举的人只能靠肉眼扫描所有文件挨个判断“这个3到底是不是我要改的那个3”。我亲眼见过一个支付项目就因为某个支付状态的魔法数字被漏改了一处线上出现了“支付失败但订单已入账”的诡异case最后排查了整整一个下午。更隐蔽的是那些看似“一眼就能看懂”的数字。if (age 18)里的18是成年线这个好猜但if (timeout 5000)里的5000毫秒没人标注的话你根本不知道这是个超时阈值还是某个算法的边界值还是单纯的拍脑袋配置。想堆出地道屎山就把所有阈值、限额、状态枚举、次数上限全写成裸数字不带一点解释。后来者打开代码看就像进了个没有标牌的迷宫每一步都在赌。1.3 技巧3注释要么不写要写就写废话注释这事特别有意思。新手写代码爱写注释因为导师说“要有注释”老油条写代码不爱写注释因为“代码就是最好的文档”。在屎山的建造工程里这两种心态可以完美结合成一种灾难关键逻辑一句注释没有一眼就能看懂的代码旁边反而挂着一大串废话。什么叫废话注释看这个# 遍历列表 for item in items: # 输出内容 print(item)你告诉我这两行注释给读者提供了任何增量信息吗完全没有。它就是把代码翻译成了人话而人话版的代码在认字的人群里并不比代码本身更易懂。更绝的是有些注释还写得文词优美的抒情散文“此处为兼容老版本所做的特殊处理如无必要请勿修改”——这种还算好的至少有人味我见过更经典的是“这一段很绕不要问我为什么能跑就行了”。这种注释对后来者毫无帮助反而让人更加焦虑。真正要堆屎山需要做到业务和原因层面的注释一句不写流程和语法层面的注释哐哐写。比如你在计算一个“用户等级积分翻倍”的规则时为什么不写“这个翻倍规则只有等级大于5的用户才生效因为产品当时给了一个特别复杂的配置条件”这段文字能帮后面的人救命。但你不写你就是留他们自己对着代码发呆看着一堆魔法数字和条件嵌套一点上下文线索都没有。还有个比没注释更坑的操作叫“注释过时”。代码逻辑改了五版注释还停留在第一版。这种情况下的注释已经不是无效信息而是误导信息——后来者照着注释去理解代码会发现怎么也理解不上他要么觉得是自己脑子有问题要么花更多时间去猜“注释里说的旧逻辑和新代码到底哪里对不上”。所以想老练地堆屎山注释策略应该是要么关键信息不写写了就让它烂掉。2. 结构与逻辑把代码变成“不可拆分的艺术”命名和注释是屎山的表面装修结构、函数、模块的组织方式才是屎山的骨架和地基。要想让一座屎山真正坚不可摧、人人都怕动它下面这三条“技巧”可以说是基本功中的基本功。2.1 技巧4复制粘贴是最高效的代码复用“复用”这个词在屎山工程里是另一种含义不是抽取公共逻辑而是把同一段代码复制到每一个需要它的地方。最典型的场景是登录校验、权限判断、时间格式化、字典转换这类通用逻辑。一开始只有两个页面需要你写了两份后来需求扩张到八个页面你复制了八份再后来某个页面多了些定制逻辑你在那一份里改了两行。到这里一切都还“顺利”。直到有一天产品经理说“登录超时时间从30分钟改成2小时”或者“所有列表页的空状态提示文案统一改一下”你才意识到自己挖了多大一个坑——你需要满项目搜索“这段长得差不多的代码”找到8处对每一处做同样的修改改完之后还要担心这8处里有没有哪一处在复制之后手欠改了点小地方导致同样的修复没法一次性落地。我有个朋友可不是我自己在维护一个后台系统的时候发现一段判断用户是否有权限的代码在项目里出现了六份。每一份的核心逻辑都一样但其中两份因为当时联调时“临时加了几个条件”行为已经和其他四份不一样了。后来排查一个越权问题他花了大半天时间最后发现罪魁祸首就是那两份被“定制”过的副本之一——那段代码既没有注释说明差异也没有抽象出公共逻辑更离谱的是它藏的还是一个违规调用。复制粘贴代码本质上是把长痛变成了一个个短痛然后把短痛存起来一起爆发。一个项目里复制粘贴越多未来的修改成本就越高就像一个不断膨胀的气球总有撑不住的那一天。而且这种写法还会让代码量虚胖让汇报PPT上“代码量”指标变得很好看——但对维护的人而言就是纯粹的折磨。2.2 技巧5函数越长越能体现工作量函数这个东西设计得当的话应该是短小、清晰、单一职责的。但在屎山代码里函数是越长越“好”——一个函数干完所有事情才显得这活儿干得扎实。你打开一个函数发现它从参数校验开始到入参转换、数据查询、逻辑计算、结果组装、缓存更新、日志打印、邮件发送一气呵成几百行下来不带拐弯。这种巨型函数内部最常伴随着层层嵌套的 if/else 和 for 循环缩进一层比一层深最后甚至出现五六个大括号连续闭合的壮观场面。读这种代码就像走进了一个没有出口的迷宫每一个分支都在说“你猜猜我什么时候能走出来”。我见过最极端的一个函数有八百多行里面的局部变量多达二十几个从函数开头定义的flag到函数末尾还在反复修改它。你想搞懂它的执行流程只能逐行跟踪大脑内存直接爆炸。为什么人会把函数写得这么长说实话很大一部分原因是“拆函数太累了”。拆函数意味着你要仔细考虑参数该怎么传、返回值该怎么定义、每个片段之间怎么衔接还得给每个新函数起名字。起名字这个步骤本身就劝退了一大波人。于是大家都在心里想“反正就这一次先这么写吧能跑就行。”可惜现实中不存在“这一次”代码写出来是要被维护一个季度、一年、甚至好几年的。等到那时候再想拆你会发现风险已经大到没人敢碰了——牵一发而动全身稍微一改整个逻辑都塌给你看。还有一种更长远的套路是把“临时加的补丁逻辑”直接塞在旧函数中间。比如一个原本只是“查询订单”的函数后来要加“超过X分钟的订单自动取消”功能有人直接在查询后面追加了一段判断和更新代码。久而久之一个主功能函数变成了一个综合处理器干啥的都有唯独失去了清晰的定位。这种代码就算作者本人再过半年回来看也得先骂一句“这他娘的是谁写的”。2.3 技巧6全局变量让一切皆可“触摸”全局变量这个东西在屎山建造者手里就是一把万能钥匙。它看起来实在是太方便了不用传参不用返回值在任意角落都能拿到、都能改完全不用思考数据流该怎么组织。一个经典的场景是把当前登录用户、系统配置、全局开关、客户端环境等信息统统挂在一个全局对象上然后项目各处直接引用。问题在于全局变量的“写权限”基本等于失控。你看到的只是当前调用的那一瞬间的值但你根本不知道它是被谁在什么时候改掉的。多线程场景下更是灾难两个线程同时读改写同一个全局状态产生的竞态问题会让你排查到怀疑人生。我调试过一个内存泄漏问题查了一整天才发现有一个全局 Map 在每次请求时往里塞数据但没有任何清理逻辑——因为写代码的人觉得“全局变量方便用完再说”结果这个“再说”就再也没说过。全局变量还有一个隐蔽的坑就是它会让模块间的依赖关系变得完全不可见。两个功能看起来毫无关联却因为共享同一个全局状态而互相影响改 A 模块的代码B 模块莫名其妙出问题。这种依赖是隐性的、没有任何声明的读代码的人根本没法从函数签名或者接口定义里看出来。最后项目里的状态管理就变成了“谁都能摸一把谁都不知道摸了会怎样”典型的公共厕所状态机。更高阶的玩法是把全局变量做得半遮半掩美其名曰“单例模式”“全局配置中心”。结果还是一个道理存储层全局共享读写的边界全靠自觉。一旦团队里有两个以上的人同时在往这个全局对象里塞字段冲突和覆盖就是迟早的事。所以要把屎山堆得牢全局变量这块一定要用好、用透让所有状态都变得无迹可寻。3. 风格与设计让同事对你“过目不忘”代码是写给人看的这句话在屎山工程里同样成立只不过“给人看”的意思是“给同事添堵”。在风格和设计层面也有很多操作能让人对你的代码“过目不忘”——只不过这种记住伴随着极大的心理阴影。3.1 技巧7缩进与格式全凭灵感代码格式这事儿每个人都有自己的审美这很正常。但如果一个项目里完全没有统一的代码风格那就不是审美问题了是战争导火索。有人用 Tab 缩进有人用两个空格有人用四个空格还有人 Jupyter Notebook 复制出来的代码还带着奇怪的空格有人喜欢把大括号换行有人喜欢跟在后边有人一行代码短得离谱有人一行代码长得需要横向滚动三屏。这种混乱对维护者的第一重伤害是 diff 地狱。你只是改了一行逻辑但因为你的编辑器自动格式化了整个文件git 的 diff 里哗啦啦全是空白字符的变化看起来就像整个文件被重写了。Code review 的时候reviewer 要在一片红红绿绿里去大海捞针找真正改的那一行。要是遇到那种提交特别频繁的项目光看 diff 就能把人看吐。第二重伤害是语义层面的误导。比如 Python 这种对缩进敏感的语言缩进混乱直接就是语法错误或者逻辑错误。你从网上复制一段代码发现里面 Tab 和空格混用一跑就报IndentationError。改成规范化缩进之后代码行为可能完全变了——这已经是踩坑级别的坑了不是小事。真正的高手还会在代码里坚持用“自己的风格”并怼一切格式化工具。他们坚信“代码风格越独特越能体现程序员的个性”。于是项目里有人坚持四个空格有人坚持 Tab有人坚持把所有 if 语句的单行分支都压缩成一行——最后整个代码库变成一座万国博览馆每次接手新文件都得先适应一会儿这个文件的“地方口音”。这份“过目不忘”的功力着实让人印象深刻印象深刻。3.2 技巧8模块之间“紧密耦合”才显功力模块间的依赖关系本来是软件架构里最重要的东西但在屎山工程里大家追求的恰恰是“分不清谁依赖谁”的混沌美感。你调我的内部函数我用你的私有状态A 模块的常量被 B 模块直接引用B 模块的返回结构又被 C 模块硬编码解析——整个项目就像一张错综复杂的蜘蛛网牵一发而动全身。“循环依赖”是这个流派里的满分操作。A 服务调用 B 服务的方法B 服务又反向调用 A 服务的方法互相等对方返回。这在编译期可能能过很多语言允许这种引用但运行起来之后初始化顺序稍微不对就直接 StackOverflowError。我见过有人在启动类里规避这个问题方法是“把其中一个服务的初始化从构造函数改成懒加载”——这招确实能让项目跑起来但它同时也埋下了一颗巨雷你不知道哪天触发懒加载的时机不对整个流程就炸了。高耦合对修改的影响是灾难性的。你要给 A 模块加一个参数但 B 模块调用 A 的时候传的是另一个对象你不得不去 B 模块改调用的地方改完 B 之后发现 C 模块依赖 B 的返回结构你又得跑到 C 模块去改解析逻辑。改一个需求动三个模块code review 时 MRMerge Request长得像一篇论文谁看谁头大。这种紧密的耦合关系在屎山工程里还常常伴随“不敢动”的思维定式动一处就崩崩了就没法定位定位也理不清依赖链于是只能继续往上堆新代码。周而复始循环往复。3.3 技巧9错误处理能吞就吞能糊就糊错误处理是屎山工程里最能看出功力的地方。真正的艺术家不会让错误暴露自己的行踪他们追求的是“错误处理后一切照常进行”——哪怕程序内部已经乱成一锅粥。最经典的姿势是空 catch。捕到异常之后catch 块里什么都不写干干净净仿佛这个异常从来没有发生过try { // 可能出错的操作 doSomething(); } catch (Exception e) { // 忽略 }这个“忽略”可太优雅了。程序没有崩日志里也没有任何记录但数据少了一段、状态没更新、任务没完成——所有这些异常情况都像石沉大海没人知道为什么。排查的时候你看着代码逻辑一步步走怎么都想不通问题出在哪里因为你压根不知道这一步曾经抛过异常而异常被无声无息地吞掉了。第二流行的是“错误信息写给人看但没人看得懂”。比如在前端页面上弹出“系统错误”在后端日志里记error code: -1。这个 -1 是哪个模块的哪一层抛出来的不知道。相比之下一条 “订单创建失败库存不足当前库存仅剩0件” 的消息能帮运营同事直接判断问题而“系统错误”只会让用户对着屏幕发呆让客服挨骂让开发在日志里翻半天。还有一个错误处理的高阶操作是把所有异常类型统一捕获然后统一转成一种自己的错。这样做的结果是你看到日志里十种不同来源的异常全部变成同一种错误码排查时根本分不清是网络问题、参数问题还是数据问题。想快速堆屎山这种“一个错误码走天下”的逻辑不能不会。4. 流程与工程把项目变成“不可维护的宝藏”前面那些还停留在代码本身到了这个阶段坑要挖到工程流程和团队习惯里去了。代码最终是要进版本库、要被编译构建、要经过测试、要交给别人维护的。在这几个环节里搞事情效果是成倍的。4.1 技巧10测试那是测试同事的事“我本地跑了一下能跑呀提交吧。”这句话在屎山工程里出现的频率仅次于“这个 bug 我复现不了”。不写单元测试、不做集成测试、不补回归测试直接把代码推给测试同事让他们去“站岗”这是很多屎山项目能迅速堆起来的核心原因。不写测试最直接的后果是代码质量的防线彻底沦陷。你只是改了一个工具的细节逻辑怎么知道它会不会影响其他调用方有测试的话跑一遍就知道没测试的话这个问题的答案就变成了“等线上出了 bug 再说”。我见过一个老项目核心的支付流程没有一行自动化测试每次发版之前全靠测试同事手动点点点一旦改动稍微大一点负责点测的人就得出几十条用例。就算这样还是挡不住线上出现“这次修改把之前的某个功能搞坏了”的回归事故。更要命的是没有测试的代码会让后来者不敢轻易重构。你看到一段烂代码想改但没有测试兜底你改完没法判断自己是不是把某个暗藏的边界条件给踩坏了。改吧怕出事不改吧它一直在那里恶心人。最后的结果就是屎山不动则已一动就进入“拆东墙补西墙”的循环。这种循环一旦转起来项目就进入了“每走一步都在还债”的模式。要想把屎山堆出新高度坚持“不写测试、本地能跑就不管、线上不炸就不看”这三步曲基本就稳了。这三步曲的底层逻辑是把风险后置把责任模糊把问题留给那些比你晚接手项目的倒霉蛋。4.2 技巧11重构是吃饱了撑的“能跑就别动。”这句话在屎山工程里是最高原则也是唯一原则。重构那是给闲得没事做的人准备的我们是来干活的不是来搞艺术的。于是代码里堆积的重复逻辑、疯狂嵌套、混乱命名、直截了当的魔法数字全都完美保留——哪怕你已经知道它有问题也坚决不动它。不重构的理由听起来往往特别正当“工期太紧先把功能做出来”“这个逻辑太核心改坏了谁负责”“我这部分代码本来就没什么问题你干嘛非要动它”这些话你大概率全都听过甚至自己说过。每一句话的单次代价看着都不大就是这次不改下次再说。但技术债这东西是会滚雪球的。今天你欠了 30 分钟的重构时间明天可能就是 3 个小时的排查时间再往后就是 3 天的维护地狱最后演变成整个团队都绕着这个模块走。最惨烈的案例是团队里有人终于鼓起勇气说“我来重构吧”结果因为代码没有任何测试支撑、依赖关系又耦合得跟蛛网一样紧密重构一上线就出了事故。从那次之后就再也没人敢提“重构”两个字。屎山因此获得了“合法性”——你要是敢拆就得先为所有历史锅买单。这种威慑力比任何代码注释都有效简直是项目管理学上的奇迹。4.3 技巧12commit message能少写就少写版本管理是团队协作的最后一环也是屎山工程里最被糟蹋的一环。见过一个项目commit message 写了 “update” 的占一半写 “fix” 的占三成剩下的还有“修改”、“提交”、“1”、甚至直接是空字符串。每次想翻 git 历史查问题都像在考古只能靠时间戳和 diff 内容慢慢猜。你以为这就完了“大杂烩提交”才是真正的狠活一次 commit 里混合了三个需求的代码改动、两处格式化工具的自动调整、还有几个无关紧要的配置修改。等到出问题了你想用git bisect二分定位结果发现每个 commit 都像一锅粥根本分不清哪次改动是针对哪个功能的。commit message 的意义在于它是代码历史的索引。一段屎山代码如果有个清晰的历史脉络后人还能通过 git log 溯源理解当初为什么这么写、改了什么、引入了什么 bug。可一旦提交信息全是 “update”这段历史就彻底失明了。开发者在排查问题时只能一遍遍地看 diff眼睛看花了都找不到线索。更高阶的 commit message 流派叫“提交信息与内容不符”。message 写着 “修复了登录超时的问题”实际改动却在调整用户头像上传的逻辑。这种提交能直接把你引向错误的方向——你跟着 message 找代码找了一圈发现自己被带到了沟里。这种把版本历史变成迷宫的操作才是真正让人过目不忘的艺术。5. 写在最后从屎山艺术家到代码守夜人我承认上面这12条我几乎全部亲身实践过。刚工作那两年我简直是屎山建造的狂热爱好者一把梭写完就交哪管它后面洪水滔天。后来自己被调去维护一个“祖传项目”每天看着那些熟悉又陌生的代码才真正体会到什么叫“出来混迟早要还的”。那些当年图省事留下的坑最后都得自己亲手填上。也是从那时候开始我养成了一个习惯每次写完代码都会拿上面这些反面教材当清单逐条自查一遍。变量名是不是图省事用了temp和data数字是不是该抽成常量注释是不是废话这个函数是不是长到该拆了提交信息是不是写得别人根本看不懂没有测试是不是在赌运气如果你试着用正向的思路重新过一遍这12条你会得到一份非常实用的 code review 检查清单。最后再分享一个小技巧我在组内的 code review 规范里加了一条“code review 时把这份反面清单贴在旁边逐条对照碰到一条就指出来”。不是为了较真而是为了让那些在赶工压力下被迫写出的“临时方案”至少被看第二个人看到过留下一点记录。每个人都会在某个瞬间写出屎山代码这很正常没什么好羞耻的。但是让屎山一直堆下去直到后来的人绕道而行那就真的不太体面了。做个靠谱的程序员从给变量起个好名字、把那段魔法数字换成常量、在提交信息里多说一句人话开始。这些事看着不起眼但它们真的是区分“差点意思”和“专业”的分水岭。我自己也是这样一步步爬出屎山泥潭的希望你也能。
返回列表