作者 | 曾雪松
软件工程里一个重要的指标就是“可用的软件”,敏捷宣言里也同样告诉我们“工作的软件高于详尽的文档”,那“可用的软件”、“工作的软件”意味着什么呢?在我的理解里,可以经历用户 “千锤百炼”的软件就是一个“可用的软件”。曾经听到过这样的说法:“一个有Bug的软件怎么能叫软件呢?”虽然这话在我们业内人士听起来有些可笑,但是这就是使用软件用户最真实的需求。所以如何在提高代码质量,最大程度地减少软件中的Bug同时,平衡软件迭代速度与交付效率是我今天想跟大家讨论的问题。

我有幸在两种完全不同风格的项目上进行过交付,让我们且称之为项目A和项目B。
项目A是一个客户为主导的巨大项目组,管理为明确纵向层级管理,横向开发团队来自于不同的供应商,并且采用瀑布式开发,由另一个事业部进行测试反馈,部门墙极其严重。

项目B则是一个由业务主导,每个敏捷团队有对应相关的业务领域,客户则是和供应商共同组成一个个敏捷团队,共同达成业务目标。

好了,完成了简单的背景介绍,我就要来说说下面的故事了。
首先,假设我们所需要达到的目标是由一个个大大小小功能(颜色模组)组成一个完整的软件,为了达到我们的交付目标,我们需要将每个功能进行开发,测试,将功能模块进行累加,最终获得一个完整而达标的软件。

同时两个项目都使用了大致相同的开发流程,为了保证质量,项目中都有基础的代码审计,CI/CD,应用测试,用户测试,等基本质量保证,软件开发的基础流程如下图:

在这种基础流程都相近的情况下,每个环节在不同架构下执行的的方式却有巨大的差异。
在讨论项目A的流程前,让我们先看看我们熟知的敏捷开发是怎么保证质量的:
项目b的每一个小敏捷团队将业务需求从路径图(Roadmap)拆解下来,落到各大的业务功能的Epic中,再拆解成具有小的业务价值的用户故事,最后再落到每个具有开发意义的任务,注意,这里提的一直是业务价值,我们还没有开始讨论如何进入开发。
Epic 更多是独立且较大的目标,用于我们识别在关键时间点需要实现的大型业务目标。而用户故事则是一个简短的描述、一个用于表达用户或客户的需求的角色和一个用于描述需求的价值或期望结果的价值陈述,在用户故事中比较关键描述是关于此价值点的“静态““动态”与“非常态“,静态更多的是对价值点的描述,在To C中往往是静态设计图(UI)的描述,动态则是交互,系统间的交互或者功能的用户旅程(UX),而非常态则是描述系统在错误或者误差情况下的表现,以确保当前的价值点在绝大多数情况下得以运行(AC)。最终用户故事将被团队中的技术领导拆解成可以单独执行的开发任务,最终没个独立的开发任务可以由不同的开发人员执行。
在一个大型的价值目标被拆解成了Epic->用户故事->开发任务的过程中需要全团队的多轮确认,多轮确认确保所有人达成统一共识 ,在最大的程度上解决沟通差带来的不确定性。最终需要通过迭代计划会议在团队内部对价值达成共识后,才会进行项目开发。

进入开发任务后每个阶段,参考下图:

我们可以看到4重质量保证:
再这样一轮一轮的开发任务到用户故事的价值交付后,又组成了一个Epic价值交付,最终通过Bug Bash的方式最后确认价值以达到交付标准,我们可以上线整个Epic用于用户的检验。
总结一下敏捷开发的特点:
用图来表示最终内建的结果,在最终快要上线时,经过团队内质量把控后仅与实际有极少差距,仅需要在日常使用中进行基础运维即可达到我们的价值目标:

这时候让我们再来看项目A,系统被产品部门完成设计后,交予开发部门进行任务划分,每个开发团队承担不同的功能开发任务,每个功能点再由单独开发人员进行开发并自行测试(本地),最后由客户方进行功能验收后(功能展示+代码审核),代码合入主线进行转测。
说到这儿,举个例子,产品部门提供了本次需要交付的20个功能的设计图,开发团队把设计图分给交付团队(大多由供应商组成),团队成员小王负责对其中一张设计图(类似于一个Epic)的功能进行开发,开发完成后开验收会议,对代码和功能进行审核验证,进入测试流程。所以开发阶段归纳下来的话,如图:

