本文最初发表在 iiSM.org 网站,经原作者 Gandalf Hudlow 授权,InfoQ 中文站翻译并分享。
许多组织都向新软件项目施加压力,要求它们“完成”,因为整个组织都面临着来自高层的压力,必须在高层任意划定的截止日期前完成。副总裁、项目经理、产品经理的奖金和聘用,都是要看他们在截止日期之前交付软件的能力如何而定。这一错误做法带来的结果就是,部署到客户手中的软件版本 1.0,充满了混乱。这种模式已经重复了一次又一次,以至于消费者都会说“用软件别用版本 1.0 的!”、“还是等等补丁包再说吧!”
这些组织没有意识到的是,所有的软件变更都可以划分成三个组成部分: 价值、填充和混乱。混乱会破坏价值,而填充只是没人想要的功能。当对代码施加截止日期的压力时,消除混乱所需的工作首先会被砍掉。混乱会破坏价值。不信?请你扪心自问,上一次你手机上的一款新应用出现混乱时,你做了什么。那款你卸载后就忘掉的应用,就是刚刚被混乱破坏的新价值尝试。
伦敦希斯路机场(Heathrow Airport)吸取了这个教训。当时他们大张旗鼓地举办 T5 候机楼启用仪式时,连女王都莅临了!随着办理登记手续的延误,混乱的局面开始出现了。接着出现行李堆积和传动带堵塞的情况,局面更加混乱了。到了下午,英国航空(British Airways) 已经完全放弃托运行李的努力,乘客们被强行推上已经晚点的航班,并含糊其辞地承诺他们的行李将会一起抵达目的地。新闻报道里充斥着堆积如山的行李照片。没有人感到高兴,尤其是女王。
由时间驱动的组织造成的 T5 候机楼混乱就是一个典型的灾难。上面下达的命令很清楚: 只要完成它就行 !要避开所有不利于按期交付的一切阻碍:所有的建议、所有的专业知识、所有的发现努力!
下面是由于时间压力而导致交付延迟的各种混乱的总结。
让我们通过一些代码示例来进一步了解破坏软件产品价值的混乱类型。
车辆在现场加速失控。发生 8 起死亡事故。其中一起撞车事故,被发现有长达 100 米的紧急刹车的痕迹,一直通往混凝土护栏。怀疑可能是由于油门被卡所致。
迫于时间压力下完成的代码
char bluetoothId[30];
int acceleratorAngle = 0;
void processAccelerator() {
if (acceleratorAngle > 0) {
engine.throttle(
acceleratorAngle);
}
}
void processBlueToothOnline(
char *deviceId) {
strcpy(bluetoothId, deviceId);
}
精心编写的代码
//Fixed issue with blue tooth
//id overflowing and corrupting
//acceleratorAngle...
//QA says wheeeeee!
char bluetoothId[30];
int acceleratorHash;
int acceleratorAngle = 0;
void processAccelerator() {
if (!validateHash(
acceleratorHash,
acceleratorAngle)) {
report_critical();
abort();
return;
}
if (acceleratorAngle > 0) {
engine.throttle(
acceleratorAngle);
}
}
void processBlueToothOnline(
char *deviceId) {
memset(bluetoothId, 0,
sizeof(bluetoothId));
strncpy(bluetoothId, deviceId,
sizeof(bluetoothId)-1);
}
评点 :你是否曾经面临过这样的压力,被告知 只需完成即可 ?你交付的代码真的完成了吗?催促工程师按截止日期交付的组织往往很愿意按期交付。这是因为他们的重点在于按期交付,而不是 一切就绪后 再发布有价值的、没有混乱的产品。控制汽油流向汽车发动机的代码需要通过各种方式进行反复锤炼,以确保永不失效。而时间驱动型的组织很少会加上这样的要求:“锤炼关键代码,找到破坏价值的缺陷!”在这种情况下,唯一的可取之处是, 值得信赖的工程师在发现混乱时,往往会做一次性的尝试,而不是被迫在周五之前进行检查。
客户报告间歇性转账金额非常大。转账本应是 1~2 美元,但结果却超过了 2 万美元!
迫于时间压力下完成的代码
//Code was ported from single
//threaded embedded device to
//multi-threaded env
//ToBcd is being called from
//multiple threads!
unsigned int gTemp;
int gShift;
unsigned int ToBcd(
unsigned short amount) {
gTemp = 0;
gShift = 0;
while(amount > 0) {
gTemp |= (amount%10)
<< (gShift++<<2);
amount /=10;
}
return gTemp;
}
精心编写的代码
//during Representative Load
//testing - no more
//unnecessary globals!
unsigned int ToBcd(
unsigned short amount) {
unsigned int gTemp = 0;
unsigned int gShift = 0;
while(amount > 0) {
gTemp |= (amount%10)
<< (gShift++<<2);
amount /=10;
}
return gTemp;
}
评点 :你有没有听过这样的说法,将现有代码移植到新平台应该很容易,只需简单地重新编译和部署即可?组织向工程师施压,迫使他们走捷径,例如将深度嵌入的代码移植到新的平台和编码范式,而在日程安排中却没有列入用 典型负载来追捕意外混乱的计划?
后端出现间歇性的崩溃,导致事务取消和时间损失。
迫于时间压力下完成的代码
lib3rdPartyUnstable.doSomethingGood()
精心编写的代码
lib3rdParty.doSomethingGood()
评点 :如果你是在任意的时间压力之下,你觉得有多大可能确保方案在负载下保持稳定?你是否应该牺牲你的周末时间去做公司可能会抱怨的工作,因为他们只是认为你这是在拖延交付日期?在任意时间压力下,工程师更有可能跳过或通过快乐路径进行负载和性能测试,而这些测试正是发现未知混乱根源所需的,比如上面那个第三方依赖项,就需要更换或升级。
最高出价有时会显示出疯狂的巨量金额。
迫于时间压力下完成的代码
void setHighestBid(long bidCents) {
if (bidCents > highestBid) {
highestBid = bidCents
}
}
精心编写的代码
/*Note: This needed to be
synchronised to
avoid corrupting max bid*/
synchronized void setHighestBid(
long bidCents) {
if (bidCents > highestBid) {
highestBid = bidCents
}
}
评点 :你是否有过这样的经历:你的软件快速通过了 QA 测试,但在进入生产环境时就崩溃了?捕获竞争危害的一个好方法是在典型负载下运行软件,同时构建并分析指示软件健康状况的指标。但这听起来像是一个以时间压力为导向的组织想要的东西吗?以我的经验来看,不是这样的!
一天结束的工作流程需要几个小时才能完成,而实际上,它们最多只需几分钟即可。
迫于时间压力下完成的代码
背景: 在整个后端代码中,应用了一种重复的架构模式,在该模式中,对象将持久化,并每次从数据库中获取一个子对象。这将会导致响应时间非常慢,但如果负载很轻的话,效果尚可。
/*Person object has several dirty
sub-objects that are persisted
one-by-one*/
person.persist();
精心编写的代码
/*Note: Persist framework
function refactored to batch
SQL updates*/
person.persist();
评点 :当你对项目的架构提出质疑时,有没有被人“嘘”过?如果你推送复杂测试呢?负载测试是众所周知的软件项目按时交付的祸根,因为它往往会使像上面例子一样的架构问题暴露出来。如果团队有足够的压力来满足这个截止日期,他们就会很高兴地进行负载测试,并将其交付,而客户将会发现软件的性能问题。然而,许多组织在进行负载测试时,并没有健康度量的指标,以使客户满意负载测试的执行情况。而如果没有度量指标显示一定负载下暴露出的不良行为,那么软件通常会顺利地通过,即使软件在经常被忽略的诊断日志中出现了大量有关问题的信息。
人们有时不付款就把产品买到手里,这简直是客服的噩梦!
迫于时间压力下完成的代码
//todo check for errors
database.recordPurchase()
paymentGateway.startPayment()
paymentGateway.completePayment()
精心编写的代码
database.startPurchase()
try {
paymentGateway.startPayment()
} catch(Exception e) {
log.error(e)
database.recordError()
return FailedStartPayment(e)
}
try {
paymentGateway.completePayment()
} catch(Exception e) {
altPayLog.recordError()
log.error(e)
database.recordPError()
return FailedCompletePayment(e)
评点 :你是否曾经在处理复杂的错误情况时被经理打断,问你能否按时交付?这种时间压力是否激励你进行一些出色的错误处理?时间压力往往会让人们不再考虑软件失败时会发生什么情况;只愿意在快乐路径上进行编码。换句话说,错误处理被抛在一边,只顾满足周五的检查截止日期。
当然,时间压力并不会导致所有的混乱被忽视。下面我列出的内容,是往往会被修复的。但是,这些被修复的内容给了组织一种虚假的安全感。
将高价值的软件项目推到一个任意日期交付,最终的结果将是,产品混乱不堪,公司在市场的声誉因此受损。因为有偿付能力的公司都有一个现有的、稳定的现金牛投资产品组合,因此,只要公司还有资金,并且愿意尝试,就会重复犯下这个错误。这就导致我们目前的情况,即开发新软件的努力以失败而著称。而失败往往是最好的结果,最坏的结果之一是产品获得足够多的采用率,使公司能够保持盈亏平衡,公司被迫维持其产品运行多年,以避免被起诉违约。
那么,为什么我们一再看到那么多老牌大公司在创造新颖的、有价值的、客户喜爱的产品方面完全失败呢?事实证明,发现新事物所需要的技能与支持和部署已经存在的事物所需要的技能是不同的。将现有的现金牛产品技术用于新产品开发的公司注定要失败。现金牛主要需要部署工作,而部署工作有利于预测性规划,因为可以可靠地计算出一个交付日期。当有精明的客户参与其中,能够对团队的增量进行良好的反馈时,敏捷开发才会有帮助。然而,大多数公司不会让工程团队与客户一起迭代,而且由于大多数成熟的公司都是严重依赖交付日期,所以他们最终将敏捷开发变成了日期 Scrum 模式,由内部的产品负责人来指导构建。
什么是日期 Scrum? 这种研发模式的主要区别在于,每天的 Scrum 会议是一个状态和风险管理会议,在这个会议上,团队要反复地重新关注于按特定日期交付。这种模式不利于发现新的、有价值的软件。
创造人们需要和想要的新软件产品是不大可能在一个庞大的工程中全部铺排在甘特图上的。它们之间的差异,可以想象成蚂蚁如何寻找饼干:那些还没找到饼干的蚂蚁会如何四处游荡寻找,而我们都见到,有序排队的蚂蚁找到了饼干!那一队队游荡的蚂蚁正在并行地进行价值尝试,每只蚂蚁的成本相对较低。现在想象一下,如果游荡的蚂蚁群中有一个强有力的领导者,带领它们朝着一个方向前进会怎么样?当你将现金牛技术应用于新产品开发上,你就会得到这样的结果。更糟糕的是,根据我的经验,这些强大的领导者实际上并不知道新的价值在何处。如果一个团队希望创造一些真正新颖的事物,那么根据定义,这一事物的表现形式是未知的,否则它就不算是新事物!当多个团队成员进行有组织的、并行的价值尝试时,发现未知的工作效果会更好,这些尝试可以扩大对以下内容的了解:
总之,时间压力会放大新的、高价值的软件产品尝试中的混乱,而要完全摧毁价值并不需要太多的混乱。除了放大混乱以外,时间压力也会抑制发现不好的产品 / 市场契合度,因为团队盲目地、高度专注于那个截止日期。新产品就存在于市场的黑暗空间里。要找到这些新产品,就需要在黑暗的空间里进行无数次低成本的价值尝试,看看有什么可以击中。当这些价值尝试准备好了,并且没有混乱的时候,再去激发这些价值尝试,而不是在达到某个任意的日期的时候!
作者介绍:
具有 20 多年的软件开发经验,国际软件管理研究所(International Institute of Software Management,iiSM.org)主任兼撰稿人。
原文链接:
https://iism.org/article/the-value-destroying-effect-of-arbitrary-date-pressure-on-code-52