说起TDD,大家都耳熟能详脱口而出:TDD不就是Test Drive Development,就是测试驱动开发嘛。
这么说也不能算错,TDD确实可以驱动开发,但是驱动开发这个说法太宽泛,其实TDD驱动的是两件东西:
1、驱动表征业务行为的接口设计,这块广为人知,我们就不重点阐述了。
2、TDD更有效的部分其实是驱动软件系统核心数据结构设计,带来了设计的核心收益:
a、非常简单、自然支撑整个核心业务流程
(这里的简单不等同于容易,简单是指贴近问题本质复杂度,而不仅仅是因为熟悉。)
b、使设计变得非常容易理解
核心数据结构是设计和场景契合的结合点,属于软件设计的重中之重,难中之难,也是解决问题的关键。
我们重点展开这部分内容。
可以说是TDD驱动的更应该是设计,所以TDD叫做Test Drive Design更精准。
大家一谈设计,往往会想到架构,而架构设计中大家想的更多的是层次清晰、模块分明、依赖合理。。。当然这些很重要,但是往往更加重要的是架构核心承载物:核心数据结构才是架构的灵魂。
为什么这么说呢,我们看几个小例子,体会下架构中核心数据结构在设计中的重要性。
案例1、数三角形(数数下列图形中三角形的个数)
设计的本质就是探究问题的本质复杂度,探索的方式就是简化,能把问题域无关的细节做最大程度简化同时又不丢失关键细节,才能得到一个好的设计,这确实挺有挑战,需要很好的平衡能力,但是做好了也最有价值。
回到数三角形这个问题来说,可以有各种各样的解法,可以数角,也可以数边等,但对于计算机能够理解的表达形式来说,必须要抽象出一个核心数据结构来表达这个问题,从而再进一步表达出计算机的输入输出和运算步骤等。
这个问题能定义的数据结构可以五花八门,这里给出一个我们认为比较合理的数据结构,那就是集合。
大家都是知道,集合是一种容器,其内部元素是无序的、唯一的,该问题中所谓的线就可以用线上点的集合来表达,这样就把线的抽象做了一个简化,线变成了点的集合,我们用{}表示集合;所有的线就可以变成线上点的集合的集合,利用集合定义的核心数据结构参照下式:
核心数据结构定好后,再看看如何利用核心数据结构求解:
无非就是大集合Lines中任意取三个点生成一个临时集合triangle,triangle中任取两个点,只要该任意两点共线而这三个点不共线,根据三角形的定义,就可以判断这三个点可以组成一个三角形。
而判断点是否在线上,就转换成了集合的子集运算。
有了这样一个核心数据结构后,整个问题求解的核心运算就变得非常自然,从而变得很容易理解,求解过程也变得顺理成章。
当然,核心功能完成后,后续可以再进一步考虑性能优化和逐步丰富辅助功能。
案例二、汉诺塔
汉诺塔要求把砖块从老塔全部移动到新塔,移动的条件是:移动到新塔的砖块一定要比新塔的最上层已有砖块小,或者新塔上没有砖块。
这个问题的核心数据结构是:
towers = <<<<1,2,3,4,5,6,7,8>>,<<>>,<<>>>>
砖的数组组成塔,所有塔又由塔的数组组成。其本质是个二维数组的结构。第二维数组的值表示砖的大小。
这样的数据结构就可以很简单、自然地把砖块按移动条件进行移动这个核心业务流程,直到满足退出条件而终止。
案例三、showhand
一手牌5张,其判别大小的规则如下:
Straight Flush(同花顺)>Four of a Kind(四条)>Fullhouse(三条+一对)>Flush(同花)>Straight(杂顺)>Three of a kind(三条)>Two Pairs(两对)>One Pair(一对)>Zilch(散牌)
当然,上述规则严格按照概率程度由小到大排列。
这个问题的核心数据结构是hashmap。
其中,hashmap的key是每手牌中各张牌的rank,value是每个rank在该手牌中出现的次数。
有了这个数据结构,就很容易就判断出这手牌的大小。比如hashmap中value的值分布如下:
(1,1,1,1,1),且最大那张牌的rank-最小那张牌的rank=4,说明这是个顺子。再配合判断是否为同花,就可以判断是否为同花顺
(4,1),说明是Four of a Kind
(3,2),说明是Fullhouse
(3,1,1),说明是Three of a kind
(2,2,1)说明是Two Pairs
其他类推。
有了这个数据结构,就可以简单自然地支撑计算一手牌大小这个核心业务流程。
综上,大家可以看出核心数据结构对架构设计、主业务流程的重要性。
那TDD是如何驱动核心数据结构设计的呢?
TDD的做法是先写出用例,再实现功能。
用例的本质是什么呢?我们看看用例的设计步骤:
step 1
用例首先是触发业务行为(业务行为是通过业务接口表达的,触发业务行为,就驱动了业务接口的设计。接口设计是设计的要点1),业务行为完成的本质表现其实就是状态组合的变化。简单说,业务行为就是状态变化。
step 2
其次通过assert来校验这些状态的变换是否符合预期。
用例的本质是触发业务并校验状态组合的变化。
怎么理解业务行为就是状态的变化,我们来举个例子看下:
比如注册这个行为register(User),表达用户注册的状态组合一般是DB表或内存表,其本质就是集合,假设注册前状态为
{u1,u2},
现在执行register(u3),再次查询已用户注册状态,变成
{u1,u2,u3},
说明用户注册这个业务行为是相对正确的。
本质就是通过校验用户注册状态的变化来校验注册这个业务行为的正确性,当然可以实现一个verify接口来校验这些状态(使状态不暴漏出来),但是状态的设计是省不掉的。
step 3
通过对状态进行抽象和组织就可以获得系统的核心数据结构,这也是我们设计的要点2。
至此,大家可以看到,TDD最有效的运用方式其实是驱动出系统核心数据结构。这些数据结构一般都是本质的、抽象的,具有一定通用性和某些数学层面上的应用。
咱们再把TDD驱动设计的思路捋一下:
用例设计-》用例触发业务行为(接口设计)-》通过assert来校验业务状态-》对核心状态抽象&组合-》核心数据结构(核心数据结构设计)。
从而完成从用例、业务行为、状态组合、核心数据结构的设计,并真正完成深入的测试驱动设计。
这里才真正把TDD的作用发挥到极致。