Daniel Teng:
Justin Searls正在比较底特律派TDD和伦敦派TDD,这是第一集,用的是成为“Discovery Testing”的伦敦派玩法。题目还是Gameof Life
https://www.youtube.com/watch?v=aeX5OXO-w30
Joseph Yao:
为了方便大家看视频,我已经把全部四集(已完结)传到了土豆上面,你懂的。整个四集的时间大概在两个半小时。
Part 1: http://www.tudou.com/listplay/dlhRQuC3a4g/nsSXLo3CE-4.html
Part 2: http://www.tudou.com/listplay/dlhRQuC3a4g/c5SOegUcqmc.html
Part 3: http://www.tudou.com/listplay/dlhRQuC3a4g/S0fs3Z7uMHQ.html
Part 4: http://www.tudou.com/listplay/dlhRQuC3a4g/JN6GRH2Gl4A.html
Justin 有关 Discovery Test 的 Blog http://blog.testdouble.com/posts/2015-09-10-how-i-use-test-doubles.html
Justin 的实现代码在这里 https://github.com/searls/game-of-life-java-example
——————————— 建议你先看完视频再看我下面的评论———————————
我在往来上海,北京和福州的飞机上面看完了所有四集。Justin 的英文说的比较快,有时挺含糊的,有些地方没太听懂。好在有代码演示,应该使我能够理解他的做法和理念了。第一集开始讲的是他对 Detroit School TDD 和 London School TDD 的理解,这部分我很认同,总结的很到位。然后他提出了自己根据 London School TDD 发展出来的 Discovery Test 方法,瞬间让我很期待后面的三集。不过可惜的是,看完之后的感觉离我的预期差距较大,我无法认同 Discovery Test 这种做法。
先来简单介绍一下 London School TDD。这种方法源自于伦敦的极限编程社区,Steve Freeman 和 Nat Pryce 的著作 “Growing Object-Oriented Software Guided by Tests” (简称为 GOOS)是对这种开发方法的经典总结。Steve 曾经和我开玩笑说,这本书是他们社区争论十年的结果。我觉得,相比 Detroit School TDD(由 TDD 之父 Kent Beck 提出),London School TDD(或者说 GOOS)是在其基础上的发扬光大,而不是看似“对立”的另一种 TDD。GOOS 方法使得 TDD 和 ATDD/BDD 可以结合在一起,更好的从业务和设计上,以测试的形式,来驱动整个开发过程。关于 GOOS 方法的细节,我之前和伍斌有过一些讨论,可以看这里 https://groups.google.com/forum/#!searchin/agileshanghai/DDD/agileshanghai/pbojfS61sBE/e-VPbUIfAgAJ
总体来说,我认为 Discovery Test 并没有很好的继承 London School TDD 的做法,甚至产生了背离。另一方面,整个过程有明显的过度设计。具体来说:
整个过程中 Justin 并没有写验收测试,以至于我无法理解他要实现的需求是什么。比如,他在讨论设计的时候提到 KeepsTime 和 TimeLimit 这样的概念,我就不明白到底是什么需求导致了这样的设计。说到底,如果没有明确的需求,我完全可以认为 Game Of Life (GOF)和时间的关系很小。比如,GOF 可以无限制的把 Cell 变换下去,每隔 0.1 秒变一次就可以了。
GOOS 方法中的验收测试,等同实例化需求中的例子,是对业务需求的最小拆分并拥有用户价值,让开发过程始终有一个业务聚焦点。夸张点说,程序员在写代码和测试的每一秒都应该思考的一个问题,就是“我如何让验收测试尽快通过,尽早交付业务价值”。
Justin 甚至都没有提到验收测试这件事,直接导致他在后面三集的开发中,对设计进行不少“YY”(我在下面会提到)。他在第一集的最后给出过别人实现的带界面的 GOF,但是他自己的代码到最后也没有实现那种效果。
如果是我来开发的话,我会先写一个验收测试,比如,在网页上面显示一个 1*1 的矩阵,里面只有一个代表 dead cell 的点(白色),经过 0.1 秒之后,还是显示这样的矩阵。虽说把矩阵显示出来并不是开发的风险所在(Justin 也这么说),但是我在实际工作中遇到过太多以为没有风险的地方最后却发生了风险的事情。写一个简单的验收测试,或者至少手动验收一下,可以很好的避免程序员喜欢“YY”的毛病,减少开发风险。
由于没有验收测试的缘故,Justin 在他的 Design Session 里面提到了很多概念,并且在之后的演示中把这些概念一个不差的变成了代码。这里面有明显的过渡设计,具体表现如下:
KeepsTime 和 TimeLimit 为什么需要?至少这两类到最后也没有实际的代码。
为什么有了 World,还要一个 MutableWorld?我感觉有 World 这个类就可以了。Justin 的解释是 GenerateSeedWorld 产生的 World 有可能对 at(Coordinates) 的实现很不相同,但是我觉得他在没有证实这一点之前就引入 MutableWorld 就太早了
总的来说,我觉得 Coordinates, Point, Contents, Outcome 都可以被 Cell 代替
为什么需要 Contents?我觉得 Contents 可以被 Coordinates/Point 这个类代替,同时给他增加一个 neighbours 方法就可以了(其实这个类应该改名叫 Cell 更合适)。到最后,Contents 里面都没有代码
为什么需要 Outcome?我觉得 ReplacesCell 的 replace 方法直接返回一个下一状态的 Point (或者说 Cell)就可以了。到最后, Outcome 里面只有 Contents 和 neighbours 这两个数据
DeterminesNextContents 和 GathersNeighbors 的代码应该被融合进 ReplacesCell,这些本来就是 ReplacesCell 应该做的事情。GathersNeighbors 没有代码实现,这个类不就是在做 Cell.neighbours 的事情吗?
因为这些过度设计,导致 Justin 的代码中有很多 Middle Man 的代码臭味
整个四集中,Justin 很少花时间重构代码,我觉得并不合理。说实话,测试代码中有不少重复是可以去掉的。他在第一集里面提到过 GOOS 方法要比 Detroit School TDD 做重构的时间要少一点,这点我认同。不过,GOOS 虽然重构少一点,但绝对不是没有,而且关键时刻还是会推翻现有设计的。大家看了 GOOS 这本书就会明白。
有人认为 GOOS 方法强调依赖倒置原则(DIP),所以称其为 Mockist TDD。我觉得这是一种误解,其实 GOOS 首先强调的是单一职责原则(SRP)。因为在 GOOS 中,每当你写一个类的UT时,就应该思考他的职责是什么,那么那些不是他的职责范围的代码就自然变成了依赖(在UT中被隔离)。Justin 其实也是这么做的,只不过因为没有一个体现需求的验收测试,也没有从最小的需求入手,所以导致了过度设计。
不知不觉写了这么多,我也是醉了。最后预告一下,经过这一年在开发和 coaching 中对 GOOS 的应用,我发现他对提升代码质量和开发团队设计编码能力非常有帮助。我计划在明年更大范围的推广 GOOS,并尝试做些社区活动或者课程来推广他。
谢谢,
zhanggang:
多谢Joseph和Daniel,一口气看完了其中3个视频, 第一集真的很好,把London School TDD的优势列的很清楚,看了之后觉得思路又清晰了不少。当然作为London School/Top-down TDD的支持者和实践者,我是觉得其实这是唯一正确的TDD方式, 并没有所谓的缺点:)从交付的角度,Top-down TDD把设计的顺序和实现的顺序实现了完美的统一,这个是非常重要的,非常怀疑过去首先自顶向下设计,然后自底向上的构建是在没有Mock这种基础设施的情况下的无奈之举。从精益的拉动角度,其实被Mock掉的接口事实上扮演了对下游实现的订单的角色,从而消除了实现的盲目性,通过直接联系到交付价值的真实反馈,对下游的实现提供了真实的需求定义。深深地觉得Justin选了Game Of Life作为例子实在失策,GOL算法比较重,设计不需要太强,是演示简单TDD的好例子,但用来做Top-down TDD没啥优势,反而容易过度设计。 Top-down的方式在那种层次比较多,职责分配比较复杂的情况下才能显出特别的价值来。
网友评论