腾讯 Code Review 规范

发布时间 2023-04-29 07:31:14作者: Jimmyhus

推荐下自己做的 Spring Boot 的实战项目:https://github.com/YunaiV/ruoyi-vue-pro
推荐下自己做的 Spring Cloud 的实战项目:https://github.com/YunaiV/onemall

为什么技术人员包括 leader 都要做 code review

谚语曰: 'Talk Is Cheap, Show Me The Code'。知易行难,知行合一难。嘴里要讲出来总是轻松,把别人讲过的话记住,组织一下语言,再讲出来,很容易。绝知此事要躬行。设计理念你可能道听途说了一些,以为自己掌握了,但是你会做么?有能力去思考、改进自己当前的实践方式和实践中的代码细节么?不客气地说,很多人仅仅是知道并且认同了某个设计理念,进而产生了一种虚假的安心感---自己的技术并不差。但是,他根本没有去实践这些设计理念,甚至根本实践不了这些设计理念,从结果来说,他懂不懂这些道理/理念,有什么差别?变成了自欺欺人。
代码,是设计理念落地的地方,是技术的呈现和根本。同学们可以在 review 过程中做到落地沟通,不再是空对空的讨论,可以在实际问题中产生思考的碰撞,互相学习,大家都掌握团队里积累出来最好的实践方式!当然,如果 leader 没时间写代码,仅仅是 review 代码,指出其他同学某些实践方式不好,要给出好的实践的意见,即使没亲手写代码,也是对最佳实践要有很多思考。

为什么同学们要在 review 中思考和总结最佳实践

我这里先给一个我自己的总结:所谓架构师,就是掌握大量设计理念和原则、落地到各种语言及附带工具链(生态)下的实践方法、垂直行业模型理解,定制系统模型设计和工程实践规范细则。进而控制 30+万行代码项目的开发便利性、可维护性、可测试性、运营质量。
厉害的技术人,主要可以分为下面几个方向:

奇技淫巧
掌握很多技巧,以及发现技巧一系列思路,比如很多编程大赛,比的就是这个。但是,这个对工程,用处好像并不是很大。

领域奠基
比如约翰*卡马克,他创造出了现代计算机图形高效渲染的方法论。不论如果没有他,后面会不会有人发明,他就是第一个发明了。1999 年,卡马克登上了美国时代杂志评选出来的科技领域 50 大影响力人物榜单,并且名列第 10 位。但是,类似的殿堂级位置,没有几个,不够大家分,没我们的事儿。

理论研究
八十年代李开复博士坚持采用隐含马尔可夫模型的框架,成功地开发了世界上第一个大词汇量连续语音识别系统 Sphinx。我辈工程师,好像擅长这个的很少。

产品成功
小龙哥是标杆。

最佳实践
这个是大家都可以做到,按照上面架构师的定义。在这条路上走得好,就能为任何公司组建技术团队,组织建设高质量的系统。

从上面的讨论中,可以看出,我们普通工程师的进化之路,就是不断打磨最佳实践方法论、落地细节。

代码变坏的根源

在讨论什么代码是好代码之前,我们先讨论什么是不好的。计算机是人造的学科,我们自己制造了很多问题,进而去思考解法。

重复的代码

重复的代码是指在程序中出现了相同或类似的代码块,这些代码块可能分散在不同的地方。重复的代码会增加代码量、降低代码的可读性和可维护性,并且难以保持一致性,如果其中一个代码块有问题需要修复,则需要修改所有的代码块,增加了工作量。

要解决重复的代码,可以采用以下几个步骤:

  • 提取公共函数或方法:如果多个地方出现了相同的代码块,可以将这些代码块提取到一个公共的函数或方法中,并从调用该函数或方法的地方传入不同的参数来实现不同的功能。

  • 使用继承或接口:如果多个类中存在相同的代码块,可以使用继承或接口来避免代码的重复。将相同的代码块放到父类或接口中,子类只需要实现自己特有的方法即可。

  • 模板方法:模板方法是一种设计模式,可以用于避免代码的重复。通过定义一个抽象基类,其中包含通用的操作步骤,而将具体的操作步骤由子类来实现,从而避免了代码的重复。

  • 代码检查工具:使用代码检查工具可以帮助发现重复的代码,同时也可自动去除重复的代码,减少开发人员的工作量。

