航母好背,屎山难躲

730 阅读19分钟

最近一直在语雀上记笔记和idea,也准备尝试在上面写文章并分享(可对互联网公开和它的花园分享体系)。试了一下后发现语雀笔记的分享成本好高!以我营销人的视角看,这个创作者体系是很难做起来了。不过,在玉伯离开后,语雀是不是就已经半死不活了。

所以还得来掘金上分享文章。好久没登过掘金网站了,上次发文还在四年前。但是掘金也早就凉了(我竟然也参与过掘金的建设,哈哈),也不是很想在掘金里分享,markdown这古老的回忆。不过u1s1,月影虽然不大行,但是坚持住了掘金作为技术社区的干净。

语雀原文链接:www.yuque.com/u25369404/d…。竟然还只有半年的公开权限!

还得思考思考该去哪、该怎么分享文章,公域上没看点没热点、私域上没流量。忽然觉得团队分享、公司内部平台分享,才是最友好的。

有人的地方就是江湖,有代码的地方就会出屎山

某天,小伙伴和我分享面经,说问了面试官一个问题:业务迭代这么快,怎样避免代码堆成屎山而没办法维护。面试官说了挺多的,先指出了这个问题很宏大,不仅仅是代码或个人的问题,说得先看部门,如果部门成员经常变动,代码质量肯定是没办法保证。再者来看长期的维护,需要相互review代码、需要部门里有架构能力比较好的小伙伴,等等。

看到这个问题和面试官的回答,我内心思绪万千:因为不久前转岗来了xxx,接手了至今见过的最烂代码;因为雄心壮志挑战这屎山,却被现实一棒敲晕。另外,面试官和我是同一个二级部门的,应该对这频繁的组织调整都深有体会。

对于代码腐化而沦为屎山,我也从我的视角和认知来讨论下。

题外话,更大的视角看,本质是软件工程没有「工程规范」。不同于机械工程、土木工程,强依赖于科学合理的设计方法与实施规范,比如豆腐渣工程就是因为不按规范或偷工减料导致,出事是早晚的。以画图来举例,机械、土木的图纸和设计都是百分百强制的规则。但是软件设计所教的UML图,甚至只是个概念。在没有硬性规范的基础上,很多东西自然是没有保障的,更别谈在这“过于”敏捷的软件迭代互联网环境。当然,这是软件行业的性质及其发展的问题。但本文主要从开发者、更贴近工作的视角上去聊聊,主要我也没什么资格聊行业。

首先,我也觉得宏观操盘手才是最关键的要素,即组织和LD。 比如部门调整、业务交接,一年搞个两三次,那么原有逻辑的坑和背景就全丢光了,新接手的人不会、也不敢动这些历史代码,所以大概率也无法在上面写出好代码,更不要说接手来的本就是一坨两坨的。还有个有趣的观点:无论你的代码写得怎么样,在交接人眼里都是垃圾罢了。当然,有人可能会说交接新业务来了就重构一把(我以前也是这么想的),但是实际上,重构这个事还得看老板。至于LD,想起xxx说的一句话:LD的视野就是这个团队的天花板。除了业务和技术的收益外,是不是重视稳定性、要不要搞性能优化、代码要怎么样写,我认为是全依赖LD的态度。比如最常见的code review,同事在需求上线前找你cr,你觉得代码写的不好或有问题,那是改不改呢、上线要不要delay呢?业务方有个需求很急,可以trick方法快速实现,那要不要快速搞一把呢?这几个问题的本质是规范。而规范需要有人制定、有人背书,其他人遵循。回过头说重构的事,之前有个超资深同学问过我:为什么要重构?如何像老板证明需要重构?重构具体有什么收益,是很难定性的,比如迭代效率、能力可扩展性、性能优化,这个饼香不香最终还是看LD是否认可。我经历的几个LD,并不关心代码有多烂,也不会考虑是因为代码或接口的腐化而导致迭代效率低下,更喜欢看业务收益。

