如何做好Code Review

news2024/11/26 10:31:44
本文主要从我们为什么需要CR?CR面临哪些挑战?CR的最佳实践几个方面分析,希望可以给读者一些参考。

为什么需要CR?

代码质量

定性来看,大家都认可Code Review(后文简称CR)能显著改善代码质量,但国内量化的研究结果比较少,以下引用业界比较知名的几个定量研究结果:

Capers Jones分析了超过12,000个软件开发项目,其中使用正式代码审查的项目,潜在缺陷发现率约在60-65%之间;大部分的测试,潜在缺陷发现率仅在30%左右。

Steve McConnel在《Code Complete》中提到:仅仅依靠软件测试效能有限–单测平均缺陷发现率只有25%,功能测试35%,集成测试45%,相反,设计和代码审查可以达到55%到60%。

SmartBear研究了一个历时3月,包括10名开发人员完成的10,000行代码的工程,通过引入CR在接下来的6个月中可以节省近6成的bug修复成本

图片

图片

技术交流

SmartBear研究报告中这段话比较能表达CR对技术交流的价值,引用如下:

Actually writing the source code, however, is a solitary activity. Since developers tend to create code in quiet places, collaboration is limited to occasional whiteboard drawings and a few shared interfaces. No one catches the obvious bugs; no one is making sure the documentation matches the code. Peer code review puts the collaborative element back into this phase of the software development process.

Google认为CR参与塑造了公司的工程师文化,CR也与笔者所在部门一贯倡导的「极致透明」的文化相契合,资深同学的CR,对团队内新人的快速成长也很有帮助。

卓越工程

Linux的创始人Linus Torvalds有句名言:Talk is cheap, Show me the code,代码是程序员的作品,CR只是一种提升代码质量的工具,敢于Show出自己的代码,并用开放的心态去优化完善,才是每个程序员审视自我,从优秀到卓越的关键所在。

既然CR好处这么多,大多团队也在实践,但为什么效果差强人意呢,主要是CR在大型项目实践中面临诸多挑战。

CR面临哪些挑战?

挑战1:CR的代码改动范围过大

笔者观察,很多项目落实CR的最大挑战是项目进度压力很大,发布计划倒排根本没有给CR预留时间,所以大部分CR是在临近提测前(甚至有些是边测试边进行)集中进行,面对动辄上千行的代码变动,评审者需要花大量时间和代码提交者交流了解业务逻辑,迫于时间压力大多只会检查最基本的编码规范问题,而没有达到CR预期的效果。SmartBear公司对 CR 节奏的研究指出:每次大于400行的CR每千行代码缺陷发现率几乎为零。

图片

 

图片

那么怎样的提交粒度比较合适呢?笔者的经验是和单元测试case匹配(这里问题来了,没时间写单元测试怎么办,本文姊妹篇再来聊聊单元测试),完成一个功能,跑一个单元测试验证逻辑,然后commit一次。如下图,Aone(阿里内部的研发平台)提供的功能内置支持按照提交版本分批DIFF,分批Review。

图片

挑战2:CR对评审者全局知识要求很高

CR一般由团队内资深的技术同学进行,对于大型复杂项目的CR需要评审者对编码规范、分布式架构设计原则、业务知识有全面的了解,举个例子,下面是某服务提供的等级查询接口关键代码CR片段:


- public Level queryLevel(LevelQueryRequest request) {
+ public Level queryLevelWithExpireRefresh(LevelQueryRequest request) {

    Level result = levelRepository.findLevelWithoutInit(request.getId());
    if (null == result || isExpired(result.getEndTime())) {
        // 如果等级为空,兜底返回L0;等级已过期,实时返回默认等级,并异步刷新
        if (result == null) {
            result = levelRepository.buildInitLevel(request.getId(), LevelEnum.L0);
        }

        //查询为空,或者已过期发送消息,刷新等级
-       LevelRefreshRequest refreshRequest = buildRefreshRequest(request);
-       levelWriteService.refreshLevel(message.getId(), refreshRequest);
        
+       RefreshMessage refreshMessage = buildRefreshMessage(request);
+       refreshMessageProducer.sendMessage(refreshMessage);
    }

    return result;
}
- public class RefreshMessageListener extends AbstractMessageListener {
+ public class RefreshMessageListener extends AbstractOrderlyMessageListener {

    @Autowired
-    private LevelWriteService levelWriteService; 
+    private LevelWriteRegionalService levelWriteRegionalService;

    @Override
    protected boolean process(String tags, String msgId, String receivedMsg) {
        RefreshMessage message = JSON.parseObject(receivedMsg, RefreshMessage.class);
        if (message == null || message.getId() == null) {
            log.warn("message is invalid, ignored, src={}", receivedMsg);
            return true;
        }

        LevelRefreshRequest refreshRequest = buildRefreshRequest(message);
-       levelWriteService.refreshLevel(message.getId(), refreshRequest);
+       levelWriteRegionalService.refreshLevel(message.getId(), refreshRequest);

        return true;
    }
}
 

面对上面代码改动,要进行富有成效的CR,下面是代码评审者必须掌握的业务和技术知识:

  • 为什么存在等级为空的情况?

  • 为什么要设计成读时写?

  • 为什么不是直接计算等级,而需要用消息队列?

  • 为什么要用Regional(区域化)接口和有序消息刷新等级?

挑战3:CR价值最大化需要团队具备卓越工程基因

前文提到CR有助于团队内的技术交流,下面是几个笔者亲历的Case,通过对典型CR问题的广泛讨论不仅提升了业务代码的质量,而且探索到了技术创新点,逐步建立起团队追求技术卓越的氛围:

CASE1:一个业务使用3个时间穿越开关

背景

时间穿越是营销类业务系统最常使用的工具之一,通过全局控制,可以提前测试某个在未来开始的业务功能。笔者接触到的一个业务由2个服务A、B组成,A是一个老应用,使用了一个开关,后来B又在不同业务场景中使用了2个新的开关,在一次CR中发现了一个业务重复使用3个不同开关的问题,由此展开了一次讨论。

private static final String CODE = "BENEFIT_TIME_THROUGH";

public Date driftedNow(String userId) {
    try {
        TimeMockResult<Long> result = timeThroughService.getFutureTime(CODE, userId);
        if (result.isSuccess()) {
            return new Date(result.getData());
        }
    } catch (Throwable t) {
        log.error("timeThroughService error. userId={}", userId, t);
    }
    return new Date();
}
观点1:彻底服务化

按照DRY(Don't Repeat Yourself)原则,最理想的方案是把该业务用到的时间穿越开关统一由一个服务提供,因为时间穿越工具是借助动态配置中心推送开关到本地,然后做内存计算;如果统一成服务后,A和B都需要依赖远程服务调用,而B是一个高并发的使用场景,会有较大性能损耗。

观点2:富客户端

把统一开关包装成一个三方库,独立提供jar包供A、B服务分别依赖,这样解决了前面方案的性能消耗问题。但两个应用需要同步做更新和升级。

观点3:配置统一,重复代码三处收拢为两处

该观点认为彻底服务化和富客户端属于两种极端,可以采取折中方案容忍部分代码重复,但使用相同的时间穿越开关。

总结:

这个Case的讨论涉及到一个公共逻辑抽取的方案权衡问题,进程内调用的富客户端性能损耗低,但后期维护和升级困难,而且过于复杂的客户端逻辑容易引发依赖方包冲突、启动耗时增加等问题;彻底服务化只需要保持接口契约一致可以实现较快迭代,但对服务提供者SLA要求高;为了平衡前两者的问题,微服务架构中的SideCar模式则是在功能性的应用容器旁部署另一个非功能性容器,使得开发团队可以对主应用和SideCar进行独立管理。关于这个问题网上有很多讨论内容读者可以进一步了解学习。

CASE2:SSR(服务端渲染)API稳定性优化

背景

图片

上图是一个典型的服务端渲染服务架构,SSR服务通过加载配置,对每个模块进行独立数据组装,并整体返回结果到端侧。一般应用在电商系统复杂只读页面的动态搭建,如首页、商品详情页、导购频道等。下面是组装数据部分的待评审代码片段。


// 提交任务
ioTaskList.stream().forEach(t -> futures.add(pool.submit(() -> t.service.invoke())));

// 阻塞获取任务结果
futures.stream().forEach(f -> {
    try {
        result.add(f.get());
    } catch (Exception e) {
        log.error(e.getMessage(), e);
    }
});
Step1:增加固定超时控制
// 提交任务
ioTaskList.stream().forEach(t -> futures.add(pool.submit(() -> t.service.invoke())));

// 阻塞获取任务结果
futures.stream().forEach(f -> {
    try {
-       result.add(f.get());
+       result.add(f.get(1000, TimeUnit.MICROSECONDS));
    } catch (Exception e) {
        log.error(e.getMessage(), e);
    }
});
Step2:自适应超时控制
public abstract class BaseService<T> implements Service {

    @Override
    public T invoke(ServiceContext context) {
        Entry entry = null;
        try {
            // 根据service类别构造降级资源
            String resourceName = "RESOURCE_" + name();
            entry = SphU.entry(resourceName);
            try {
                // 未触发降级,正常调用后端服务
                return realInvoke(context);
            } catch (Exception e) {
                // 业务异常,记录错误日志,返回出错信息
                return failureResult(context);
            }
        } catch (BlockException e) {
            // 被降级,可以fail fast或返回兜底数据
            return degradeResult(context);
        } finally {
            entry.exit();
        }
    }

    public abstract T realInvoke();
}
Step3:自适应超时控制+自定义资源key
public abstract class BaseService<T> implements Service {

    @Override
    public T invoke(ServiceContext context) {
        Entry entry = null;
        try {
            // 这里的key由service实现,融合了服务类型和自定义key构造降级资源
            String resourceName = "RESOURCE_" + key(context);
            entry = SphU.entry(resourceName);
            try {
                // 未触发降级,正常调用后端服务
                return realInvoke(context);
            } catch (Exception e) {
                // 业务异常,记录错误日志,返回出错信息
                return failureResult(context);
            }
        } catch (BlockException e) {
            // 被降级,可以fail fast或返回兜底数据
            return degradeResult(context);
        } finally {
            entry.exit();
        }
    }
    

    public abstract String key(ServiceContext context);

    public abstract T realInvoke();
}
总结:

Step1很容易理解,增加1000ms超时设置可以避免某个数据源严重超时导致整个渲染API不稳定,做到fail fast,但核心挑战在于多长的超时时间算合理;Step2通过依赖降级组件,根据不同数据源服务设置不同超时时间,实现了自适应超时控制;Step3相比Step2改动非常小,不了解业务背景可能不清楚它们的区别,Step2的降级控制作用在服务类别上,比如营销服务、推荐服务各自触发降级,但还有一类数据源服务其实是网关类型,内部耗时会根据某个或某些参数不同有较大差异,例如TPP(阿里内部的算法平台,不同算法逻辑共享一个网关API,但不同算法复杂度耗时差异巨大)服务就是一个典型,所以Step3允许自定义key()实现更精细的超时控制。

图片

团队由CR引发的技术深入讨论和持续优化形成了这套自动化降级能力,上图是实际线上运行效果,可以看到系统随着依赖数据源服务RT的抖动实现了自动化自适应降级和恢复。

CR有没有最佳实践?

Code Review的边界

图片

对于什么是一个好代码,上图从可靠、可维护和功能完备做了划分。笔者认为CR并非包治百病的银弹,它也有它的能力边界。把设计方案交给设计评审,把业务逻辑验证交给单测;把编码规范交给静态代码扫描(Static Code Analysis),剩下部分再由Peer Review做最后一道把关。CR引发的技术传承、技术交流以及由此形成的追求卓越的团队文化,才是它的最大价值。

图片

出发点:程序员的初心

归根结底,程序员的好奇心和匠心才是提升代码质量的根本,目前笔者所在部门已经在晋升考核中增加了CR环节,烂代码会被一票否决,这就需要日常工作中不断追求技术卓越,在平时多下功夫。

看不见的手:自动代码扫描

之前在某社区看到有个热帖讨论程序员的工作是不是劳动密集型,某个回帖比较形象「我们的工作本应是CPU密集型,结果却成了IO密集型」。基本的编码规范完全可以借助代码自动化扫描识别,而这个占比也是比较高,可以有效降低CR成本。业界的CheckStyle、FindBug都有完善的CI/CD插件支持,阿里云也提供了IDE智能编码插件,内置了编码规范支持。

看得见的手:Team Leader的重视

喊口号没有用,只有躬身入局。身边几位参加晋升同学CR的评审官普遍感受是:代码质量分布通常会团队化,不要指望个别优秀的同学带动团队的整体水平提升。确实如此,代码质量需要Team Leader高频参与CR,技术文化的形成需要主管以身作则。


以下是我收集到的比较好的学习教程资源,虽然不是什么很值钱的东西,如果你刚好需要,可以评论区,留言【777】直接拿走就好了

各位想获取资料的朋友请点赞 + 评论 + 收藏,三连!

三连之后我会在评论区挨个私信发给你们~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/844705.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

自监督去噪:Blind2Unblind原理分析与总结

文章目录 1. 方法原理1.1 动机与贡献1.2 方法细节(1) Noise2Void(2) re-visible without identity mapping(3) 综合说明 2. 效果3. 总结 1. 方法原理 1.1 动机与贡献 摘要要点&#xff1a;基于盲点去噪的网络受网络设计和/或输入数据的影响会丢失部分信息 --> 有价值的信息…

UNIX 系统概要

UNIX 家族UNIX 家谱家族后起之秀 LinuxUNIX vs LinuxUNIX/Linux 应用领域 UNIX 操作系统诞生与发展UNIX 操作系统概要内核常驻模块shell虚拟计算机特性 其他操作系统 LinuxRichard StallmanGNU 项目FSF 组织GPL 协议Linus Torvalds UNIX 家族 有人说&#xff0c;这个世界上只有…

优维低代码实践:对接数据

优维低代码技术专栏&#xff0c;是一个全新的、技术为主的专栏&#xff0c;由优维技术委员会成员执笔&#xff0c;基于优维7年低代码技术研发及运维成果&#xff0c;主要介绍低代码相关的技术原理及架构逻辑&#xff0c;目的是给广大运维人提供一个技术交流与学习的平台。 优维…

CS 144 Lab Five -- the network interface

CS 144 Lab Five -- the network interface TCP报文的数据传输方式地址解析协议 ARPARP攻击科普 Network Interface 具体实现测试tcp_ip_ethernet.ccTCPOverIPv4OverEthernetAdapterTCPOverIPv4OverEthernetSpongeSocket通信过程 对应课程视频: 【计算机网络】 斯坦福大学CS144…

Gradle Run with --stacktrace option to get the stack trace

IDEA中使用Gradle的时候遇到以下异常&#xff1a; * Try:Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights. 解决办法&#xff1a; IDEA中File-Settings-Build&#…

比特鹏哥2-数据类型和变量【自用笔记】

比特鹏哥2-数据类型和变量【自用笔记】 1.数据类型介绍字符&#xff0c;整型&#xff0c;浮点型&#xff0c;布尔类型 2.signed 和unsigned3.数据类型的取值范围sizeof 展示字节大小--- 计算机中单位&#xff1a;字节 4.变量 常量4.1 变量创建变量&#xff08;数据类型 变量名&…

awk基础知识和案例

文章目录 awk1 awk用法入门1.1 BEGIN和END语句块1.2 awk语法1.2.1 常用命令选项1.2.2 awk变量内置变量自定义变量 1.3 printf命令1.3.1 格式1.3.2 演示 1.4 操作符 2 awk高阶用法2.1 awk控制语句(if-else判断)2.2 awk控制语句(while循环)2.3 awk控制语句(do-while循环)2.4 awk控…

PingCAP 入选 Gartner 《Hype Cycle for Data Management 2023》代表厂商

日前&#xff0c;全球科技咨询与研究机构 Gartner 发布了《Hype Cycle for Data Management 2023》&#xff08;2023 年数据管理技术成熟度曲线报告&#xff09;&#xff0c;PingCAP 凭借技术积累和产品优势&#xff0c;入选报告“用于数据管理的生成式人工智能”&#xff08;G…

Win10下webots2020b闪退

下载安装完之后打开软件就会停留在这个界面几秒钟&#xff0c;什么都点不了&#xff0c;然后就会闪退回桌面 原因: webots安装路径中有中文 解决方案&#xff1a; 安装路径下的中文改为英文

真的不想知道录音转文字怎么弄才简单吗

哇哦&#xff01;听说你想知道如何将录音转成文字&#xff1f;这简直是一个超酷的技能&#xff0c;让我来为你揭开这个神奇的面纱吧&#xff01;想象一下&#xff0c;当你有一堆录音文件需要处理时&#xff0c;你不再需要费尽心思地一遍遍倾听、抄写。现在&#xff0c;你只需要…

【Go 基础篇】开发环境搭建与开发工具选择

介绍 Go语言&#xff0c;也被称为Golang&#xff0c;是由Google开发的一门开源编程语言。它以其简洁高效、并发性能优异而备受开发者青睐。若想开始Go语言的学习和开发&#xff0c;首先需要搭建适合的开发环境&#xff0c;并选择合适的开发工具来提高效率。本篇博客将详细介绍…

迭代器模式(C++)

定义 提供一种方法顺序访问一个聚合对象中的各个元素&#xff0c;而又不暴露(稳定)该对象的内部表示。 应用场景 在软件构建过程中&#xff0c;集合对象内部结构常常变化各异。但对于这些集合对象&#xff0c;我们希望在不暴露其内部结构的同时&#xff0c;可以让外部客户代…

kafka:java client使用总结塈seek() VS commitSync()的区别(三)

最近一段日子接触了kafka这个消息系统&#xff0c;主要为了我的开源中间件项目simplemq增加kafka支持&#xff08;基于kafka-client【java】&#xff09;&#xff0c;如今总算完成&#xff0c;本文是对这个过程中对kafka消息系统的使用总结 线程安全 关于线程安全&#xff0c…

04-2_Qt 5.9 C++开发指南_SpinBox使用

文章目录 1. SpinBox简介2. SpinBox使用2.1 可视化UI设计2.2 widget.h2.3 widget.cpp 1. SpinBox简介 QSpinBox 用于整数的显示和输入&#xff0c;一般显示十进制数&#xff0c;也可以显示二进制、十六进制的数&#xff0c;而且可以在显示框中增加前缀或后缀。 QDoubleSpinBox…

无人车沿着指定线路自动驾驶与远程控制的实践应用

有了前面颜色识别跟踪的基础之后&#xff0c;我们就可以设定颜色路径&#xff0c;让无人车沿着指定线路做自动驾驶了&#xff0c;视频&#xff1a;PID控制无人车自动驾驶 有了前几章的知识铺垫&#xff0c;就比较简单了&#xff0c;也是属于颜色识别的一种应用&#xff0c;主要…

Vue + Cesium快速搭建,全流程(最新总结)

方式一&#xff1a;直接引入&#xff08;最简单&#xff09; 1.安装Cesium&#xff08;Vue搭建可以看我上一期的文章&#xff09; npm i cesium -save2.将node_modules\cesium\Build\Cesium文件夹拷贝到项目的public文件中 3.在public\index.html引入Cesium <!DOCTYPE h…

1466. 重新规划路线

题目描述&#xff1a; 主要思路&#xff1a; 将所有有向边抽象为无向边&#xff0c;将原有的方向权重置为1&#xff0c;其余置为0。 从0开始遍历所有城市&#xff0c;ans权重和。 class Solution { public:vector<vector<int>> a,w;int ans0;bool book[500010];v…

Node.js |(一)Node.js简介及计算机基础 | 尚硅谷2023版Node.js零基础视频教程

学习视频&#xff1a;尚硅谷2023版Node.js零基础视频教程&#xff0c;nodejs新手到高手 文章目录 &#x1f4da;关于Node.js&#x1f407;为什么要学Node.js&#x1f407;Node.js是什么&#x1f407;Node.js的作用&#x1f407;Node.js下载安装&#x1f407;命令行工具&#x1…

【Linux】多路转接 -- poll函数

文章目录 1. poll函数原型2. poll服务器3. poll的优点和确定 1. poll函数原型 poll函数和与我上一篇文章介绍的select函数一样&#xff0c;都是系统提供的多路转接接口&#xff0c;允许进程或线程在同一时间监听多个文件描述符。 本篇文章的一部分内容与上一篇介绍select函数…

Report Sharp-Shooter Lite Edition Crack

Report Sharp-Shooter Lite Edition Crack 报告Sharp Shooter™ 是为.NET Framework设计的&#xff0c;使用C#编写&#xff0c;并且只包含100%的托管代码。Report Sharp Shooter能够从多个数据源生成任何复杂的报告&#xff0c;并将生成的报告导出为大多数格式&#xff0c;包括…