TL;DR
参考资料
https://composity.com/post/too-busy-to-improve
https://commadot.com/wtf-per-minute/
https://dl.acm.org/doi/10.1145/3585004#d1e372
https://google.github.io/eng-practices/review/reviewer/standard.html
https://book.douban.com/subject/35513153/
https://zhuanlan.zhihu.com/p/549019453
名词解释
CR: Code Review
CR:代码审查
CL: Stands for "changelist", which means one self-contained change that has been submitted to version control or which is undergoing code review. Other organizations often call this a "change", "patch", or "pull-request".
CL:代表“变更列表”,表示已提交到版本控制或正在进行代码审查的独立更改。其他组织通常将其称为“更改”、“补丁”或“拉取请求”。
LGTM: Means "Looks Good to Me". It is what a code reviewer says when approving a CL.
LGTM:意思是“对我来说看起来不错”。这是代码审查员在批准 CL 时所说的。
CR意义
灵魂拷问:为什么我们接手的每个代码库都如此难以维护?

重要原因之一:Code Review 执行不彻底
万能借口:太忙!
- 疲于应付眼前
- 不可见,意识不到
- CR 非功能性开发
- CR 不是当务之急,没有眼前收益
- 危害被低估,忽视“复利”的威力(非线性)
意义
现代代码评审【modern Code Review】,业内认为有效的、基于最佳实践的质量保证工作流,可通过人工审代码降低风险、增强可维护性和提升研发效率,同时可以有效提升个人和团队技术能力。更是一种对代码精益求精、追求极致的态度、是“工匠精神”的一种体现。
CR原则
-
只要CL可以提高整体代码健康状态,就应该倾向于批准合入,即使CL并不完美
-
基于技术事实和数据的沟通
- 基于技术事实和数据否决个人偏好和意见
- 软件设计问题不能简单归结为个人偏好
-
解决冲突:不要因为无法达成一致而卡壳
-
善用工具
- 基于Lint、公司代码规范等工具
- 大模型辅助
发起CR
发起前的准备
- 推荐自己做一个 checklist
- 把自己当作 Reviewer 来对自己的代码进行 CR
- 预估代码可能出问题的地方
- 进行充分自测,保证代码可运行
- 不要指望别人帮你找出问题
checklist 可参考Code Review 速查手册
利用自动化工具进行前置检查
- 单元测试检查
- 新增单元测试检查
- 方法行数过多
- 圈复杂度过高
- 代码规范检查
- lint 检查
- 体积监控
建议平均时长不要超过10分钟, 所以 e2e,性能检查等建议不阻塞发起MR流程
合理的规模


https://smartbear.com/learn/code-review/best-practices-for-peer-code-review/
- 一次评审200LOC为佳,最多400LOC
- 评审量应低于500LOC/小时
- 一次评审不要超过60分钟
- 采用轻量级评审方式
- 全员参与代码评审
- 每周花费0.5~1天开展CR
- 严格且彻底的评审
如何拆分 CL
https://google.github.io/eng-practices/review/developer/small-cls.html
Commit 描述
Bad Case:

“修复错误”是不充分的 CL 描述。什么 bug?你做了什么来修复它?
其他类似的不良描述包括:
- 逻辑修复
- 添加补丁
- 增加XX规则
- 删除XX逻辑
Good Case:
◆ 摘要:【xx模块】新增xx功能
◆ 背景: 新功能需求,要求xxx, 详见[卡片链接]
◆ 说明: 由于xx,新增xx处理模块…
- 摘要:删除 RPC 服务器消息自由列表的大小限制
- 说明:像 FizzBuzz 这样的服务器有非常大的消息,可以从重用中受益。使自由列表更大,并添加一个 goroutine 随着时间的推移慢慢释放自由列表条目,以便空闲服务器最终释放所有自由列表条目。
必要时,应使用 cz 等工具进行规范。
心态
- 一次 CR 其实是开启的一次“对话”
- 应该期待评论和反馈,并及时进行回复
- 做好心理准备自己的代码可能会有缺陷
- CR 的目的之一就是发现问题, 所以不要有抵触
CR内容
代码是写给人看的, 不是写给机器看的,只是顺便计算机可以执行而已。------《计算机程序的构造和解释》
应该被 CR 的内容:
自上而下,优先级从高到低:

| 模块 | 简介 |
|---|---|
| 设计 | 代码是否设计良好并适合您的系统? |
| 功能 | 代码的行为是否符合作者的预期?代码的行为方式对其用户有好处? |
| 安全性 | 代码是否安全合规? |
| 复杂性 | 代码可以更简单吗?当其他开发人员将来遇到此代码时,他们是否能够轻松理解和使用此代码? |
| 测试 | 代码是否具有正确且设计良好的自动化测试? |
| 命名 | 开发人员是否为变量、类、方法等选择了明确的名称? |
| 注释 | 注释是否清晰有用? |
| 风格 | 代码是否遵循京东代码风格规范? |
| 文档 | 开发人员是否更新了相关文档? |
https://google.github.io/eng-practices/review/reviewer/looking-for.html
CR流程顺序

https://google.github.io/eng-practices/review/reviewer/navigate
京东实际代码片段评审讲解
设计
应该有合理的职责划分,合理的封装
good case
1componentDidMount() { 2 this.fetchUserInfo(); 3 this.fetchCommonInfo(); 4 this.fetchBankDesc(); 5}
bad case
1componentDidMount() { 2 const { location, dispatch, accountInfoList } = this.props; 3 const { token, af } = getLocationParams(location) || {}; 4 dispatch({ 5 type: 'zpmUserCenter/fetchUserInfo', 6 payload: { 7 token, 8 }, 9 }).catch(e => { 10 const zpmOpenAuthUrlLogin = decodeURIComponent(getCookie('zpmOpenAuthUrlLogin')); 11 // 如果token过期则跳转回第三方平台 12 if ([TOKEN_EXPIRED_CODE, '300000'].includes(e.errorCode) && (isSaveUrl(af) || isSaveUrl(zpmOpenAuthUrlLogin))) { 13 setTimeout(() => { 14 window.location.href = isSaveUrl(af) ? af : zpmOpenAuthUrlLogin; 15 }, 2000); 16 } 17 }); 18 19 if (!this.showWhichHeader() && !this.showGatewayHeader()) { 20 dispatch({ 21 type: 'zpmUserCenter/fetchAccountInfo', 22 payload: { 23 token, 24 }, 25 }); 26 } 27 this.getBlackList() 28}
问题1,fetchUserInfo 未进行封装
问题2,af 命名过于随意
问题3,‘300000’ 魔法字符串
问题4,选择使用 af 或 zpm 这两个URL的逻辑建议封装,避免多次调用 isSaveUrl
优秀代码设计的特质 CLEAN
• Cohesive:内聚的代码更容易理解和查找bug
• Loosely Coupled:松耦合的代码让实体之间的副作用更少,更容易测试、复用、扩展
• Encapsulated:封装良好的代码有助于管理复杂度,也更容易修改
• Assertive:自主的代码其行为和其所依赖的数据放在一起,不与其它代码互相干预(Tell but not Ask)
• Nonredundant:无冗余的代码意味着可以只在一个地方修复bug和进行更改
应避免过度设计
别人在阅读代码时,能清晰辨别我在代码中的设计模式,并且能够随着这个模式继续维护
功能
逻辑正确,逻辑合理,避免晦涩难懂的逻辑
bad case:一段表单代码(原代码过长,仅贴出其中典型的一段)
1{ hasQuota ? ( 2 ['11', '12'].indexOf(invoiceType) === -1 ? ( 3 <div className="m-b-4 row"> 4 <div className="col-11"> 5 <FormItem 6 label="基础核验" 7 > 8 9 {basicVerifyStatus ? '已通过' : <div> 10 未通过 11 <Tooltip title={basicVerifyMsg} placement="bottom"> 12 <span className='iconfont icon-tishi theme-primary m-l-1 font-14' /> 13 </Tooltip> 14 </div>} 15 </FormItem> 16 </div> 17 <div className="col-11"> 18 <FormItem 19 label="剩余额度" 20 > 21 {formatAmount(availableLimit)} 22 </FormItem> 23 </div> 24 </div>) : 25 <div className="m-b-4 row"> 26 <div className="col-11"> 27 <FormItem 28 label="基础核验" 29 > 30 31 {basicVerifyStatus ? '已通过' : <div> 32 未通过 33 <Tooltip title={basicVerifyMsg} placement="bottom"> 34 <span className='iconfont icon-tishi theme-primary m-l-1 font-14' /> 35 </Tooltip> 36 </div>} 37 </FormItem> 38 </div> 39 </div>) : 40 ['11', '12'].indexOf(invoiceType) === -1 ? <div className="m-b-4 row"> 41 <div className="col-11"> 42 <FormItem 43 label="基础核验" 44 > 45 46 {basicVerifyStatus ? '已通过' : <div> 47 未通过 48 <Tooltip title={basicVerifyMsg} placement="bottom"> 49 <span className='iconfont icon-tishi theme-primary m-l-1 font-14' /> 50 </Tooltip> 51 </div>} 52 </FormItem> 53 </div> 54 </div> : 55 null}
关键问题:连续三元判断 + 嵌套三元判断
其他问题:
- 魔法字符串, 且重复出现
['11', '12'].indexOf(invoiceType) === -1
- 无意义的空行,严重影响代码阅读
- FormItem 重复过多
Reviewer 建议:
- 对重复代码,梳理内容,进行合理命名
const isNotOnlineInvoice = ['11', '12'].indexOf(invoiceType) === -1;
- 每个 FormItem 也进行命名,三元逻辑梳理,重构
| isNotOnlineInvoice true | isNotOnlineInvoice false | |
|---|---|---|
| hasQuota true | verifyCodeFormItem invoiceCodeFormItem goodNameFormItem modifiedDateFormItem basicVerifyFormItem availableLimitFormItem | availableLimitFormItem modifiedDateFormItem basicVerifyFormItem |
| hasQuota false | verifyCodeFormItem invoiceCodeFormItem goodNameFormItem modifiedDateFormItem basicVerifyFormItem | basicVerifyFormItem modifiedDateFormItem |
安全性
代码中应注意,不要存储敏感内容
1// 微信服务号 生产配置中复写 2const WX_APP_ID = 'xxxxxxxxxx'; 3const WX_APP_SECRET = 'xxxxxxxxxxxxxxxxxxxxx'; 4 5// 票将军网站应用配置 (测试环境) 6const PJJ_APP_ID = 'xxxxxxxxxx'; 7const PJJ_APP_SECRET = 'xxxxxxxxxxxxxxxxxxxxx';
一致性
-
代码一致性:
- 函数名和实现一致
- 注释和代码一致
-
命名方式一致
-
异步写法一致(promise, async await,callback混用)
-
抽象层级一致
-
不建议混用 import 和 require
注释与代码不一致
1getCheckboxProps: record => ({ 2 disabled: !record.basicVerifyStatus || (hasQuota && record.availableLimit <= 0), // 状态为验证成功的时候才可选择使用 3})
命名不一致
1this.state = { 2 isOffline: false, 3 shouldShowFollowLink: false, 4 shouldShowToast: false, 5 ifReceiveNotify: false, 6 bShowAllDocsRedPoint: false, 7 isNewPushNotify: false, 8}
没事别写注释
good case:
为什么接下来的代码要这么做
bad case:
接下来的代码做了什么
复杂度
- 优先使用标准库中的能力
- 封装细节
- 写的代码越简单, bug越少
- 尽量遵守单一职责原则
- DRY——Don’t Repeat Yourself
无意义的函数封装
1// 根据admitStatus判断按钮试算前置灰状态 2export const isDisabledByAdmitStatus = discountListItem => { 3 if (!discountListItem?.bankInfo?.admitStatus) { 4 return true 5 } else { 6 return false 7 } 8}
建议使用moment、dayjs等标准时间库处理时间:
1// 本季度开始时间、结束时间,返回毫秒值 2export const getQuarterStartAndEndTime = ({ 3 time = null, 4 isTimestamp = true, 5 split = '/', 6 startDateTime = ' 00:00:00', 7 endDateTime = ' 23:59:59', 8} = {}) => { 9 let date = checkDate(time) ? new Date(time) : new Date() 10 let year = date.getFullYear() 11 let month = date.getMonth() + 1 12 let startTime = null 13 let endTime = null 14 if (month <= 3) { 15 startTime = year + split + '01' + split + '01' + startDateTime 16 endTime = year + split + '03' + split + '31' + endDateTime 17 } else if (month > 3 && month <= 6) { 18 startTime = year + split + '04' + split + '01' + startDateTime 19 endTime = year + split + '06' + split + '30' + endDateTime 20 } else if (month > 6 && month <= 9) { 21 startTime = year + split + '07' + split + '01' + startDateTime 22 endTime = year + split + '09' + split + '30' + endDateTime 23 } else { 24 startTime = year + split + '10' + split + '01' + startDateTime 25 endTime = year + split + '12' + split + '31' + endDateTime 26 } 27 28 // 本季度开始时间 29 startTime = isTimestamp ? getTimestamp(startTime) : startTime 30 31 // 本季度结束时间 32 endTime = isTimestamp ? getTimestamp(endTime) : endTime 33 34 return { 35 startTime, 36 endTime, 37 } 38}
DRY——Don’t Repeat Yourself
下面三个方法中重复逻辑非常多,应该进行合理的封装,降低复杂性。另一个比较常见的问题,console.log 这种调试代码不应该被合入主干。
1handleMergedInvoice = selectedRows => { 2 const { invoiceList } = this.state; 3 const { limitNum } = this.props; 4 5 const newInvoiceList = []; 6 for (const item of selectedRows) { 7 if (newInvoiceList.length && newInvoiceList.length >= limitNum) { 8 Message.error(`上传失败,发票数量不可超过${limitNum}张`); 9 return; 10 } 11 item.invoiceUrl = item.invoiceUrl || item.dataUrl || ""; 12 delete item.dataUrl; 13 item.invoiceDate = item.invoiceDate 14 ? moment(item.invoiceDate).format("YYYY-MM-DD") 15 : ""; 16 newInvoiceList.push({ ...item, id: getIncrementCid() }); 17 this.setState({ invoiceList: newInvoiceList }, () => { 18 const { invoiceList: list, errIndexList } = this.state; 19 if (list.length) { 20 this.verifyInvoiceList(list); 21 } 22 this.invoiceListReduce(); 23 }); 24 } 25};
1updateInvoice = ({ ...data }, i) => { 2 const { invoiceList } = this.state; 3 const oldInvoice = invoiceList[i]; 4 data.invoiceUrl = data.invoiceUrl || data.dataUrl || ""; 5 delete data.dataUrl; 6 data.invoiceDate = data.invoiceDate 7 ? moment(data.invoiceDate).format("YYYY-MM-DD") 8 : ""; 9 this.setState( 10 { 11 invoiceList: [ 12 ...invoiceList.slice(0, i), 13 { ...oldInvoice, ...data }, 14 ...invoiceList.slice(i + 1) 15 ] 16 }, 17 () => { 18 this.invoiceListReduce(); 19 } 20 ); 21};
1addInvoice = ({ ...data }) => { 2 const { invoiceList } = this.state; 3 const { limitNum } = this.props; 4 if (invoiceList.length && invoiceList.length >= limitNum) { 5 Message.error("上传失败,发票数量不可超过500张"); 6 return; 7 } 8 data.invoiceUrl = data.invoiceUrl || data.dataUrl || ""; 9 delete data.dataUrl; 10 data.invoiceDate = data.invoiceDate 11 ? moment(data.invoiceDate).format("YYYY-MM-DD") 12 : ""; 13 14 this.setState( 15 { 16 invoiceList: [...invoiceList, { ...data, id: getIncrementCid() }] 17 }, 18 () => { 19 const { invoiceList: list } = this.state; 20 if (list.length) { 21 this.verifyInvoiceList(list); 22 } 23 this.invoiceListReduce(); 24 } 25 ); 26};
封装细节
看到下面这段代码,大概能够想象 newValidate 出现的原因,为了文章阅读体验, 删除部分代码
这个验证函数,严重违反了单一职责,首先包含了多种校验逻辑,还承担了 submit 数据预处理、submit、error处理;不仅如此,还和视图层耦合,包括了回到顶部、定位到错误位置、错误DOM样式调整的逻辑。
当然了,看到newValidate代码行数,也没有好到哪去。
此处200多行代码就成了这个工程的毒瘤。

1validate = () => { 2 const { form, totalDraftAmount, output, outputBasename } = this.props; 3 const { validateFields } = form; 4 const { financeChannel, orderNo: oldOrderNo, checked } = this.state; 5 const ocrDomList = document.querySelectorAll('.ocrform'); 6 const checkboxDomList = document.querySelectorAll('.ant-checkbox'); 7 // 每次 validate 前先去除上次打的标, Object.values(ocrDomList)为了兼容 ie 8 Object.values(ocrDomList).forEach(item => { 9 item.classList.remove('field', 'error'); 10 }); 11 validateFields(async (err, data) => { 12 if (err) { 13 Message.warn('请核对填写的内容'); 14 if (output) { 15 // 回到顶部 16 scrollToTop(); 17 return; 18 } 19 // 定位到第一个错误 20 setTimeout(this.goToError(document.querySelector('.field.error'))); 21 return; 22 } 23 // 验证发票 24 const { contractAmt, draftInfos = {}, invoiceInfos } = data; 25 /* 此处省略20行 */ 26 if (totalDraftAmount > contractAmt) { 27 /* 此处省略7行 */ 28 } 29 // 访问子组件中的勾选状态 如没勾选,则校验不通过 30 const { checkedA, checkedB } = this.myRef.current.state; 31 // 只要有一个没勾选就进来 32 if (!checked || !checkedA || !checkedB) { 33 // 如果是 确认背书未勾选则 提示相应文案 34 Message.warn('请阅读并同意相关服务协议'); 35 if (output) { 36 // 回到顶部 37 scrollToTop(); 38 return; 39 } 40 // 先打标记,再定位错误 41 checkboxDomList[0].classList.add('error'); 42 // 定位到第一个错误 43 setTimeout( 44 this.goToError(document.querySelector('.ant-checkbox.error')) 45 ); 46 return; 47 } 48 let selectedDraftInfo = []; 49 /* 此处省略 14 行 selectedDraftInfo 数据组装*/ 50 const sendData = { 51 ...data, 52 draftInfos: selectedDraftInfo 53 }; 54 const { couponReleaseNo } = getLocationParams(location) || {}; 55 if (couponReleaseNo) sendData.couponReleaseNo = couponReleaseNo; 56 if (oldOrderNo) sendData.orgOrderNo = oldOrderNo; 57 // 用户提交时选择为无重复背书, 删除已上传重复背书文件 58 const { billReusedStatus } = this.endorseBillRef.current.state; 59 if (billReusedStatus === billReusedStatusEnum.noReuse) { 60 delete sendData.repeatedEndorseFileUrl; 61 } 62 try { 63 /* 此处省略 19 行 发起接口调用, 成功 + 失败逻辑处理*/ 64 } catch (error) { 65 this.setState({ 66 loading: false 67 }); 68 let errMessage; 69 // 通过 error.message 字符串中 拿到错误模块对应的 invoiceNo 70 errMessage = error.message.split('[')[1]; 71 if (!errMessage) return; 72 errMessage = errMessage.split(']')[0]; 73 // 通过报错信息定位到错误模块索引 74 const errorInvoiceIndex = invoiceInfos.findIndex( 75 item => item.invoiceNo === errMessage 76 ); 77 // 添加标红样式 78 ocrDomList[errorInvoiceIndex].classList.add('field', 'error'); 79 if (output) { 80 // 回到顶部 81 scrollToTop(); 82 return; 83 } 84 // 定位到发票错误位置 85 setTimeout(this.goToError(ocrDomList[errorInvoiceIndex])); 86 } 87 }); 88};
认知复杂度与圈复杂度
整体来说正相关, 也有例外:
1function getWords(number) { // +1 2 switch (number) { 3 case 1: // +1 4 return "one"; 5 case 2: // +1 6 return "a couple"; 7 case 3: // +1 8 return "a few"; 9 default: 10 return "lots"; 11 } 12} // 圈复杂度:4 13 14function getWords(number) { 15 switch (number) { // +1 16 case 1: 17 return "one"; 18 case 2: 19 return "a couple"; 20 case 3: 21 return "a few"; 22 default: 23 return "lots"; 24 } 25} // 认知复杂度:1
1function sumOfPrimes(max) { // +1 2 let total = 0; 3 for (let i = 1; i <= max; i++) { // +1 4 for (let j = 2; j < i; j++) { // +1 5 if (i % j === 0) { // +1 6 continue; 7 } 8 } 9 total += i; 10 } 11 return total; 12} // 圈复杂度:4 13 14function sumOfPrimes(max) { 15 let total = 0; 16 for (let i = 1; i <= max; i++) { // +1 17 for (let j = 2; j < i; j++) { // +2 18 if (i % j === 0) { // +3 19 continue; // +1 20 } 21 } 22 total += i; 23 } 24 return total; 25} // 认知复杂度:7
复杂度评判标准
- 需要添加“黑客代码(hack)”来保证功能的正常运行。
- 总是有其他开发者询问代码的某部分是如何工作的。
- 总是有其他开发者因为误用了你的代码而导致出现bug。
- 即使是有经验的开发者也无法立即读懂某行代码。
- 你害怕修改这一部分代码。
- 管理层认真考虑雇用一个以上的开发人员来处理一个类或文件。
- 很难搞清楚应该如何增加新功能。
- 如何在这部分代码中实现某些东西常常会引起开发者之间的争论。
- 人们常常对这部分代码做完全没有必要的修改,这通常在代码评审时,或者在变更被合并进入主干分支后才被发现。
--- 《编程原则》
规范性
这部分内容比较多,更多内容见 Code Review 手册
import 排序的例子
可以看到第一段代码,没有规律,阅读成本高,第1行, 第5行出现了重复引用。
reviewer建议:
使用工具进行格式化,提高可读性
https://github.com/lydell/eslint-plugin-simple-import-sort
https://github.com/import-js/eslint-plugin-import/
1import { ref } from 'vue' 2import Taro from '@tarojs/taro' 3import gwApi from '@/api/index-gateway-js' 4import api from '@/api/index-js' 5import { onMounted, reactive, watch } from 'vue' 6import InputRight from '../components/InputRight.vue' 7import { isweapp, getParams } from '@/utils' 8import { FACTORY_ADDRESS_ORIGIN } from '@/utils/const.js' 9import { sgmReportCustom } from '@/utils/log' 10import { genAddressStr } from '@/utils/address'
1import Taro from '@tarojs/taro' 2import { onMounted, reactive, ref, watch } from 'vue' 3 4import api from '@/api/index.js' 5import gwApi from '@/api/index-gateway-js' 6import { getParams, isWeapp } from '@/utils' 7import { genAddressStr } from '@/utils/address' 8import { FACTORY_ADDRESS_ORIGIN } from '@/utils/const.js' 9import { sgmReportCustom } from '@/utils/log' 10 11import InputRight from '…./components/InputRight.vue'
命名(世上问题千千万,问题命名占一半)
-
不用宽泛的模块或文件名
-
没有拼写错误,单复数也应该正确
-
符合规范:
- 文件名kebab-case
- 类名PascalCase
- 文件作用域内 常量、变量、函数 camelCase
- private 是否采用下划线,应保持一致
bad case:
1// 无意义命名 2let array = [1, 2, 3, 4, 5] 3let temp = false 4import Part1 from './Part1'; 5import Part2 from './Part2'; 6import Part3 from './Part3'; 7import Part4 from './Part4'; 8 9// magic number 10let point = CGPoint(x: 123, у: 456) 11 12// 硬编码 13reportEvent("ImageClickEventId")
其他
连等
1// 连等 2elm.onload = elm.onreadystatechange = resolveFn
一段重试逻辑
虽然 if 嵌套不多, 但是让人心智负担很重, 无法快速看出 count 值是多少会 false, 代码写的像面试题
1if ((data && data.eid) || count++ > 20) { 2 if (!data.eid) { 3 webLog.custom({ 4 type: 1, 5 code: 'getEidInfo-empty', 6 msg: data, 7 }) 8 } 9 clearInterval(timer) 10 resolve({ data }) 11}
reviewer 建议:
使用卫语句提前剔除负向逻辑后, 虽然代码更长, 但是更好理解了。
1if (!data.eid && count <= 20) { 2 count++ 3 return 4} 5if (!data.eid) { 6 webLog.custom({ 7 type: 1, 8 code: 'getEidInfo-empty', 9 msg: data, 10 }) 11} 12 13clearInterval(timer) 14resolve({ data })
CR落地-常见挑战
Code Review时看不出问题
参考解法:
组织集体审查讨论,提升大家审查能力,在代码质量上达成共识
代码审查方式对比
| 分类方式 | 审查方法 | 优点 | 缺点 |
|---|---|---|---|
| 审查方式 | 线下异步审查 | 时间灵活 | 实时性差 |
| 干扰小 | |||
| 易于存档 | |||
| 面对面审查 | 实时性好 | 对审查者干扰大 | |
| 审查人数 | 一对一审查 | 安排容易 | 多人同时线下审查容易出现重复工作 |
| 干扰小 | |||
| 团队集体审查 | 讨论深入,审查细致 | 人数多时,容易效率低下 | |
| 审查范围 | 增量审查 | 聚焦重点效率高 | |
| 全量审查 | 系统性 | 工作量大,不能常常进行 | |
| 专项集中检查 | |||
| 审查时机 | 代码入库前门禁检查 | 对于把关入库代码的质量,效果很好 | 太过死板的话,会降低代码入库效率 |
| 代码入库前的设计时检查 | 最早发现问题,从而大大降低问题修复成本; | ||
| 代码作者抵触情绪小; | |||
| 有效的架构讨论工具; | |||
| 代码入库后检查 | 既不阻塞代码入库,又可以对提交的代码进行审查 | 有问题代码错过检查而上线的风险 |
担心冲突、害怕出错
比如 A 看出了不少问题,但是发现代码作者非常不耐烦,导致 A 不敢把看到的所有问题都提出来。
参考解法:
冲突发生
- 解决冲突
✅ Leader协助沟通及仲裁
✅ 协商达成共识
✅ 寻求第三人评估
✅ 组内讨论
❌置若罔闻
❌放任自由
预防冲突
- 沟通技巧
尽量疑向、不要太肯定
✅如果采用......是否会更合适?
❌这里应该......
✅是否考虑过......这样的方案?
❌......方案肯定更好
✅这个地方似乎会影响滚动性能?
❌这样写肯定会影响滚动性能 - 发现问题,尽量提供建议
✅......这样会更简洁
❌你这代码复杂度太高了
✅根据......项目规范,这里应该这样...
❌你这代码不符合项目规范
特别注意
不要吝啬称赞👍👍👍
没必要力求完美👍👍👍
没有时间 CR
浇花很有意义, 但是先把火灭了
首先区分紧急与真正紧急:
| 不是真正紧急情况 | 真正的紧急情况 |
|---|---|
| 内部计划Deadline | 显著影响用户生产环境的Bugfix |
| 长时间开发后需要进行的必要合入日常Bugfix | 导致整个团队工作暂停 |
| 回滚导致的测试失败或构建破坏 | 处理紧迫的法律问题 |
| 不跟版本会导致重大损失的Deadline | |
| 安全漏洞等 |
CR 时间不够
工作量评估要包含 CR 时间
推荐预留 20% 的 CR 时间
权衡:
关注设计大于具体实现;
保证不出线上问题为底线;
管理好交付里程碑
越大的里程碑越容易产生大型CL,会拖慢CR速度
建议数据:400行/小时(样式、dom行可适当剔除)
建议里程碑交付周期:1周,最长2周
真正紧急情况
同步CR
写完代码当面或电话同步Review
并行CR
结对编程
紧急情况后门
google 的做法:自己是Owner,写“To be reviewed”可绕过审查
历史包袱过重
通解: 卡住增量,治理存量
CR的目的是让每一次合并都在改善代码仓库的水平
其他-提升团队工程素养
✓ 设计模式: 掌握24种设计模式
✓ 设计原则: 掌握SOLID原则(单一职责、开闭原则、里氏替换、接口隔离、依赖反转)
✓ 方法学: 了解Devops,极限编程,Scrum,精益,看板,瀑布,结构化分析,结构化设计
✓ 实践: 实践测试驱动开发,面向对象设计,结构化编程,函数式编程,持续集成,结对编程
书籍推荐
《编程原则( understanding software)》
《重构:改善既有代码的设计》
《编写可读代码的艺术》
《代码大全》
《敏捷软件开发》
《架构整洁之道》
《代码整洁之道》
