本篇,让我们尝试用科学的方法进行一些讨论,为什么工作量往往难以估计,为一个Delay的项目加人通常是下策,为什么重构有用,以及为什么好的设计至关重要。
定性分析
研发效率 R 随着代码量 C 的增长而递减
-- John Adhoc
定量分析
研发效率本身是难以度量的,但为了讨论起见,我们不妨定义几个变量,并试着加以讨论。
-
C 当前的代码总量,业界通常以行数计。
注:此处不考虑不同编程语言的差异,假设有统一的语言(PHP除外) - Ce 当前的有效代码总量,指实际产生价值的有效代码。
打个形象的比方,C 好比一袋薯片,包装完好,膨胀十足。 Ce 好比打开了包装,瞬间泄气,发现包装里一半是空气,其真正有效的薯片总量远远少于包装给你的预期。之所以严格区分, C 和 Ce,本质就在于通常人们直接见到的工作量由C 决定,那就像人月神话一文中提及的,码农的工作产出其实是非线性的,即真实的劳动价值无法用直接观测可得的C决定。
由此,我们可以定义研发效率函数 R = d(Ce) / d(C) ,即假设 Ce 是相对于 C 的函数,则 R 对应其导函数,基于我们的定性假设,R 是一个 减函数,即随着C的增加,研发效率是递减的。
- Rc 表示当前代码规模 C 下的生产效率,即在当前规模下,每增长单位代码行数,所能实际产生的有效代码行数。
举例来说,假设以10000行代码为基本生产单位。下表是一个具体的例子(注意到研发效率随代码规模增加而递减),假设 R = 10000 / (10000 + C) 的情况(这是一个假设,实际中,也较难衡量R函数的实际曲线,在此我们使用一个双曲线函数来估计,从笔者的经验来看,这不妨是一种可能合理的近似):
C | Ce | 当前研发效率:Rc | 每再增加10000行代码能获得的有效代码 |
---|---|---|---|
0 | 0 | 10000 / (10000 + 0) = 1.0 | 10000 * 1.0 = 10000 |
10000 | 10000 | 10000 / (10000 + 10000) = 0.5 | 10000 * 0.5 = 5000 |
20000 | 15000 | 10000 / (10000 + 20000) = 0.33 | 10000 * 0.33 = 3300 |
30000 | 18300 | 10000 / (10000 + 30000) = 0.25 | 10000 * 0.25 = 2500 |
40000 | 20800 | 10000 / (10000 + 40000) = 0.2 | 10000 * 0.2 = 2000 |
提升研发效率的方法
我们换个思路,将上表的最后一列置换成:增加10000行 有效代码 所需要的代码量:
C | Ce | Rc | 在当前规模下,增加10000行 有效代码 所需要新增的代码量 |
---|---|---|---|
0 | 0 | 1.0 | 10000 |
10000 | 10000 | 0.5 | ~30000 |
40000 | 20800 | 0.25 | ~70000 |
我们注意到一个算不上惊人的事实,假设一个码农的生产力为 10000行代码 / 月,注意到在项目初期,代码规模较小的情况下,完成10000行有效代码的工作时间为 10000 / 10000 = 1 人月。 之后再增加10000行有效代码,工作时间为 30000 / 10000 = 3 人月。 突然暴增3倍。 进一步,如果在此基础上,再增加10000行有效代码, 则需要工作时间为7人月,几乎一定会导致项目Delay。
在现实生活中,我们把每10000行代码视作产品的一个新增功能。 则对应每增加一个新增功能,对应的工作量远远超出直觉(项目经理/项目经理的预期)。 造成这一困扰的主要原因只有一个,即 R 随着 C 增长而下降。
避免这一困境(项目要延期)的方案:
- 在 Ce 不变的情况下,减少当前的 C ,这相当于间接提升 R
- 提升 R
- 提升 C 的增加速度
对应现实,方案1通常意味着功能回归情况下,代码重构,清除冗余。 方案2通常意味着架构变更,模块解耦,使得开发能够并行。方案3,意味着加人,但相对而言是性价比较低的策略。显然,1,2是上策。 3是下策。如果非要选择3,建议和1,2中的某一个选择并行。
为什么重构是必要的
劳动人民在田间劳作,每年收获农作物。土地也需要恢复,肥力会下降。所以有经验的农民往往会交叉种植不同的农作物,并适当修养土地,才能保证来年更好的收成。在业界,这一现象,叫做重构。
C | Ce | 当前研发效率:Rc | 每再增加10000行代码能获得的有效代码 |
---|---|---|---|
0 | 0 | 10000 / (10000 + 0) = 1.0 | 10000 * 1.0 = 10000 |
10000 | 10000 | 10000 / (10000 + 10000) = 0.5 | 10000 * 0.5 = 5000 |
20000 | 15000 | 10000 / (10000 + 20000) = 0.33 | 10000 * 0.33 = 3300 |
30000 | 18300 | 10000 / (10000 + 30000) = 0.25 | 10000 * 0.25 = 2500 |
40000 | 20800 | 10000 / (10000 + 40000) = 0.2 | 10000 * 0.2 = 2000 |
40000 | 22800 | 0.20 | 此时选择重构,缩小代码规模至 23000,生产效率恢复到0.30 |
40000 | 22800 | 0.30 | 3000 (如果不重构,则为2000, 提升50%效率) |
上表展示了重构造成的差异,本质上是通过减少代码规模,使得研发效率函数右移(对应的是效率回到了之前小规模代码的程度)。 定量的计算,使得重构的价值可以被衡量,注意,在日常工作中,经理们往往不容易被码农的经验直接说服,通过一些看似“科学”的计算方式,可能更容易争取到空间,这背后的动机我们放到之后的章节中再讨论。
由此表也可以看出,重构的时间是非常关键的,在不同的 C 规模下,选择重构,对应的生产效率的提升百分比差异是相当大的(有兴趣的读者可以自己计算,在表格不同行选择重构意味着什么)。
最后,请注意这里讨论的假设:
- 重构总是顺利的(事实上总不是),将代码规模下降其实是一件远比增加代码困难得多的事情。
- 重构其实是需要代价的,这就是研发需要平衡的,即重构需要的工作量和带来的生产效率的比较,上表中,50%的效率提升,如果对应重构需要的工作量仅为3人天,则显然是划算的,反之,如果对应重构的工作量就需要1人月,则不如将这1人月继续投入到水深火热的劳作中去,带来的收益更大(即重构可以推迟)
为什么设计如此重要
最后强调一下设计的重要性。
设计的本质,就是降低系统协作的复杂性。
好的设计,可以使得 研发效率曲线 R 下降的更慢。这是设计好坏的有且唯一的评价指标。
-- John Adhoc
再举例来说,假设以10000行代码为基本生产单位。下表是一个具体的例子(注意到研发效率随代码规模增加而递减),假设 R = 10000 / (10000 + C / 2) 。(这次我们聘请了经验丰富的设计师,做了充分且有效的架构设计,使得研发效率下降变慢)
C | Ce | 当前研发效率:Rc | 每再增加10000行代码能获得的有效代码 |
---|---|---|---|
0 | 0 | 10000 / (10000 + 0) = 1.0 | 10000 * 1.0 = 10000 |
10000 | 10000 | 10000 / (10000 + 10000 / 2) = 0.67 | 10000 * 0.67 = 6700 |
20000 | 16700 | 10000 / (10000 + 20000 / 2) = 0.5 | 10000 * 0.5 = 5000 |
30000 | 21700 | 10000 / (10000 + 30000 / 2) = 0.4 | 10000 * 0.4 = 4000 |
40000 | 25700 | 10000 / (10000 + 40000 / 2) = 0.33 | 10000 * 0.33 = 3300 |
50000 | 29000 | 10000 / ( 10000 + 50000 / 2) = 0.28 | 10000 * 0.28 = 2800 |
60000 | 31800 | 注意到,之前完成30000行有效代码,需要花费110000行代码,现在仅需要 60000行,效率提升接近1倍 |
从上表可以看到,仅仅是修正研发效率曲线 R , 从 R = 10000 / (10000 + C) => R = 10000 / (10000 + C / 2),即使得研发效率在生产30000行有效代码时,提升接近100%(有兴趣的读者可以继续计算,越往后差异越大)。 这就是 设计的力量。
网友评论