总之,重复的代码会增加代码的复杂性、降低代码的可读性和可维护性,并且难以保持一致性。可以通过提取公共函数或方法、使用继承或接口、模板方法以及使用代码检查工具等方法来避免代码的重复,从而提高代码的质量和可维护性。

早期有效的决策不再有效

现在看,这个代码挺好的,长度没超过 80 行,逻辑比价清晰。但是当 isMerge 这里判断逻辑,如果加入更多的逻辑,把局部行数撑到 50 行以上,这个函数,味道就坏了。出现两个问题:

1)函数内代码不在一个逻辑层次上,阅读代码,本来在阅读着顶层逻辑,突然就掉入了长达 50 行的 isMerge 的逻辑处理细节,还没看完,读者已经忘了前面的代码讲了什么,需要来回看,挑战自己大脑的 cache 尺寸。

2)代码有问题后,再新加代码的同学,是改还是不改前人写好的代码呢?出 bug 谁来背?这是一个灵魂拷问。

代码早期有效的决策不再有效可能是因为在软件开发过程中,需求或技术发生了变化,导致原来的设计或架构不能再满足当前的需求或要求。为了解决这个问题,可以采取以下几个步骤:

  • 重新审视需求:首先需要重新审视需求,了解新的需求和变化,明确系统的目标和功能,从而重新定义系统的设计和架构。

  • 重构代码:如果早期有效的决策不再有效,可能需要对代码进行重构。重构代码可以优化代码结构,去除重复代码,减少耦合度,提高代码的可读性、可维护性和可扩展性。

  • 引入新的技术和框架:如果早期使用的技术或框架不能再满足当前的需求,则可以考虑引入新的技术或框架,以适应当前的情况。

  • 采用敏捷开发:敏捷开发注重迭代和自适应,能够更好地适应变化。采用敏捷开发,可以快速响应变化,及时调整方向。

  • 预留一定的灵活性:在进行软件开发时,预留一定的灵活性,避免出现硬编码和过于死板的设计,从而更好地适应变化。

总之,代码早期有效的决策不再有效可能是因为需求或技术发生了变化。可以通过重新审视需求、重构代码、引入新的技术和框架、采用敏捷开发以及预留一定的灵活性等方法来解决这个问题,并更好地适应变化。

过早的优化

过早的优化是指在程序开发的早期,就对代码进行一些优化,如减少代码运行时间、减少内存分配等。虽然优化可以提高代码的性能,但过早的优化可能会带来以下问题:

  • 浪费开发时间:在程序开发早期进行优化,需要额外的时间和精力,可能会浪费开发人员的时间。

  • 降低代码可读性:过早的优化可能会使得代码变得更加复杂,难以理解和维护,从而降低了代码的可读性。

  • 可能导致性能下降:过早的优化可能会影响程序的正常运行,甚至会导致性能下降,因为很多时候优化的效果并不直观,需要在实际运行中测试验证。

  • 可能引入bug:过早的优化可能会引入新的bug,从而影响程序的正确性和稳定性。

因此,在程序开发早期应该注重代码的可读性和可维护性,尽量避免过早的优化。只有在代码已经成熟稳定,而且在性能方面存在瓶颈的情况下,才应该考虑进行优化。在进行优化时,可以采用以下几点:

  • 定位瓶颈:首先需要定位代码的性能瓶颈,了解程序中哪些部分执行时间较长。

  • 基准测试:通过基准测试来比较优化前和优化后的代码性能差异,以验证优化效果。

  • 选择合适的算法和数据结构:对于一些性能瓶颈,可以考虑采用更高效的算法和数据结构,从而提高代码的性能。

  • 避免过度优化:在进行优化时,要避免过度优化,尽量保持代码的简洁和易读性。

综上所述,过早的优化可能会带来一些问题,因此应该注重代码的可读性和可维护性。只有在代码已经成熟稳定,且在性能方面存在瓶颈的情况下,才应该考虑进行优化。

对合理性没有苛求

对代码合理性没有苛求可能会带来以下问题:

  • 代码质量不佳:如果对代码合理性没有苛求,可能会导致代码结构混乱、重复代码过多、耦合度高等问题,从而降低代码的可读性、可维护性和可扩展性。

  • 可靠性差:如果对代码合理性没有苛求,可能会出现潜在的逻辑错误或边界条件问题,从而影响系统的稳定性和可靠性。

  • 效率低下:如果对代码合理性没有苛求,可能会导致代码执行效率低下,从而降低系统的性能。

  • 维护成本高:如果对代码合理性没有苛求,可能会使得代码难以维护,从而增加开发人员的工作量和维护成本。

