查看原文
其他

专访鹅厂专家:从工程实践的角度,谈谈如何进行代码重构?

willjxyao 腾讯技术 2022-11-18

导语| 近日,Hacker News上有一则帖子,其内容是:我继承了我见过的最差的代码和技术团队,该怎么办?帖子一经发出便收到近700条建议。有人建议这段代码不要完全重写,也有的人了解其现状,认为“跑路”才是正解。

带着这样的疑问,笔者邀请到最新一期通过红带认证(“PCG打造的工程能力认证体系,红带是具备将Readability及Testability相关技能在实际项目中熟练运用的能力”)的zhongmingqu老师,一起聊聊技术债以及重构代码那些事。

本文作者:willjxyao

受访嘉宾:zhongmingqu,腾讯PCG工程效能专家


日前, Hacker News上有一则帖子热度非常高,其内容是:我接手了一份极其糟糕的代码和一支技术团队,接下来该怎么办?帖主在帖子中阐述了其面临的挑战:继承的代码历史长达12年且是没有版本控制的PHP代码、这段代码每年能产生超过2000万美元的收入、疫情之下公司预算和人力紧张、公司管理层对解决技术债面临的阻碍因素并没有真正了解,反而制定了非常激进的路线图……

帖子原文内容
对于贴主这样的遭遇,有不少有经验的程序员深有同感,纷纷在帖子下评论回复,有的人建议这段代码不要完全重写,认为重写的新系统有可能比旧系统更糟糕;也有人了解其现状,认为“摆烂跑路”才是最优解。带着这样的疑问,笔者邀请到最新一期通过红带认证的zhongmingqu老师,一起聊聊技术债以及重构代码那些事。

zhongmingqu:腾讯PCG工程效能专家


历史遗留代码的问题普遍存在,几乎无一例外

Q1:帖子中提到的内容,软件开发公司老大难的“祖传代码”的问题普遍存在吗?
zhongmingqu:
很不幸,几乎没有例外。一般而言,产品存活越久,历史积累的祖传代码问题就越严重。技术强如谷歌亦是如此,我们自身也会面临同样的问题。值得指出,“祖传代码”这个问题并不是孤立存在的,而是与工作人员的认知水平、工程和企业文化、组织结构、市场条件等高度相关的,是一个系统性问题。
“祖传代码”这个称呼隐式地把这个问题的范围限定在“代码”层面,这在一般的谈话中并无不可。不过当我们详细谈论这个问题之前,还是有必要解释清楚这个问题的范围的。

Q2:历史遗留的代码问题一定要去处理吗?如果我不处理,会有哪些风险?
zhongmingqu:
现在的软件工程已经具有“复杂系统”甚至部分“巨复杂系统”的特征。合作的人数非常多,跨多个时区、语言和文化,涉及不同的利益实体,时间跨度巨大,等等。这些都促使我们不能仅仅以思考“简单系统”的方式考量现在的软件工程领域产生的问题,头痛医头脚痛医脚。而是要系统性地思考遇到的诸多问题,在纷繁复杂的表象中归纳、总结并抽象出主要矛盾,并且承认事物会螺旋而非单调上升。
复杂系统和巨复杂系统的根本矛盾是自发熵增导致的自我解体。也就是王熙凤说的“大有大的难处”。一个系统,大了,复杂了,利于其自身维持(熵减)的行为往往会带给其各个子系统的额外成本,当这种额外的成本不利于该子系统自身发展时子系统就会拒绝执行这一要求,诸多子系统都这样做(熵增),这个大的、复杂的系统就会趋于解体。
复杂系统发展的关键在于控制熵增的速度以及在必要情况下消除特定熵。而祖传代码往往意味着熵增到无以复加,这就非常恐怖了。
从微观上看,如果你用的技术栈很老旧、版本非常凌乱,那么老版本的库的安全问题、性能问题、功能问题又该如何去解决呢?一般而言,落后上游版本超过2年是非常危险的。这种时刻,遇到具体急需解决的问题,当事团队往往无力彻底更新,只能选择短视的方案,而这种做法更加重了技术上安全、性能、功能等方面的问题,使得今后更加只能选择短视的方案。如此反复,直到有一天发现没有可行的短视方案了。
从宏观上看,遗留代码产生的技术债,你不去处理它,随着时间的发展,这些“债务”会越积越多。债务越多,利息(短视方案)越多。当“债务”总和超过系统所能容纳的上限,或者团队连技术债务的“利息”都要付不起(连短视方案都没了)的时候,出现问题就不是一个两个,而是系统性爆发出一系列直接影响商业成果和进展的问题了。
你接手这个烫手的“山芋”时,可以选择视而不见,但是这样做并没有解决问题,只是让这个问题变成了庞氏击鼓所传的花。这种心照不宣的击鼓传花造成的后果不仅仅是一段代码的腐化,更会因为其脱离实际(因为实际不可观测)而导致浮于表面的扯皮,最终促使团队文化劣币逐良币。任何团队的管理者、企业投资人和员工都不希望看到这种事情发生。
代码是由团队编写和维护的。当一个团队对代码的掌控力达到100%的时候,你知道它不会有问题,或者当出现问题的时候马上就能得到修复。但是,当团队对代码的掌控力从100%慢慢下降到只有1%或2%的时候,那么出问题就只是时间的问题了。等到出现问题的时候再谈追加投入、谈工程效能,已经于事无补。
值得一提的是,当所有人都意识到事态无药可救时,绝大多数情况下人的反应并不是韩信背水的“跟他拼了”,而是以自己身利益为第一考量的相互踩踏。这种演化逻辑往往会急剧加重客观问题的性质和程度,使事态向更糟糕的方向演化。
千里之堤毁于蚁穴。为什么Google的那些老软件专家们对于写代码要求那么严格?因为他们并没有陶醉于诸如“多样性”和“规模”等看似优越且显而易见的指标,而是时刻关注着软件工程这一复杂系统的达摩克里斯之剑——熵。为了维持某软件工程体系的自我发展,处于关键位置的人必须做出很多“有悖于小团体发展”、“有悖人性”的技术和管理决策,并且不带任何个人情感、在商言商地严格执行。
比如“2空格缩进”、“C/C++只允许单继承”、“Python不允许使用meta”等规则并不是很多人以为的“职场霸凌”和“陈年老规矩”,而是基于现实的经验总结出来的能让这个体系维持和发展下去的最低程度必要手段。
处理历史遗留问题不是停滞,而是为了更好地发展。事物的螺旋上升周期中,初期的大快猛上打下了一片江山,中期运营逐渐扩大优势,同时也逐渐出现问题。这时候能自反、自省、自我调整好,清理一些历史遗留问题。大而不骄,战战兢兢,深渊薄冰。这样的组织就具备了下一个周期的入围资格,能更好、更长久的发展。

