致敬 客户端重构场景分析 重构 (致敬客户图片2024)

致敬 客户端重构场景分析 重构 (致敬客户图片2024)

《重构:改善既有代码的设计》是计算机领域的一本经典之作,本书清晰揭示了重构的过程,解释了重构的原理和最佳实践方式,并给出了何时以及何地应该开始挖掘代码以求改善。

距离该书首次出版已经过去了二十余年,这二十多年里行业发生了很多变化,但有一件事的必要性并没有随着技术的进步,工具的升级,方法论的扩展而被消灭:

这就是重构本身。

因为在大部分工程师的职业经历中,能够从头开始搭建一个项目,并且在时间充裕、需求明确的条件下,用正常节奏设计开发完整系统的过程只是一个美好的理想——更多时候是在接手的现有系统/模块中如何平衡好代码质量和需求迭代。

本文的重点不是为了说明重构的细节,而是从工程角度,在客户端场景下给提出一些关于重构的建议;如果大家关心重构方法论本身,直接去阅读《重构》就是最好的解答。

首先,为什么要强调客户端?客户端和其他场景有什么区别?

我们简单和后台研发模式做个对比:

但客户端有个特征:

而即使做了容器化,很多状态的共享是不可回避的,比如:

这一点和基于 SOA 思路设计的后台服务中,一般故障都局限在某个服务内部的特点有非常大的不同, 所以:

客户端一个模块被写烂了,相比一个基于 MQ/RPC 相互连接的后台模块或子系统,会对整个项目造成更大危害。

(这里只是就代码形式而言——不是说后台就简单了,后端复杂的场景往往在别的方面,会有类似架构设计是否支持平行扩展,故障如何隔离,服务怎么降级等其他问题)

那么什么情况下必须立即重构,什么情况下可以推迟重构?

很多时候,研发会因为各种原因,推迟重构,直到这件事情被一拖再拖,从重要而不紧急的事情变成了既重要也紧急的事情。

下面我会从之前的一些项目经验中给出一些建议,供大家参考。

重构的建议

要提前计划重构,不要赶时间

大部分重构都是代码债务的清偿的过程,赶时间容易积累新的债务。

而对于一个突发的重构需求,其实已经错过了解决问题的最佳时机。

小步稳跑,尽可能保证肉眼可见的正确性

大多数时候,我们很难找到整块时间重构,这往往意味着你可能会相对分散地修改到正常需求开发不会触碰的逻辑,这种情形对于 QA 同学来说是非常容易发生误判的——即使和对方同步过,但双方仍然可能对问题的影响面判断不一致。

什么叫 肉眼可见的正确性? 概念很简单,就是一些相对可靠的推论而已,比如:我改了一个变量类型,只要我足够认真,同时确认过编译器所有警告——那么一个有经验的程序员是可以保证这个修改风险可控的。

不同程度的工程师,能看出不同深度的问题,这一点需要执行重构的人,对自己的段位有客观的评价,不要贪图效率。

多做系统性方案,考虑越周到越好

之所以会重构,有很大一部分原因是因为补丁方案已经快要 hold 不住了,这个时候再次使用小规模,单点方案,大概率会退化成另一种补丁。

时间紧的情况下,想 100%,每次落地 x%, 保持方向正确,一步到位不了,两步,三步……

坚持做,代码会累积的你的努力。

不过,是否这个假设过于乐观了,有累积不了的情况吗?有的,看下面。

单兵收敛重点

有些复杂的事情必须要有单点管控,人越多越乱:

如果发现单兵速度跟不上团队其他人驱动的工程演进速度,这一点需要认真想办法解决,否则就是浪费资源,原因例如:

可能的场景:

这可能是重构里面最棘手情况了,解法需要看实际情况来决定,没有定式,我举个例子说明:

场景分析:组件化

关键点:

问题: 模块移动 + 模块本身修改 + 改名 如果导致冲突,此冲突 不可解

分析: 此时应该考虑的点是如何锚定修改的 可累积性:

只看步骤,很容易知道,这个过程是肉眼可确认正确性的,那么在把握好提交节奏+做好沟通的前提下,不容易产生重大风险。

可以选择纯手工做,但是对于人肉来说,往往非常枯燥,而人天生讨厌枯燥的事情,后面我们会专门讲讲利用工具的思路。

把握节奏

重构往往影响比较大,需要综合考虑和 QA 同学的协作。

我们先从时间维度看看:

上面是重构提交时机的风险分析,中间的圆表示重构合入后 bug 泄漏到用户侧的风险。实际项目如果重构没有让 QA 知道,属于违规操作。

这里的基本论点是:

另外,在不同情况下重构落地的步骤也不尽相同,下面简单分析一下。

稳定的老模块

这种情况下,重构会给 QA 带来额外的回归成本,需要提前确认好资源:

这里说的比较抽象,举个例子:

改版类需求

一般的改版都意味着好的重构窗口已经出现。

如果该模块有重构的必要,需要做的就是在比较早期的时候,将这部分成本计算在内。

重构的工具

重构的时候大概率会遇到巨大而繁冗的工作量,这也是阻止很多重构被落地的重要原因,下面从个人经验出发,介绍一下可能的工具。

首先,我们需要明确一点,工具的使用必须是在能保证正确性的前提下,使用场景可能有:

这里最想讲的就是正则,基本所有工具都支持,无论是替换文本还是用来发现问题都是非常好用的,比如:

上面这个例子,用来将所有 XX 开头的模块替换为 YY 开头的模块,$1 对应正则的第一个分组,replace 的输入框里都是纯文本,不用想的太复杂。

值得说明的是,这里的 Matching Case 可以用来控制正则是否大小写敏感,做大规模替换的时候,一定注意。

类似的工具还有 sed 比如:

sed-r-i's/XX(.*?)/YY\1/'*
复制代码

差别只在于\1 表示第一个分组。

再如:

\s\S 这两个可以用来匹配换行符,这个例子里用来发现哪些 onFinally 回调中使用了 let,当然这里可能会因为括号嵌套导致有误判,但是对于一般的定位需求,是基本可以满足的。

再提供一个文件批量重命名的 case:

find.-name".swift"-print0|rename-0's/XX([a-zA-Z]?).swift/YY$1.swift/'
复制代码

rename 是一个 perl 工具,mac 上可以通过很多方式安装;上面命令会把所有 XX 开头的文件改成 YY 开头的文件——这对于做了组件化的工程基本无影响,如果不是那可能还要修复一下工程文件引用

剩下就是一些相对复杂的逻辑了,当量级小的时候,可以简单点手动处理掉。

但是当量级非常大的情况下,使用代码重构代码,就变成了一种可能值得实施的做法。

这里不讲具体 case,主要看希望做什么,下面举一个 python 作为重构脚本的例子。

这里的大体思想是根据实际需求,建立必要的 SourceModel,根据实际对 SourceModel 进行 Transform,比较适合操作大量相对复杂的重构;

但是,有一点,大规模推进的时候,一定要:

很多时候代码逻辑是复杂的,就和一开始很多人写宏的时候会写出来:

#defineMUL(x,y)x*y
复制代码

这种考虑不周的形式一样——MUL(1 + 2, 3)就会得出非预期的结果。

脚本操作很容易遇到代码情况和预期不符,最终做了一些 有问题 的大规模修改。

这个时候如果正确修改和错误修改混在一起,除了推倒重来,基本无解;推荐的做法是:

代码重构代码的方式很多,之前也有些情况使用 Grunt 来做。Grunt 的用途是打包,对文件处理有非常强的支持。

最后,可以考虑准备一套基础设施直接放在项目里,常常会发现实际需要的时候会比一开始认为的多很多。

团队杠杆

在实际工作中,劣币逐良币是非常常见的:一旦烂代码成为普遍现象,写好的模块的成本就会变得非常高——这好比在浮沙筑高台。

这里的核心思路是:

为了抓住重点,我们只分析最棘手的情况——整体架构出问题了。

首先,如何判别这种情况已经出现?最明确的信号——两个以上核心业务模块出现了牵一发动全身的情况。这种时候就需要将重构的优先级提升了,否则情况容易随着迭代每况愈下。

这里涉及到两个关键问题:

说实话,我个人觉得第一个问题的解决难度远远低于第二个问题。

从治理的角度来说,一个足够有经验的研发,如果被给予充裕的处理时间,往往是能够比较好的完成任务的(特别是如果这个模块只是因为项目节奏太快导致没有时间完成架构演进的情况下)

但是这里仍然有几个关键因素可能会影响最终结果,按重要性:

(这也是有人将工程设计问题称为“险恶问题”(Wicked Problem)的一个原因,你不解决它,你不知道你一定能解决它)

上面这些问题需要执行重构的人和使用方达成共识,才不会产生过于理想主义的问题;不过,这里没有标准答案,下面这条建议是很多即便很优秀的研发也容易弄反的东西。