因此,虽然在开发过程中不能要求完美的代码,但是仍然需要对代码合理性有一定的追求。可以参考以下几点:

  • 设计良好的架构:先进行设计,理清系统的功能模块和流程,避免出现混乱、冗余的代码。

  • 代码规范性:制定一定的代码规范,统一代码的格式和命名规则,提高代码的可读性和可维护性。

  • 测试覆盖率:对代码进行全面的测试,包括单元测试、集成测试等,提高代码的可靠性和稳定性。

  • 重构优化:及时对代码进行重构优化,去除重复代码,减少耦合度,提高代码的可读性、可维护性和可扩展性。

综上所述,虽然不能要求完美的代码,但是仍然需要对代码合理性有一定的追求,以提高代码的质量和可维护性。

过度使用面向对象/封装

将数据和行为封装在一起,形成一个独立的单元,代码封装层次,越底层公用形成一个独立的单元,service层不要相互调用导致循环,一个类的依赖不要太多
封装是面向对象编程的一个核心概念,它可以将数据和行为封装在一起,形成一个独立的单元。封装使得对象的使用者无需知道其内部实现细节,从而提高了代码的可维护性、可扩展性和可重用性。但是,如果过度使用面向对象/封装,会带来以下几个问题:

  • 过度封装会增加代码的复杂性:如果在设计中过度封装,会导致类之间的耦合度变高,增加代码的复杂性,并且增加代码的理解难度。

  • 过度封装会降低代码的灵活性:如果对某些方法或属性进行过度封装,会使得修改或扩展这些方法或属性变得困难,从而限制了代码的灵活性。

  • 过度封装会影响代码的性能:每次访问对象的属性或方法都需要经过一定的开销,如果对所有属性和方法都进行封装,会导致代码的运行效率下降。

  • 过度封装会增加代码的维护成本:如果封装不恰当,会使得代码难以维护,从而增加代码的维护成本。

为了避免过度使用面向对象/封装带来的问题,需要遵循以下几点:

  • 封装适当:封装能够提高代码的可维护性、可扩展性和可重用性,但过度封装会带来上述问题。因此,在设计中应该根据实际情况适当地使用封装。

  • 设计良好的接口:一个好的接口可以使得代码易于理解、易于使用、易于修改和扩展。因此,在设计时应该考虑到接口的设计,尽量保持接口简单、清晰和易于使用。

  • 拆分粒度合理:在设计类和方法时,应该合理拆分粒度,将类和方法的职责划分清楚,从而减少耦合度,提高代码的可读性、可维护性和可扩展性。

  • 性能和效率平衡:在进行封装时,应该考虑到性能和效率问题,避免过度封装导致代码运行效率下降的问题。

综上所述,过度使用面向对象/封装会带来一些问题,但如果使用得当,可以提高代码的可维护性、可扩展性和可重用性。

根本没有设计

这个最可怕,所有需求,上手就是一顿撸,'设计是什么东西?我一个文件 5w 行,一个函数 5k 行,干不完需求?'从第一行代码开始,就是无设计的,随意地踩着满地的泥坑,对于旁人的眼光没有感觉,一个人独舞,产出的代码,完成了需求,毁灭了接手自己代码的人。这个就不举例了,每个同学应该都能在自己的项目类发现这种代码。
如果一个代码根本没有设计,往往表现为代码结构松散、耦合度高、缺乏可重用性等问题。这样的代码通常是由不断累加新功能和修改而形成的,缺少一定的系统性和规划性。

这种情况下,建议采用如下措施:

  • 重新审视需求:首先需要对需求进行重新审视,分析业务流程和功能模块,明确系统架构和模块之间的关系,建立系统设计的基础。

  • 进行重构:针对已有的代码,将其进行重构,改进代码结构,去除重复代码,减少耦合度,提高代码的可读性、可维护性和可扩展性。

  • 引入设计模式:设计模式是一些通用的解决方案,适用于不同的场景和问题。可以考虑引入一些常用的设计模式,如工厂模式、策略模式等,以提高系统的稳定性和可维护性。

  • 采用测试驱动开发(TDD):测试驱动开发能够使得开发人员更加关注系统的设计和实现细节,着重强调代码的可测试性,从而提高代码的质量和可维护性。

  • 建立良好的文档:建立良好的文档,包括需求文档、设计文档、接口文档等,能够帮助开发人员更好地理解系统架构和代码实现细节,提高代码的可维护性和可扩展性。

综上所述,如果一个代码根本没有设计,应该重新审视需求,进行重构,引入设计模式,采用测试驱动开发,建立良好的文档等措施,以提高代码的质量和可维护性。

model 设计

在软件开发中,Model是MVC(Model-View-Controller)模式中的一个组成部分,用于表示系统中的数据和业务逻辑。在设计model时,需要考虑以下几个方面:

  1. 独立性:Model应该是独立的,不依赖于其他模块或组件。这样可以提高模块的重用性和可维护性。

  2. 可扩展性:Model应该具有一定的可扩展性,能够容易地增加新的功能或属性。同时,要避免引入过多的复杂性,导致代码难以维护。

  3. 易于测试:Model应该易于测试,可以通过单元测试来验证其正确性和稳定性。

  4. 安全性:Model应该考虑安全性问题,避免出现安全漏洞或数据泄露等问题。

在具体的实现中,可以采用以下几个步骤:

确定数据模型:首先需要确定数据模型,包括模型的属性、关系和行为等。要考虑到模型的各种需求,并结合实际情况来确定模型的属性和方法。

  • 设计接口:为Model设计接口,使得Model可以与其他组件进行交互。接口应该简单清晰,易于理解和使用。

  • 考虑持久化:在Model设计时,需要考虑数据的持久化问题。可以采用ORM(对象关系映射)等技术来简化数据持久化操作。

  • 保持一致性:在设计Model时,要保持一致性。比如,命名、属性、方法的风格和格式应该统一,从而提高代码的可读性和可维护性。

总之,在设计Model时,需要考虑独立性、可扩展性、易于测试和安全性等问题,并采用合适的接口和持久化技术,保持一致性,从而提高代码的质量和可维护性。

Keep It Simple Stuped!

KISS原则是“保持简单和愚蠢”,即在设计和实现软件时应该尽可能地保持简单和易于理解。要实现KISS原则,可以采取以下几个步骤:

  • 确定需求:首先需要明确系统的需求和目标,并确定必要的功能和特性。要避免过多的复杂性和不必要的功能。

  • 使用简单的设计:采用简单的设计模式和架构,避免过度设计或过度工程化。要保持设计的简洁性和可读性。

  • 保持代码清晰易懂:在编写代码时,要保持代码的清晰易懂,避免出现复杂的逻辑、冗长的代码和深嵌套的条件语句。可以使用合适的命名和注释来提高代码的可读性。

  • 避免重复代码:在编写代码时,要避免重复代码,减少重复的工作量,提高代码的复用性。

  • 将复杂性隐藏在接口后面:对于一些复杂的功能和算法,可以将其隐藏在接口后面,使得用户只需要使用简单的接口就可以完成相应的操作。

总之,实现KISS原则需要从需求、设计、编码等方面入手,通过选择简单的设计模式、保持代码的清晰易懂、避免重复代码等方法来实现。通过实践KISS原则,可以提高代码的可读性和可维护性,从而更好地适应变化和需求的变化。

原则 3 组合原则: 设计时考虑拼接组合

在代码设计时,拼接组合(composition)是一种常见的设计原则,它允许将多个对象组合成一个更复杂的对象。这种设计方式可以提高代码的可复用性、可维护性和可扩展性。以下是实现拼接组合的一些步骤:

  1. 划分类别:首先需要对系统中的类进行分类。可以根据功能、属性等方面进行分类,并将相同类型的类放在一起。

  2. 抽象共通特性:识别出各个类之间的共通特性,并将其抽象出来。这样可以为后续的组合提供基础。

  3. 设计接口:为每个类设计适当的接口,并确保各个类之间的接口是一致的。这样就可以方便地将多个对象组合成一个更大的对象。

  4. 组装对象:使用组合模式来将多个对象组合成一个更大的对象。组合模式通过递归方式将对象与子对象组合起来,形成一棵树形结构。

  5. 消除耦合:消除各个对象之间的耦合,使得对象之间的关系更加简单清晰。

通过拼接组合的方式,可以实现代码的重用和扩展,减少代码的冗余和耦合度,从而提高代码的可读性和可维护性。拼接组合是一种常见的设计方式,在软件开发中具有重要的应用价值。