Q3:像这种成熟的、有现金流的业务,在做重构时,既要保证业务平稳运营,又要解决历史遗留问题,两者如何兼顾?
zhongmingqu:
这种情况对于处于成熟期或者更晚期的产品团队很常见,中国美国都类似。以Google为代表的公司做得好的一点是有具有强话语权的TI(technical infrastructure)部门,能软硬兼施推动一些具备外部性、公共属性但是缺少即时内部价值的工作。
国内很多公司也想做这种事,但是受限于一些早期为了迎合和抢占市场做出的一些取舍,推行不易。用力过轻则容易蜻蜓点水没有实际效果,用力过猛则容易出现代价进一步向弱势群体(一线研发和基层管理者)转嫁的情况从而进一步激化矛盾。寻找一个中庸的、帕累托式(没有任何存量效益衰退)的改进的平衡点难度颇高。
二者兼顾地解决这类问题是几乎每一个大规模的公司都面临的问题。其中“事情”本身的问题远远小于“人”的问题,“人”的问题远远小于组织文化的问题。更多的时候我们看到的是“人”不愿意做出具有较高短期成本风险但是长期收益更高的选择,以及诸如思想不统一、策略失当、执行不力等因素,以及在这背后驱使众人做出这种行为和决策的组织和文化机制因素。很多提升效能的努力都由于种种原因“浅尝辄止”。今后这方面的尝试需要建立在针对之前的相关努力中暴露的问题的反思之上。

Q4:手Q iOS端将数百万行代码迁移到bazel,取得了一些经验和成果。您在大仓建设上负责哪一块,未来还会做哪些尝试?
zhongmingqu:
2022年上半年我主要在PCG大仓(tencent2)中从事依赖治理(third_party)、trpc-bazel方案、工具链(remote_cc_toolchain)、开发环境标准化(dev-image)等工作,未来根据公司战略和技术方向会做代码治理、工具建设、文化宣传等方面的工作。


代码重构的最大的难点不在技术,而是在于分配机制和意识认知