追求业务收益,更是引起代码劣化的推手,甚至是故意为之,并且大概率长期存在。 无论是蓝海时期先出功能抢占先机,还是红海时期先出mvp版本进行前置验证,都在讲究敏捷迭代,恨不得今天提需明天上线。不管是不是真敏捷吧,反正发展越是高速期,越是不会管技术方案和架构质量(说实话也没时间),而更关注快速迭代与试错性。不过这好像是全互联网的通病吧。实现和迭代就是熵增行为,是挑战和破坏架构的行为。抖音电商直播间初期也就是个草台班子,没有几行代码是优雅的,此时完成需求最要紧。而在直播间重构期间,最早也只是先治理稳定性,而后才花了一年多的时间完成重构。甚至还有些尝试性的业务功能,使用量和收益真的很少,但是会大幅度提升全链路的复杂度,印象最深的是电商的搭配购(淘宝、抖音电商、tt电商都有这玩意)增加了购物车近两倍的复杂度。

最后是研发,可能有这几点问题吧:(1)年轻程序员在一个不太关注技术的环境时,其“年轻”的知识、技能与经验就很容易产生有问题的或破坏架构的代码;(2)心态,或者说摆烂了,因为我觉得能进大厂的人,能力肯定没什么问题,所以那种随处可见的奇怪命名、copy-code等问题大概率是程序员的心态导致的,包括不考虑上下文只想着赶工、只关注眼前的代码;(3)炫技,常见的就是给一个简单功能套个新的框架,搞个状态机、设计模式,把简单的打包逻辑搞成复杂看不懂的流程编排,纯纯过度设计。

债务代码或叫屎山,是哪都有

程序员老是喜欢吐槽别人的垃圾代码为屎山,如上(hahaha)。但是这个词是有点难听的,胡蜂(京东)称代码即是资产又是债务,我觉得这个比喻挺好的

恰好看到腾讯社区里讲屎山/债务的两篇文章,很多观点都如出一辙(文章中描述得比我更详细更生动些),比如为什么代码会腐化、代码如何能变好。这里也简单摘抄部分深有所感的内容:

假设人的专业技能已经无可挑剔,排除人的原因系统本身就会变得越来越复杂,系统熵增定律是让全宇宙都绝望的定律,无序的增加是事物发展的必然结果,其本质就是时间定律。熵增更具体的表现是耦合度越来越高。
敏捷在很多情况下是不思考的遮羞布。拿到需求不考虑合理性,不做分析、不做设计、不考虑可维护性,上来就是写代码,口头禅是“先扛住再优化”,美其名曰敏捷。可实际上这和敏捷八杆子打不着,图了一时快却得到了一屁股债,后期需要花费大量的时间治理债务。
理想中的软件开发团队,随着功能的不断增多,代码和功能理论可服用率逐渐升高,开发成本应该至少保持一个线性的关系。而且如果引入了一些重大的新技术,开发成本的线性增长率还会更低。如果团队有一个好的技术架构和合理的模块划分方案,考虑到可复用性可以减少成本,functionalities-cost 的关系会更平缓。然而,现实可能会给你沉重一击。随着向系统中添加越来越多的功能,实现每个功能会变得越来越困难而不是越来越简单。复用?基本不存在的。
业务系统复杂的根本原因:
当系统变得复杂,功能之间会逐渐产生耦合,它们的关联关系也会变得复杂。到项目后期,每新增一个变更,除了修改这个变更本身,可能还要修改和它耦合的n+1 个位置。而且没有办法通过软件上的优化来消除这种复杂性,因为复杂性是不灭的。
代码腐化是不可避免的,比如在所谓的敏捷迭代中,架构设计和模块抽象只能面向当下,它就天然是短视的

为什么需要清理债务代码

说了很多代码腐化成屎山的原因,那么问题来了,代码债务和屎山有什么问题吗?或者说好的架构、好的代码有什么用呢?我觉得首当其冲的应该是程序员的幸福感,看着这啰里八嗦的代码、不知所以的逻辑和注释,真的很痛苦。再说软件工程维度的,代码逻辑可以分成业务功能、调度&架构、可观测性&稳定性三类,屎山的危害也相对应:(1)迭代效率低下,比如难以梳理改动点的影响面,甚至不知道现状是什么、该改动哪里;(2)垃圾代码自会藏污纳垢,产生很多性能浪费,比如无效或重复的遍历、调用下游;(3)降低稳定性,提升问题排查的难度。