在软件开发中,耦合度是指系统内部各个组件之间相互依赖的程度。如果一个代码具有高耦合性,则说明其中的模块之间关联性较强,修改其中一个模块可能会影响到其他模块,从而导致代码的可维护性和可扩展性降低。以下是一些常见的高耦合代码的表现:

  • 强依赖关系:如果一个模块依赖于其他模块,并且对其进行了直接调用,这就说明两个模块之间关联性较强,称为强依赖关系。

  • 难以重用:如果一个模块难以被其他模块所重用,这就说明该模块与其他模块之间的耦合度较高。

  • 多重角色:如果一个模块同时扮演多种角色,例如拥有过多的属性或方法,在不同的功能之间相互交织,那么这个模块就会显得比较复杂,容易出现问题。

  • 参数过多:如果一个函数或方法需要传递过多的参数,这就说明其与其他模块之间的耦合度较高,而且这样的代码也很难阅读和理解。

  • 全局状态:如果一个模块使用了全局变量或状态,这就说明它与其他模块之间的耦合度较高,因为全局状态可能会被其他模块改变,从而对该模块产生影响。

总之,高耦合代码具有强依赖关系、难以重用、多重角色、参数过多和全局状态等表现。要避免高耦合代码的出现,需要尽可能地降低各个模块之间的依赖关系,提高代码的可维护性和可扩展性。

关于 OOP,关于继承,我前面已经说过了。那我们怎么组织自己的模块?对,用组合的方式来达到。linux 操作系统离我们这么近,它是怎么架构起来的?往小里说,我们一个串联一个业务请求的数据集合,如果使用 BaseSession,XXXSession inherit BaseSession 的设计,其实,这个继承树,很难适应层出不穷的变化。但是如果使用组合,就可以拆解出 UserSignature 等等各种可能需要的部件,在需要的时候组合使用,不断添加新的部件而没有对老的继承树的记忆这个心智负担。

使用组合,其实就是要让你明确清楚自己现在所拥有的是哪个部件。如果部件过于多,其实完成组合最终成品这个步骤,就会有较高的心智负担,每个部件展开来,琳琅满目,眼花缭乱。比如 QT 这个通用 UI 框架,看它的Class 列表,有 1000 多个。如果不用继承树把它组织起来,平铺展开,组合出一个页面,将会变得心智负担高到无法承受。OOP 在'需要无数元素同时展现出来'这种复杂度极高的场景,有效的控制了复杂度 。'那么,古尔丹,代价是什么呢?'代价就是,一开始做出这个自上而下的设计,牵一发而动全身,每次调整都变得异常困难。

实际项目中,各种职业级别不同的同学一起协作修改一个 server 的代码,就会出现,职级低的同学改哪里都改不对,根本没能力进行修改,高级别的同学能修改对,也不愿意大规模修改,整个项目变得愈发不合理。对整个继承树没有完全认识的同学都没有资格进行任何一个对继承树有调整的修改,协作变得寸步难行。代码的修改,都变成了依赖一个高级架构师高强度监控继承体系的变化,低级别同学们束手束脚的结果。组合,就很好的解决了这个问题,把问题不断细分,每个同学都可以很好地攻克自己需要攻克的点,实现一个 package。产品逻辑代码,只需要去组合各个 package,就能达到效果。

这是 golang 标准库里 http request 的定义,它就是 Http 请求所有特性集合出来的结果。其中通用/异变/多种实现的部分,通过 duck interface 抽象,比如 Body io.ReadCloser。你想知道哪些细节,就从组合成 request 的部件入手,要修改,只需要修改对应部件。[这段代码后,对比.NET 的 HTTP 基于 OOP 的抽象]

说到组合,还有一个关系很紧密的词,叫插件化。大家都用 vscode 用得很开心,它比 visual studio 成功在哪里?如果 vscode 通过添加一堆插件达到 visual studio 具备的能力,那么它将变成另一个和 visual studio 差不多的东西,叫做 vs studio 吧。大家应该发现问题了,我们很多时候其实并不需要 visual studio 的大多数功能,而且希望灵活定制化一些比较小众的能力,用一些小众的插件。甚至,我们希望选择不同实现的同类型插件。这就是组合的力量,各种不同的组合,它简单,却又满足了各种需求,灵活多变,要实现一个插件,不需要事先掌握一个庞大的体系。体现在代码上,也是一样的道理。至少后端开发领域,组合,比 OOP,'香'很多。

原则 6 吝啬原则: 除非确无它法, 不要编写庞大的程序