Q5:帖子中的题主遇到的问题很复杂,但工作中不可避免会维护遗留代码,对其进行重构。提及重构,一线同学就会哀声载道,遇到需要重构的情况时,你会怎样去解决?
zhongmingqu:
重构代码对一线同学来说是一件绝对的脏活、累活。没有可以大幅提升效率的方法,难以沉淀有效的、体系化的、可复用的技术作为抓手。对业务来说没有明显的瞬间增量,对代码重构的当事人精力和时间消耗巨大。大多数需要重构的项目没有合理的测试保护,重构的结果无法有效快速验证。这些问题都是困扰重构的第一序列现实因素。
重构代码更大的难点不在于技术,而是在于分配机制和意识认知。一方面,在分配机制上,国内公司推崇的“业务为王”、“业务先行”文化使得管理者并不关注段代码的好坏,而只是希望手下能快速“出活”,响应业务的需求,并不会以硬通货奖励重构、提效这种为后来人铺路的行为。
另一方面,在意识认知上,当一线同学去解决遗留代码的问题时,如果开发者自身没有超越的自我追求、职业素养和技术水平,大多数重构容易沦为换瓶子或者狭隘的技术、人事斗争的由头,从而无法带来希望中的组织层面的效能效率提升。
以上两方面问题得到解决后,抛砖引玉一下,可以先暴露问题:把这段代码拿出来构建(如bazel build、cmake、make等)一遍,然后看看改一行代码之后重新构建一遍需要多长时间;看这段代码有哪些依赖的库,把这些先捋出来,看看依赖是否合理、有没有循环等。这样就能暴露绝大多数的问题。把暴露出来的问题逐一分析,根据具体情况解决。如果碰到这些问题重复出现,可以通过尝试写个工具/脚本去解决。

Q6:重构代码有很多好处,例如重构可以提高可读性,改善源代码的可维护性和整体架构和功能,使代码更易于扩展和添加新功能。重构后,我们如何度量重构的效果?重构的指标有哪些?
zhongmingqu:
这类问题上,不同流派的专家意见可能还不太一样,无论是哪一种经验,它各有利有弊,需要结合具体情况取舍。很多指标,如“接口设计的好坏”等,非常重要,但是无法以简单的数字衡量。有鉴于此,我们提倡“数字驱动决策”,但是也要警惕“数字决定论”的思维误区。
基于个人经验,我推荐考虑以下几个指标来衡量代码库重构是否成功,包括且不限于:
  • 环路依赖大小及数量(均越少越好)
  • 相同功能的库的重复度(日志功能不应该有10个不同的库)
  • 构建速度(顶层应用改动一行代码后增量构建速度)
  • 构建一致性(不同人、机器、时间构建,应该能得到sha256相同的产物)
  • 更新依赖是否促进理解依赖(不应该出现引入一个重量级的库只为了一个Pair类的情况)
如果要选择唯一的可测量指标来评价代码构建是否成功,我认为“改一行代码后的增量构建速度”最关键的指标,因为这个指标决定了尝试新的想法的速度,也决定了迭代速度。

Q7:你在着手对代码进行重构前,需要做哪些准备?需要重点考虑哪些问题?
zhongmingqu:
项目的基础设施成本(测试、API设计、构建系统、依赖管理等)应和项目预期寿命相关。寿命短的项目可以酌情少投入甚至不投入,寿命长的项目应当前期投入一些保证项目长期可持续运作。项目寿命变化的情况,应当将项目寿命变化导致的基础设施成本的变化计算在项目成本中。
准备:
  1. 了解现有的构建情况(所有依赖是否每次都确定)
  2. 分析现有依赖关系(循环依赖、上帝依赖等)
  3. 测试成本(有无测试、跑测试一次需要多久)
比如,没有测试的项目是无法重构的,因为无法以合理的成本验证重构之后的行为是否符合预期。
比如,依赖关系一团乱麻的项目原则上也不应该“重构”,而是应该通过增加一层抽象“封装”其内部接口对外暴露简单、可预期的接口。
比如,基于cmake、makefile、gradle的构建系统转换为bazel难度因项目、编程语言而异,转换难度过大的情况需要谨慎一些。
诸如此类基于场景的判断还有很多,很难说有什么可以一言以蔽之的一定之规或者什么包治百病的银弹。关于“做不做”、“怎么做”、“怎样算做完”这些问题,都需要基于事实和经验的判断。

Q8:你在重构代码时,历史遗留代码有的共性问题是什么?(请举例top5说明)后续在开发-构建过程中需要做注意哪些?
zhongmingqu:
从我自身经验看,目前在历史遗留代码治理中看到的共性问题,包括:
  • 构建:缺乏一致性,速度慢影响迭代速度
  • 缺乏测试和测试困难,难以有效验证重构的改动
  • 依赖不清晰、不确定
  • 循环依赖、上帝依赖、菱形依赖等

Q9:在代码重构中,有哪些“编程利器”或工具可以向大家推荐?
zhongmingqu:
工具之于码农而言,就像计算器之于数学家。好的工具可以让人如虎添翼,但是只有那个人自己才能决定自己是猫还是虎。
工具不分高低贵贱:我见过裸emacs写java的,也见过没有自动补全不会写代码的,他们的工作都令我感到尊重。
我个人经历仅供参考:我是读书期间vim + screen(ctrl-a冲突需要按两次很难受)学起来的,后来转向VSCode + neovim + tmux,浏览器用vimium插件,开发基本不用鼠标挺爽的。