继续第二个问题:有问题的代码债务和屎山需要清理吗?分两种情况吧,其一,这个业务跑着跑着出大事故了,或者跑不动了。此时才会回过头来审视:为什么开发个小功能要这么久的时间,为什么某功能总是问题频出,等等。否则,以业务和大多数LD的视角来看——能跑就行。不过这也是符合社会发展规律的,就好比先买个老破小住着,有钱且刚需后再换大房子。其二,代码运行而产生了业务功能,而业务有价值时才证明这段代码是有价值的。在全屏都是快速迭代feature的业务下,一些新功能很快被提出落地,又很快被淘汰。吐槽一下,PM最爱做的事情就是功能虽然没人用,但是要先留着、不下线。那么,某一段没什么流量没什么产出的代码逻辑有必要管吗?其实就是有效工作和性价比的衡量,答案很明确,技术要以解决问题,创造收益为目标——这种代码自然无需去管理,甚至出bug都没人发现。

回过头再看问题“为什么需要清理债务代码”:解决问题,以支撑业务更高的天花板。 当然,从组织的角度来看,就是找点事情做。

清除债务太难听,那就叫架构升级

随着业务的发展,屎山是没法避免的,毕竟熵增是不可逆的、且人天然是惰性的。对有价值的功能和代码,若真动了治理的心思,又要如何做呢?唯一方法就是重构。重构除了治理和解决眼前遇到的问题,另一个重要目标是应对未来一段时间内的需求迭代和能力扩展性。 每个系统及其存在的问题大有迥异,本文也就讲不了重构的套路和思路。不过提一下几个重构中常被忽视的点:

要确认重构的结果和效果,历史债务的偿还情况。 直白点讲就是总结报告,其中的具体收益是什么、相关数据展示,其实是在重构方案中就设计好的,而不是最后回过头来找收益找数据。比较强烈的感受就是,以终为始。如果“以终为始”,在发现结局不一定好、不一定满足最初目标时,这次重构方案应该被修改或叫停。另外,现在处处流行向上管理,这个报告怎么写得漂亮还是比较讲究的。像我,天天被喷说描述的内容不够拔高、不够高层次。

对于债务偿还情况,也包含新债务引入问题。重构完、处理完屎山,也不表示任务完成,因为随着需求继续迭代,熵增依旧在继续。而不加管制的变更马上就会对新架构尝试冲击和破坏。所以要进行避免或者减缓腐化的速度。也就是说,重构的结果还应该包含新架构的规范。 不过这个规范是不是能运作起来,回到文章最初,依赖组织、LD、开发者多个维度的努力。

最后一点关于重构的时机。 最幸运的是可以跟着业务的大变更,顺势而为,直接跟着进行全盘的重新设计。此时因为业务功能也在变更,很多历史遗留问题、差劲的实现都可以拉出来讨论和重写。不过这种情况很难得,比如抖音电商的商详/SKU面板改版,阿里的五彩石计划、单元化改革。其次,就是在系统遇到问题、或者实在没事做了就找点儿问题出来,然后针对问题进行架构层次上的优化。最后,xxx分享时引用到“在每次迭代时进行重构”,我的理解就是要时刻对抗系统复杂度的煽增,其实对应的就是日常的代码质量和规范。不过日常再怎么做优化,也只是局部的,对于架构、整体结构、业务形态上等问题,还是需要靠周期性地复盘和重构。

顺便吐槽吐槽我的这次重构

文章最初提到了我最近对xxx进行了重构,重构细节和优化内容,在部门内做了分享。这里针对重构这件事情,复盘复盘遇到的问题和个人的思考,真的是每个阶段都在踩坑。

  1. 立项阶段

接口模型和逻辑代码的烂,是整个部门甚至其他团队,都众所皆知的事实。但是历史以来,要么就没人去优化,要么就优化出另一坨屎。而我这个刚来的愣头青接手后,就满腔热血地出手了。(btw,这里的重构只针对xxx业务,因为我接手的是这个方向,而我对这块业务的理解也是较为深入的。部门维度的多业务一起横向重构,我暂时还没有这个能力,仍有待进步。)

要重构的事情是很快被确认,但是立项和方案设计是一点也不顺利。最初我的设想是针对接口维度直接重写:全新的架构、新的业务身份、新的交互模型、废弃现存的垃圾逻辑。然后第一棒就是高阶和LD在质疑为什么要新写,说白了就两句话:“现在的代码能不能跑”、“成本很大,但收益呢”。当然我给出的一系列理由不够solid;更本质的是这次重构的目标只是清理问题,没有太多的未来规划。