可能有些同学会觉得,把程序写得庞大一些才好拿得出手去评 T11、T12。leader 们一看评审方案就容易觉得:很大,很好,很全面。但是,我们真的需要写这么大的程序么?

我又要说了"那么,古尔丹,代价是什么呢?"。代价是代码越多,越难维护,难调整。C 语言之父 Ken Thompson 说"删除一行代码,给我带来的成就感要比添加一行要大"。我们对于代码,要吝啬。能把系统做小,就不要做大。腾讯不乏 200w+行的客户端,很大,很牛。但是,同学们自问,现在还调整得动架构么。手 Q 的同学们,看看自己代码,曾经叹息过么。能小做的事情就小做,寻求通用化,通过 duck interface(甚至多进程,用于隔离能力的多线程)把模块、能力隔离开,时刻想着删减代码量,才能保持代码的可维护性和面对未来的需求、架构,调整自身的活力。客户端代码,UI 渲染模块可以复杂吊炸天,非 UI 部分应该追求最简单,能力接口化,可替换、重组合能力强。

落地到大家的代码,review 时,就应该最关注核心 struct 定义,构建起一个完备的模型,核心 interface,明确抽象 model 对外部的依赖,明确抽象 model 对外提供的能力。其他代码,就是要用最简单、平平无奇的代码实现模型内部细节。

原则 7 透明性原则: 设计要可见,以便审查和调试

首先,定义一下,什么是透明性和可显性。

"如果没有阴暗的角落和隐藏的深度,软件系统就是透明的。透明性是一种被动的品质。如果实际上能预测到程序行为的全部或大部分情况,并能建立简单的心理模型,这个程序就是透明的,因为可以看透机器究竟在干什么。

如果软件系统所包含的功能是为了帮助人们对软件建立正确的'做什么、怎么做'的心理模型而设计,这个软件系统就是可显的。因此,举例来说,对用户而言,良好的文档有助于提高可显性;对程序员而言,良好的变量和函数名有助于提高可显性。可显性是一种主动品质。在软件中要达到这一点,仅仅做到不晦涩是不够的,还必须要尽力做到有帮助。"

我们要写好程序,减少 bug,就要增强自己对代码的控制力。你始终做到,理解自己调用的函数/复用的代码大概是怎么实现的。不然,你可能就会在单线程状态机的 server 里调用有 IO 阻塞的函数,让自己的 server 吞吐量直接掉到底。进而,为了保证大家能对自己代码能做到有控制力,所有人写的函数,就必须具备很高的透明性。而不是写一些看了一阵看不明白的函数/代码,结果被迫使用你代码的人,直接放弃了对掌控力的追取,甚至放弃复用你的代码,另起炉灶,走向了'制造重复代码'的深渊。

透明性其实相对容易做到的,大家有意识地锻炼一两个月,就能做得很好。可显性就不容易了。有一个现象是,你写的每一个函数都不超过 80 行,每一行我都能看懂,但是你层层调用,很多函数调用,组合起来怎么就实现了某个功能,看两遍,还是看不懂。第三遍可能才能大概看懂。大概看懂了,但太复杂,很难在大脑里构建起你实现这个功能的整体流程。结果就是,阅读者根本做不到对你的代码有好的掌控力。

可显性的标准很简单,大家看一段代码,懂不懂,一下就明白了。但是,如何做好可显性?那就是要追求合理的函数分组,合理的函数上下级层次,同一层次的代码才会出现在同一个函数里,追求通俗易懂的函数分组分层方式,是通往可显性的道路。

当然,复杂如 linux 操作系统,office 文档,问题本身就很复杂,拆解、分层、组合得再合理,都难建立心理模型。这个时候,就需要完备的文档了。完备的文档还需要出现在离代码最近的地方,让人'知道这里复杂的逻辑有文档',而不是其实文档,但是阅读者不知道。再看看上面 golang 标准库里的 http.Request,感受到它在可显性上的努力了么?对,就去学它。

原则 10 通俗原则: 接口设计避免标新立异

原则 11 缄默原则: 如果一个程序没什么好说的,就沉默

这个原则,应该是大家最经常破坏的原则之一。一段简短的代码里插入了各种'log("cmd xxx enter")', 'log("req data " + req.String())',非常害怕自己信息打印得不够。害怕自己不知道程序执行成功了,总要最后'log("success")'。但是,我问一下大家,你们真的耐心看过别人写的代码打的一堆日志么?不是自己需要哪个,就在一堆日志里,再打印一个日志出来一个带有特殊标记的日志'log("this_is_my_log_" + xxxxx)'?结果,第一个作者打印的日志,在代码交接给其他人或者在跟别人协作的时候,这个日志根本没有价值,反而提升了大家看日志的难度。