如果模块内部设计优雅和模块外部使用简洁发生冲突的时候,优先保证模块外部使用简洁——对复杂度的封装既是模块本身存在的意义,也是代码治理的目的。

下面我们来讲讲如何确保重构的结果,这里的答案很简单,但是不同团队做起来难度不同——共识。

如果 100 个工程师对优秀有 100 个标准,那么哪怕他们都是出色的工程师,一起合作出来的东西仍然可能是一团乱麻。

不过,这里讨论“优秀”有些主观,coding 是一个非常复杂,非常广阔的领域——每个人的认知中哪怕是对同一个功能的,可以称得上“优秀”的代码写法的认知也是非常不同的。

所以,我们需要换一个角度看待问题,除了“优秀”之外,很多维度都可以来度量代码的设计,其中最重要的参考是“复杂度”的控制,借用《代码大全》的一个观点——所有软件工程手段的最终目的都是为了控制复杂度。

那么回到主题,重构成果的本质是复杂度的降低,或者对于复杂度可控性的提升;在这个基础上,根据团队状况会有所不同:

对于第一种团队,可能好的实践是,用大家都能理解的方式,确保团队里短板的部分也能基本 hold 住复杂度;

对于第二种团队,或许业界最前沿而有效的做法,就是大家认为好的方式——因为很多人会时刻跟踪业界变化,对这些熟稔于心。

最后一个建议是随手除草:

共性是这些东西都是一些可改可不改的东西,但是改起来成本又比较可控 。

代码每天被阅读的次数往往超过你的认知,这种做法具有对团队的引导效果——投入几个热键修改代码的成本,产出大家渐渐共识的过程。

此时【果断要改】,这是值得一个长期做的事情。

久而久之,代码质量会慢慢提升,尤其对于一开始赶工比较严重的项目,往往有不错的效果。

通常这种场景做一些流程也很有用,比如 swift/oclint,sonar 等都可以考虑——只要有一个负责人和大家同步好哪些规则是大家可以接受的,以及它的好处是什么。

总的来说,要确保重构结果不滑坡,最需要的是团队需要共识方案的合理性——除了方案本身必须是自洽的之外,和团队同步设计考量并且形成共识是非常必须的。

所以,团队内部多分享,多沟通,多讨论是非常重要的过程,并且如果可能要 留下文档。

设计文档的重要性可能是:

或许有些模块的实现形式会变迁,但是一个设计方向正确的文档,会持续帮助人们达成设计共识。

除了文档之外,在字节还可以建立一个 lark 讨论组,将一些架构问题抛在里面:

重构本身需要充分的讨论,为了这个事专门开会很多项目的节奏不允许,如果发在 lark 群里,容易被刷走,导致问题没有充分讨论。

重构的方向

并不鼓励为了重构而重构,下面列出的点是我们考虑重构方案的时候,可以努力去兼顾的一些目标:

重构的目的是为了让架构变得更加合理,但是架构变好和气候变好一样是一个缓慢而又不容易量化的过程,在效果的观测上,可以根据一些现象来判断是否在正确的轨道上。

隐含收益

从人的角度来说,重构本身有一项非常重要,但是往往大家认知有限的收益:

因为没有任何一个过程能够比一个人通过设计,编码,调试来了解之前业务/技术细节的来的更有效率。

很多优秀的设计在基础代码质量存在缺陷的情况下,往往带来的是负收益。这是因为所有设计都是建立在合理解耦的基础上。

在解耦关系不佳的情况下,很多设计就必须迁就现有代码实现,从而模糊了设计思想,反而让其他人更加看不明白。

人对代码的了解程度的提升,加上代码结构的改善,会带来更低的 bug 量。

总结

最后分享一个博弈论问题:

答案很有意思:大量增加情报数量和复杂度,直到远远超出 A 的个人能力,这个时候 B 就有可能获得 50%的胜算。

这个可能也是为什么很多非常优秀的团队仍然会陷在 bug 泥潭里的原因,系统的熵太大。

正如热力学中所说的熵增一样,宇宙万物永远都在经历着从有序到混乱的过程,代码也不例外,但

优秀工程师始终会通过各种手段来控制复杂度,从而赋予代码某种生命的特征,也许能从侧面印证一个说法—— 好的代码是活的

原文链接:致敬《重构》:客户端重构场景分析

声明:本文来自用户分享和网络收集,仅供学习与参考,测试请备份。