最近团队经常出现开发周期超长的故事卡,有的甚至需要超过一个月。在进行复盘时,团队表示他们也很无奈,故事卡太复杂了,没有办法进行拆分,只能硬着头皮开发。这样压力就给到了负责这张卡的开发和测试的同学,结果是必须要成功的,但过程是绝对惨烈的。我们就真的没有任何办法来处理这种情况了么?
什么是复杂的故事卡?
复杂是相对的,我们觉得复杂的事情在别人看来也许很简单。反过来理解,如果团队对某张故事卡的工作量无法达成一致,那么这张故事卡很可能是复杂的。同时,这也表明团队内部存在谷仓效应,只有少数人掌握了大量的知识。究其原因,有的是因为团队缺少内部分享,有的是因为团队没有定期轮换结对编程,还有的是因为团队成员被长期分配单一类型的工作。
复杂也是绝对的,无法在一个迭代内完成的故事卡就是复杂的故事卡,这也是经常被忽略的一种情况。以笔者所在的团队为例,我们通常使用斐波那契数列对故事卡进行估点。当故事点大于或等于5时,故事卡往往无法在一个迭代内完成。究其原因,有的是因为故事卡中缺少上下文,有的是因为团队缺少相关的业务或技术知识,还有的纯粹是因为故事卡包含的内容太多了。
为什么要拆分复杂的故事卡?
参考我们在如何处理未知问题中所讨论的内容,拆分复杂的故事卡是一种降低损失的措施,它既可以降低我们在每张故事卡上的时间和人力投入,也可以缩短故事卡中未知问题的持续时间(至多一个迭代)。从敏捷的角度来讲,拆分复杂的故事卡可以缩短反馈时间,让我们可以及时的对下个迭代进行调整,避免在错误的方向上越走越远。
以开车为例,我们会根据观察到路况来调整汽车的速度,两次观察之间的时间越短,我们收到的反馈时间就越短,留给我们调整汽车速度的机会就越多,我们也就越安全。很明显,我们不可能在看到前方没车后,直接闭上眼睛10分钟,一直保持同样的速度行驶,因为我们无法预测这段时间内的路况变化,按照同样的速度行驶无疑是危险的。我们当然也不可能将观察的时间间隔缩短到无限小,因为眨眼,神经传输,肌肉反应和信息处理等都需要时间。所以,最佳的观察时间间隔是一个经验值,以这个间隔观察路况基本不会出问题。
这里,迭代长度对应的就是观察时间间隔,通常是两周。如果故事卡无法在一个迭内完成,那么其对应的迭代长度就会增加,也就意味着我们要闭着眼多开一会儿车,风险也就增加了。
复杂的故事卡一定可以被拆分么?
这里笔者想引用还原论的思想:
复杂的系统、事务、现象可以通过将其化解、拆解各部分的方法来加以理解和描述。
虽然我们已经知道还原论有其局限性,例如无法描述复杂系统的涌现性质,但我们仍可以将它用于软件开发,因为软件系统在大多数情况下属于简单系统,不具有涌现性质。所以,笔者认为只要我们具备足够的业务和技术知识,复杂的故事卡是一定可以被拆分的。
那故事卡可以被无限拆分么?笔者认为是不行的,因为我们对故事卡的最低要求是
- 可集成性,也就是可以通过编译,自动化测试,打包等流水线步骤。
- 可测试性,也就是可以提供操作入口或存在可被观察到的数据。
- 可部署性,也就是可以部署到生产环境,不影响现有功能或可被用户接受。
我们显然不可能为我们输入的每行代码都单独创建一张故事卡。
那满足最低要求的故事卡是不是就不能继续拆分了?不是的,随着我们对业务和技术的认知程度越来越深,我们是有可能识别出更小粒度的满足最低要求的故事卡的。典型的例子就是史诗级故事卡,特性故事卡,用户故事故事卡和任务故事卡的拆分过程。
在满足最低要求的基础上,我们只要保证拆分出的故事卡是清楚明白且能够在一个迭代内完成的就足够了。
如何拆分复杂的故事卡?
能说清楚的
对于能说清楚的故事卡,我们要做的就是从业务或技术的角度对其进行拆分。
业务拆分
如果我们能够按照MECE原则以Given-When-Then的形式说出故事卡的验收标准,且团队所有人都能够理解,那么我们可以将验收标准拆分到不同的故事卡。
例如,我们的原始需求是,任务调度器需要能够计算出重复任务的下次启动时间,团队和BA讨论出的验收标准为
Given: 当前任务是一个重复任务
When: 重复类型是按天重复
Then: 任务的下次启动日期为第二天
And:启动时间与任务设定的第一次启动时间相同
Given: 当前任务是一个重复任务
When: 重复类型是按工作日重复
Then: 任务的下次启动日期为下一个工作日
And:启动时间与任务设定的第一次启动时间相同
Given: 当前任务是一个重复任务
When: 重复类型是按周重复
Then: 任务的下次启动日期为下一周的同一天
And:启动时间与任务设定的第一次启动时间相同
Given: 当前任务是一个重复任务
When: 重复类型是按月重复
Then: 任务的下次启动日期为下一月的同一天
And:启动时间与任务设定的第一次启动时间相同
可以看到,虽然每条验收标准都有需要进一步澄清的地方,但团队成员基本都能理解其内容,那么,我们就可以为每条验收标准都单独创建一张故事卡。
另外,故事卡的验收标准必须是可证伪的的,否则我们无法证明故事卡是否被正确的实现了。例如,任务的下次启动日期应该是正确的启动日期,就是一个不可被证伪的验收标准,我们怎么知道什么是正确的启动日期呢?
技术拆分
如果从业务角度拆分后的故事卡仍然很复杂,我们可以进一步从技术角度进行拆分。首先,我们需要能够依托进程间和进程内架构说出故事卡的实现步骤,且团队所有人都能够理解。然后,我们可以使用依赖倒置原则对实现步骤进行解耦,从而将其拆分到不同的故事卡。
这里我们主要讨论有直接依赖关系的步骤,没有直接依赖关系的步骤可以直接被拆分到不同的故事卡,或者通过有直接依赖关系的步骤的拆分被间接拆分到不同的故事卡。
以步骤A和步骤B为例,当步骤A直接依赖于步骤B时,拆分的过程如下:
- 定义步骤A和步骤B之间的接口协议。例如,进程间需要定义API Schema, Event Schema等,进程内需要定义Domain Model, Domain Service, Port等。
- 创建功能开关,以保证拆分后的故事卡具有可部署性
- 为步骤B创建单独的故事卡,按协议实现接口并隐藏在功能开关之后。例如,进程间需要实现RESTful API, Event Generator等,进程内需要实现Application Service, Adapter等。
- 为步骤A创建单独的故事卡,按协议使用简单的步骤B'替换步骤B以保证其具有可测试性,并隐藏在功能开关之后。例如,进程间可以使用Wiremock, Pact, LocalStack等模拟步骤B,进程内可以通过接口继承来实现简单的的步骤B‘。
- 用步骤B替换步骤B'
- 清理功能开关
其中,第5条的内容可能在单独的故事卡中完成,也可能在步骤B的故事卡中完成。这取决于步骤A和步骤B的故事卡是否是并行开发,如果是并行开发,就需要创建单独的故事卡。
说不清楚的
如果我们无法说清楚故事卡的验收标准或实现步骤,那么团队可能存在以下问题:
- 缺乏业务知识。例如,领域知识,上下文,业务规则等。
- 缺乏技术知识。例如,代码,架构,部署环境等。
- 缺乏跨功能需求知识。例如,安全性,可靠性,可扩展性等。
这说明团队还没有做好开发这张故事卡的准备,我们可以创建一张有时间限制的Spike故事卡来学习相关知识,然后在下个迭代重新对该故事卡进行评估,如此循环,直到团队有能力把故事卡说清楚为止。
总结
面对复杂的故事卡,有的团队宁愿把压力给到负责开发和测试的同学,也不愿花费更多的精力来对其进行拆分。因为他们觉得拆分复杂故事卡的工作量加上拆分后故事卡的工作量要大于不做拆分的工作量。笔者觉得这种算法是错误的,因为他们默认了复杂的故事卡不会存在错误,只需要考虑如何快速的实现它就好了。但是,复杂的故事卡是极易出错的,我们做拆分就是为了应对这些错误,是在考虑如何做正确的事情,事情本身如果是错误的,做的越快,损失就越大。
拆分复杂的故事卡是我们应对未知问题的重要手段,也是提高团队开发体验的有效措施。我们无法或不愿对复杂的故事卡进行拆分往往代表着我们缺乏某种知识,而知识不是免费的,我们需要投入时间和精力来获取这些知识,否则,我们将无法培养团队持续提升的能力。