一个服务一跑起来,就疯狂打日志,请求处理正常也打一堆日志。滚滚而来的日志,把错误日志淹没在里面。错误日志失去了效果,简单地 tail 查看日志,眼花缭乱,看不出任何问题,这不就成了'为了捕获问题'而让自己'根本无法捕获问题'了么?

沉默是金。除了简单的 stat log,如果你的程序'发声'了,那么它抛出的信息就一定要有效!打印一个 log('process fail')也是毫无价值,到底什么 fail 了?是哪个用户带着什么参数在哪个环节怎么 fail 了?如果发声,就要把必要信息给全。不然就是不发声,表示自己好好地 work 着呢。不发声就是最好的消息,现在我的 work 一切正常!

"设计良好的程序将用户的注意力视为有限的宝贵资源,只有在必要时才要求使用。"程序员自己的主力,也是宝贵的资源!只有有必要的时候,日志才跑来提醒程序员'我有问题,来看看',而且,必须要给到足够的信息,让一把讲明白现在发生了什么。而不是程序员还需要很多辅助手段来搞明白到底发生了什么。

每当我发布程序 ,我抽查一个机器,看它的日志。发现只有每分钟外部接入、内部 rpc 的个数/延时分布日志的时候,我就心情很愉悦。我知道,这一分钟,它的成功率又是 100%,没任何问题!

原则 12 补救原则: 出现异常时,马上退出并给出足够错误信息

其实这个问题很简单,如果出现异常,异常并不会因为我们尝试掩盖它,它就不存在了。所以,程序错误和逻辑错误要严格区分对待。这是一个态度问题。

'异常是互联网服务器的常态'。逻辑错误通过 metrics 统计,我们做好告警分析。对于程序错误 ,我们就必须要严格做到在问题最早出现的位置就把必要的信息搜集起来,高调地告知开发和维护者'我出现异常了,请立即修复我!'。可以是直接就没有被捕获的 panic 了。也可以在一个最上层的位置统一做好 recover 机制,但是在 recover 的时候一定要能获得准确异常位置的准确异常信息。不能有中间 catch 机制,catch 之后丢失很多信息再往上传递。

很多 Java 开发的同学,不区分程序错误和逻辑错误,要么都很宽容,要么都很严格,对代码的可维护性是毁灭性的破坏。"我的程序没有程序错误,如果有,我当时就解决了。"只有这样,才能保持程序代码质量的相对稳定,在火苗出现时扑灭火灾是最好的扑灭火灾的方式。当然,更有效的方式是全面自动化测试的预防:)

具体实践点

前面提了好多思考方向的问题。大的原则问题和方向。我这里,再来给大家简单列举几个细节执行点吧。毕竟,大家要上手,是从执行开始,然后才是总结思考,能把我的思考方式抄过去。下面是针对 golang 语言的,其他语言略有不同。以及,我一时也想不全我所执行的 所有细则,这就是我强调'原则'的重要性,原则是可枚举的。

  1. 对于代码格式规范,100%严格执行,严重容不得一点沙。

  2. 文件绝不能超过 800 行,超过,一定要思考怎么拆文件。工程思维,就在于拆文件的时候积累。

  3. 函数对决不能超过 80 行,超过,一定要思考怎么拆函数,思考函数分组,层次。工程思维,就在于拆文件的时候积累。

  4. 代码嵌套层次不能超过 4 层,超过了就得改。多想想能不能 early return。工程思维,就在于拆文件的时候积累。
    下面这个就是 early return,把两端代码从逻辑上解耦了。

  5. 从目录、package、文件、struct、function 一层层下来 ,信息一定不能出现冗余。比如 file.FileProperty 这种定义。只有每个'定语'只出现在一个位置,才为'做好逻辑、定义分组/分层'提供了可能性。

  6. 多用多级目录来组织代码所承载的信息,即使某一些中间目录只有一个子目录。

  7. 随着代码的扩展,老的代码违反了一些设计原则,应该立即原地局部重构,维持住代码质量不滑坡。比如:拆文件;拆函数;用 Session 来保存一个复杂的流程型函数的所有信息;重新调整目录结构。

  8. 基于上一点考虑,我们应该尽量让项目的代码有一定的组织、层次关系。我个人的当前实践是除了特别通用的代码,都放在一个 git 里。特别通用、修改少的代码,逐渐独立出 git,作为子 git 连接到当前项目 git,让 goland 的 Refactor 特性、各种 Refactor 工具能帮助我们快速、安全局部重构。

  9. 自己的项目代码,应该有一个内生的层级和逻辑关系。flat 平铺展开是非常不利于代码复用的。怎么复用、怎么组织复用,肯定会变成'人生难题'。T4-T7 的同学根本无力解决这种难题。

  10. 如果被 review 的代码虽然简短,但是你看了一眼却发现不咋懂,那就一定有问题。自己看不出来,就找高级别的同学交流。这是你和别 review 代码的同学成长的时刻。

  11. 日志要少打。要打日志就要把关键索引信息带上。必要的日志必须打。

  12. 有疑问就立即问,不要怕问错。让代码作者给出解释。不要怕问出极低问题。

  13. 不要说'建议',提问题,就是刚,你 pk 不过我,就得改!

  14. 请积极使用 trpc。总是要和老板站在一起!只有和老板达成的对于代码质量建设的共识,才能在团队里更好地做好代码质量建设。

  15. 消灭重复!消灭重复!消灭重复!