这里的教训是不够坚定,高阶和LD他们只是基于经验提问,他们不对结果负责,甚至不了解现状。他们都给不了建议,那为什么要听他们的质疑呢?具体方案和落地,还是建议和执行者开发者进行详细的沟通讨论,也就是最后谁为这段逻辑这个接口这个服务擦屁股。

  1. 方案设计

本来重构就是重构,不简单但很独立的一件事,但是TL说要带着「表达一致性」项目一起做(第二棒)。为什么要一起做呢?TL的意思就是「表达一致性」项目是对外的,可以刚好把重构的内容对外推出,比如接口迁移、模型迁移之类的。但是我们的重构都还没开始啊......所以方案设计就变成了围绕着这个项目去开展,至于重构的细节和方案,逐渐变得无人关心,甚至没有时间去设计。

为什么要搞得这么复杂,无非还是收益的问题。重构对我有收益、对组织有收益,但是对老板没收益。

额外说更坑的经验教训,先不说「表达一致性」项目挖的坑(PM说希望通过这个项目实现对外无需定容,但是PM全程也没参与进来),就说「表达一致性」项目的方案。本来是按照“E2E收敛各域的xx表达”来推进的,结果这个目标被大老板直接拍掉了,大老板说营销C不能干E2E这个事。于是就换了个方案和思路推这个项目,其实新思路非常仓促,而且一看就有大坑。这个项目的方案和推进,我没有话语权,但是我想到了“以终为始”这句话,做项目一定要先想清楚落地效果是什么,再反向去推演方案是什么、及其可行性。

  1. 具体实现

待到我去真正落地和实现的时候,被自己的幼稚捶了第三棒:(1)试图去优化每一个功能、重写每一段逻辑,而现实是有上万行的代码,牵扯了数不清的需求、跨越了两三年的时间,不仅变更风险不可控,而且完全没精力去做这个事情啊,比如券排序逻辑就有十几种,代码和输出都分不清;(2)妄图从业务变更去解决历史遗留问题,还是举券排序逻辑就有十几种这个例子,我去找产品沟通这些功能点的优化和下线,最后都是白费口舌。

也是自己的一个成长吧,减少自己的理想化,做重构得先明确该改什么、不碰什么。 比如前文描述过,很多内容是没有优化价值的。

  1. 回顾总结

在回顾重构的收益,向LD层进行汇报时,这个结果总结文档写了几个通宵。虽然最后改得很漂亮了,但我是有点别扭,因为包装的太好了。第一版文档里,我回顾了方案的好坏和落地效果,提到实现方案并非是最理想的,然后他们说这个描述是在喷大老板的决策失误,不能这么写。简单来说,就是事情做错了但是不暴露出来,通过其他方式修修补补、遮掩躲藏。

并且结果总结更多是面向「表达一致性」项目的,服务和代码的重构优化,并没有摆在台面上详细汇报,还是挺难受的。不过有几个同学来反馈说新架构很好,听到这话其实还是有成就感的。

  1. 马上劣化

重构上线不到一个月吧,code review的时候发现两个事情:(1)标签(调度&召回&生成)的功能模块架构里,已经实现了很优雅很简单的debug能力,但一位同学为了debug在代码中打了巨多的日志;(2)一位同学觉得通用的标签AB能力太麻烦,直接硬编码+if/else一套组合拳嵌入在标签功能模块的编排流程中,破坏了模块维度的结构/架构。

我不禁在想这代码劣化的也太快了吧?到底是我的架构没架好,导致无法约束大家的实现,还是架构&代码的整洁之道就是个胡扯的事情。

最后的话

为什么要介绍我重构的经历呢?因为里面大部分的经验和坑,和屎山的产生原因、解决方案是如出一辙的:需要LD强支持、考虑业务的合理性等等。照应着解决和避免屎山的方法,在糟心的重构实践后发现这一切都非常非常的理想主义。不过也可能是因为这个部门太拉垮了。想到AllHands的一句话“逃逸平庸的重力”,但事实是重力不是由个人所引起的,是环境如此,甚至个人的影响力真的是微乎其微。更残酷的事实是,爬得越高才受重力影响越小;而先爬山的人,更有机会去修路或高空抛物;后上路的人,也没有变得轻松。

吐槽完这次重构,就开始悲观起来了,笑死。但是要鼓励一下自己:在《架构的整洁之道》推荐序中耗子叔说,要从程序员向工程师蜕变;还是要希望自己能够怀揣着匠心,写出更优雅的代码、实现更合理的架构。