这样乍一看确实没有什么问题,开发流程中的各种实践也在做,那这种项目研发模式问题出在哪儿呢?这个时候我们看项目A的关键质量保证动作:测试。
项目A的测试步骤:

先抛结论,在测试阶段,80%时间用于确定问题+定位问题(标红)。所以我们可以着重讨论一下这两个阶段。
确认问题:在确认问题阶段, 往往由测试组发起,通过层层追溯,可以追溯到开发人员(也就是小王),跟小王确认表现层的“静态”/“动态”/“非常态”问题后,测试顺利成章地建立一个问题工单,并分配给小王,宣告此单插在了小王头上,小王需要修正再找测试回归。乍一看又没什么问题,是个好流程,但是执行起来此流程会出现:
举例:(一个电话拉会)“小王,我觉得这个页面帧数好低,你要优化一下。”“啊???”(此处省略battle的10分钟)终于电话给了产品,产品一句话:“是帧数有点低啊!小王,这你得改”“…”
举例:测试打电话给小王,小王说“这不是我的问题,你找xx团队的小李 ”,小李接上电话,“这是你小王开发的啊”..(再次省略battle时间)最终问题很有可能上升到客户方确定问题边界,这样1个小时就过去了。
定位问题: 定位问题同样占据了开发人员的大量时间,总体来说:
后续的修改流程往往较为顺利,但是也会出现一个工单反复无法通过回归的问题,这毕竟是少数,也不是我们主要探讨的范畴。项目在强流程驱动下最终的结果就是:
所有人每天都在加班,所有人每天都在增加流程以确保质量,所有人都很痛苦,当然这里包括小王。
用图来表示开发结束后的状态,空隙区域代表不确定问题,空隙部分需要测试->开发->产品逆流程更改

说了这么多细节,我想现在跳出来问“为什么会出现这样的问题?”这个问题我也想留个大家做一点思考,我做了一些简单而又主观的总结,放在这里:
看完了项目A和项目B的整体, 我们最后再来聊聊效率,我们发现,在同等的质量要求下,敏捷效率反而高很多,在流程更短的情况下却交付出了同样质量很高的产品,最后我们通过对比总结一下,为什么敏捷在保证质量的同时还能有更高的效率?


我们暂且停在这儿,我要引用SAFe中的一张图来结束我今天的阐述,也在用实例回答:“为什么企业要做大规模敏捷?”
我想答案是:质量高,效率快,大家都开心。
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co
我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
为什么4.1%2返回0.0999999999999996?但是4.2%2==0.2。 最佳答案 参见此处:WhatEveryProgrammerShouldKnowAboutFloating-PointArithmetic实数是无限的。计算机使用的位数有限(今天是32位、64位)。因此计算机进行的浮点运算不能代表所有的实数。0.1是这些数字之一。请注意,这不是与Ruby相关的问题,而是与所有编程语言相关的问题,因为它来自计算机表示实数的方式。 关于ruby-为什么4.1%2使用Ruby返
它不等于主线程的binding,这个toplevel作用域是什么?此作用域与主线程中的binding有何不同?>ruby-e'putsTOPLEVEL_BINDING===binding'false 最佳答案 事实是,TOPLEVEL_BINDING始终引用Binding的预定义全局实例,而Kernel#binding创建的新实例>Binding每次封装当前执行上下文。在顶层,它们都包含相同的绑定(bind),但它们不是同一个对象,您无法使用==或===测试它们的绑定(bind)相等性。putsTOPLEVEL_BINDINGput
我可以得到Infinity和NaNn=9.0/0#=>Infinityn.class#=>Floatm=0/0.0#=>NaNm.class#=>Float但是当我想直接访问Infinity或NaN时:Infinity#=>uninitializedconstantInfinity(NameError)NaN#=>uninitializedconstantNaN(NameError)什么是Infinity和NaN?它们是对象、关键字还是其他东西? 最佳答案 您看到打印为Infinity和NaN的只是Float类的两个特殊实例的字符串
如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象
关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?
我刚刚被困在这个问题上一段时间了。以这个基地为例:moduleTopclassTestendmoduleFooendend稍后,我可以通过这样做在Foo中定义扩展Test的类:moduleTopmoduleFooclassSomeTest但是,如果我尝试通过使用::指定模块来最小化缩进:moduleTop::FooclassFailure这失败了:NameError:uninitializedconstantTop::Foo::Test这是一个错误,还是仅仅是Ruby解析变量名的方式的逻辑结果? 最佳答案 Isthisabug,or