主干开发

代码主干开发(Trunk-Based Development)是一种敏捷开发方法,它强调频繁地集成代码和持续发布。这种开发方式将所有的代码提交到同一个代码库中,团队成员都从主干代码开始开发,减少了分支和合并带来的复杂性。以下是一些代码主干开发的优点:

  1. 更快的反馈:由于代码主干开发强调频繁地集成和持续发布,因此可以更快地获得反馈,及时发现和解决问题。

  2. 更高的可靠性:在主干上进行开发能够避免分支的复杂性,减少了出现错误的可能性。同时,也可以更容易地进行测试和验证,提高代码的可靠性。

  3. 更方便的协作:在主干上进行开发可以促进团队成员之间的协作,减少代码的重复工作和沟通成本。

  4. 更好的代码质量:代码主干开发鼓励开发人员频繁地提交代码和集成代码,从而使代码更加规范和清晰,提高了代码的质量。

  5. 更高的可维护性:代码主干开发可以使代码库保持整洁和有序,减少了代码的冗余和混乱,提高了代码的可维护性。

总之,代码主干开发能够提高软件开发的效率、可靠性和质量,减少分支和合并带来的复杂性,同时也能促进团队成员之间的协作,提高代码的可维护性。

最后,我来为'主干开发'多说一句话。道理很简单,只有每次被 review 代码不到 500 行,reviewer 才能快速地看完,而且几乎不会看漏。超过 500 行,reviewer 就不能仔细看,只能大概浏览了。而且,让你调整 500 行代码内的逻辑比调整 3000 行甚至更多的代码,容易很多,降低不仅仅是 6 倍,而是一到两个数量级。有问题,在刚出现的时候就调整了,不会给被 revew 的人带来大的修改负担。

如何进行代码主干开发

代码主干开发(Trunk-Based Development)是一种敏捷开发方法,它强调频繁地集成代码和持续发布。以下是进行代码主干开发的一些步骤:

  • 维护一个稳定的主干:将所有的代码提交到同一个代码库中,并维护一个稳定的主干分支。该分支应该具有良好的测试覆盖率,并且必须保证代码的质量和可靠性。

  • 采用小型的增量式开发方式:采用小型的增量式开发方式,每次只提交少量的代码并及时进行集成和测试。这样可以及早发现问题并尽早解决。

  • 频繁地进行集成和构建:在进行代码主干开发时,要频繁地进行代码集成和构建。通过自动化构建工具(如Jenkins),可以实现快速、准确地构建和测试代码。

  • 进行代码评审:在提交代码之前,需要进行代码评审。代码评审可以发现隐藏的问题,提高代码的质量和可靠性。

  • 持续集成和交付:采用持续集成和持续交付等技术,可以让代码更快地进入生产环境。这样可以更快地获取反馈,及时发现和解决问题。

总之,代码主干开发的核心是频繁地集成和持续发布。在进行代码主干开发时,需要维护一个稳定的主干分支,并采用小型的增量式开发方式。同时,还需要频繁地进行集成、构建和评审,并采用持续集成和持续交付的技术来加快代码的发布速度。