与人为善、学会提问、关注变化是成为优秀工程师的重要捷径

Q10:作为一名经验丰富的Staff/Principal-Level Engineer,您有哪些经验或心得供给大家借鉴?
zhongmingqu:
刨根问底:在任何时候都保持好奇心,凡事刨根问底。比如,说任何一句话,用到的所有名词都是已经定义清晰的,及物动词表达的关系恰当,系动词描述的属性正当,形容词和副词一律不许出现而应该替换为具体数字或者更具体的描述。这种方法训练出来的思维逻辑能力非常适合从事高度理性、高压力的事务。
记得当年正是这种习惯使我瞬间理解了“世界是由语言构建的”这一哲学观点。再比如,当我学习完如何从集合论构建“数”之后的某一天,忽的尤里卡了“事物的内涵可以定义为其可观测到的外延的集合”这一可以区别不同哲学体系的公理性假设。日光下无新鲜事,挖得足够深,道理都相通。
独立思考:有个故事,说的是一个孩子问自己的父母“太阳为什么从东边升起”。父母说,书里写的。孩子接着问,“书是谁写的”。父母说,“科学家写的”。孩子问,“科学家是谁认证的”。父母说,“老一辈科学家”。孩子说,“那么最初的科学家呢”。父母思考了一会儿,没再说话,而是给了这孩子一巴掌。故事比较极端,但意思很明确:很多创新都起始于对于我们已有认知的独立思考。
开放心态:我从来不喜欢别人以微操的眼光盯着我工作,但一向欢迎同事看我写代码大家一起讨论一起进步。我从不介意有人从我身边走过时看我在干什么,相反我倒是希望有人能就我在处理的问题主动与我聊两句。我做的工作也从来不藏着掖着,都拿出来与人分享,不担心有人“偷”有人“惦记”。能“偷”得走说明我做的工作原创性还不够强,别人会“惦记”说明我的行为让别人觉得我好欺负。坦荡的心态最大的作用是帮助当事人排除外在的干扰,使其可以专注于自身所从事的事情。

Q11:你认为的优秀工程师的品质、技能、素养应该有哪些?
zhongmingqu:
行业关注:就像老Google面试喜欢问“浏览器中输入完地址敲回车后会发生什么”(说出libresolv不?),以前我面试的时候喜欢问一些“奇怪”的问题,例如“乔布斯死于哪一年,死于哪一种癌症”(2011年,胰腺癌)、“硅谷公司的计量单位是谁”(前几年AMD的市值只有Intel的几分之一)、“哪一家公司首先推出了民用级64位处理器”(AMD64),类似这样的问题。
如果是对这个行业真的感兴趣的,应该去了解一下。很多人说IT变化快,但我只觉有点类似“物理学的每次进步都是以更加高级的方法计算简谐振子”,其底层的规律几十年来没有变化过。
学会提问:“为什么C语言的a = 0编译成 xor %eax, %eax而不是mov %eax, 0”。这个问题并不难回答,标准的美国本科CS必修课的作业题。难在没上过美国CS本科的人主动去研究C语言编译的结果以及看到后提出这个问题。
这种结合具体事务、有针对性地提出问题的能力有利于人可以在纷繁复杂的信息中高频率、高效地发现有价值的方向。人和人之间绝对的智力差距很小,对于绝大多数人而言,只要方向正确,在某一领域建立专家知识和信心只是时间问题。
与人合作:一个人再厉害一天也就24小时,与人合作才能做出更大的事情。人很复杂,不是每个人都值得我们花心思合作。当你处于优势地位时,保持对所有人开放,但是每个人只给一次机会。
当你处于弱势地位时,不要怜惜自己的脸面和尊严,要死皮赖脸把握机会。卓越的人都是将自身长板发挥到最长的,找到合适的人比学会适应一个不合适的人更有效,并且不必纠结于不愉快的合作经历。
与人为善:严格要求自己,宽容体恤他人。借用一位我非常尊重的人的话说,他人的尊重不仅是实干做出来的,更是“高风亮节让出来的”。强者不光要让自己强,还要带动周围的人也强大起来。大家好,才是真的好。


# 腾讯技术直播 #
腾讯工程师分享技术干货

点击预约,get开播提醒

往期回顾:
技术文档写作的道与术 
如何在业务开发中实现自我成长
程序员面对新业务,如何快速融入、规划自身发展?

点个关注,我们下期再见👋

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存