作者|原深剑问题背景databatcher这个名字是我暂时放弃的名字,源于我指导的客户团队的开发人员在核心系统中要解决的一个实际业务当时的问题——Oracle的数据库删除一次只支持1000条记录。这个问题更确切的说是因为Oracle对如下SQL语句的支持约束:deletefromt_tablewhereidin(ids)问题就出在这个whereidin...,后面传入的set参数ids支持最大1000条.但是实际业务场景中有1000多条数据,所以需要进行批处理。针对这个问题,我暂且不探讨SQL机制本身的合理性[1]。在这篇文章中,我想借此机会谈谈如何使用TDD来完成数据批处理的设计和开发。需求分解就是基于这样的问题。我的习惯是先把问题分解,这是TDD之前的一个关键动作——Tasking。我赶紧做了一个Tasking:(1)不是1000的整数倍,小于1000(2)不是1000的整数倍(3)不是1000的整数倍,大于1000根据Tasking结果,实例化上面的需求场景,在实例化过程中,边界值是我考虑的重点:(1)不是1000的整数倍,小于10000369(2)100010002000的整数倍(3)不是整数倍1000,1000多条记录,1369条记录,2222条记录。上面代码最初设计的for循环中做了两件事。在拦截不同范围的recordsId的同时,调用了CustomCustomerRiskScoreRepository来做数据库操作,批处理逻辑和存储混合在一起的过程设计,直截了当的优点,当然也让deleteByRecordIds()承担了太多的责任。新的设计是基于我对软件设计的浅薄理解。我认为将批处理逻辑与Repository数据存储逻辑分开会更优雅。支持我的主要原因是:数据存储逻辑更纯粹,只使用关心数据的CRUD。重要提示:批处理逻辑可以轻松地自动化以进行测试。批处理逻辑独立,便于复用和维护。基于以上原因,我在原来过程式编程的基础上引入了一些OO概念,进行了对象建模,比如抽象出一个databatcher。我将对象建模过程视为TDD之前的一些简单且必要的设计。TDD不提倡在开始之前不做任何设计,而是提倡做一些简单而必要的程序界面设计——非教条主义通过对象建模分析,我设计了两个简单的对象,一个是BatchDivider,一个是BatchDivider。就是Range,UML如下:BatchDivider接收到一个total,然后可以返回一个包含开始和结束范围的集合,比如接收到1369,返回集合[Range(0,1000),(1000,1369)],起止信息对象我用Range来表示。之前的设计就是这么干的,我也没有在细节上花太多时间纠结,因为现在的设计已经足够我上手了。后续如发现不完善,将交由后续司机进行改造。至此,我已经将需求进行了分解和实例化,然后进行了简单的程序设计。跑前热身后,打算以TDD跑姿小跑。前面的实例化我写的比较简单。由于习惯用Given-When-Then的方式来描述,所以结合现在的编程设计,对之前的简单实例化进行了提炼:1000的非整数倍,小于1000项(1)0项给定的数据是batched为0项批处理时那么批处理结果为空集,[](2)369项给定待批处理的数据为369项批处理时则批处理结果包括1个范围的集合,的整数倍[(0,369)]1000(1)1000条给定的待批处理数据为1000条批处理时则批处理结果包含一组Range,[(0,1000)](2)2000条给定的数据待批处理的是2000件,批处理时Then批处理结果包含一组2个Ranges,[(0,1000),(1000,2000)]不是1000的整数倍,大于1000(1)1369给定的数据待批处理的是1369分批处理时那么批处理结果包含一组2个范围,[(0,1000),(1000,1369)](2)2222给定为批处理数据为2222条,当进行批处理时,批处理结果包含一组3个Ranges,[(0,1000),(1000,2000),(2000,2222)]第一次测试给定待批处理数据为0时batchprocessing那么batch结果就是空集合,[]写测试代码的时候,有一种方法是从结果驱动向前,比如我会先写assert,然后逼着我按照思路一步步往前走意图给变量命名,给变量赋值。下面是我写的第一个测试用例:你现在看到的5~11行代码,我的写顺序是11-->5。这只是我个人的习惯,因为受以尾为首的观念影响和面向意图的编程,养成了这样的习惯。KentBeck在《测试驱动开发》中也提到过这种方式。根据逆向驱动方式写的代码,我在测试代码中发现了一些不需要的临时变量,但我并不急于重构,我会专注于让这个测试通过,至少代码的可读性是很好现在很好。很快,借助IDE的快捷键,我写了实现代码通过了测试:我用伪实现快速通过了测试,然后用Inline的方法清除了不需要的临时变量totalItemSum和batchCount在测试代??码中。第二个testGiven,待批处理的数据是369批处理的时候那么batchresults包含一组Range,[(0,369)]第二个test命名为given_item_count_below_1000,这里我没有使用具体的例子369,你可能将其命名为given_item_count_is_369。个人习惯命名抽象的场景,因为我觉得这样可以更直观的提供业务场景的信息。在第二次测试中,我直接在ranges中添加了一个Range对象,并且保留了之前测试的判断,很快就通过了测试:ofRange,[(0,1000)]这个测试运行后直接通过,一切正常,运气还不错。第四个测试给定,要批处理的数据是2000批处理时,那么批处理结果包含一组2个Ranges,[(0,1000),(1000,2000)]为了通过这个测试,我写了下面的Functional代码,为了节省篇幅,我只摘录了核心的batchProcess方法:很好,我通过了第四次测试,我发现1000这个数字在这个方法中出现了好几次,这是一个神奇的数字,而且反复出现,在我看来,这是一个有两种味道的恶魔,所以我提取了一个常量ONE_BATCH_SIZE。就在我准备写下一个测试的时候,我运行了所有之前的测试,发现第二个测试挂了——我破坏了原来的功能。我回去检查了batchProcess方法的实现。原来是totalItemCount/ONE_BATCH_SIZE操作出了问题。当totalItemCount=369时,整除结果为0。为了定位问题,根据之前的开发经验,尝试了几种Math函数的方法,最终找到了Math.ceil(double)方法:
