主题:【原创】闲聊敏捷编程——测试驱动开发(一) -- 代码ABC
你们没有遇到另一类问题,就是客户不断更改需求,以及需求非常复杂,以至于项目组成员看到客户就害怕。客户的需求更改非常正常,当他看到实际成品出来的时候会接二连三的冒出‘灵感’,此时如何对应这些‘灵感’是agile/SCRUM体制中最最精彩的部分。
待我有空,我来写SCRUM。
。
软件开发组织过程中需要完成两个任务:第一是让需求描述更精确,第二是让代码能够准确反映需求。现实的矛盾是需求会发生变化,而变化导致代码难以准确反映需求。于是人们引入测试和设计。测试用于验证和检查(更多的是检查)代码和需求的契合程度,设计则希望代码可以更好地适应变化。
对比传统的软件开发模型和敏捷方式我们可以看出,前者更强调第一个任务,文档、字典、审核、会议等各种过程都是为了发掘需求、描述需求和规范需求变更而做的。其代价是需求变更的反应时间变长。作为程序员的本能对变更代码有强烈的抵触,而客户则不可能在一开始就将需求描述清楚,因此在实际项目中这些软件工程的工具常常不自觉地(有时是故意的)被用作开发团队和客户扯皮的工具。这样就偏离了这些过程原本设计的目的。在保持需求稳定和代码返工的选择上大多数开发团队会本能地选择前者,虽然最终大多需要屈服。因为需求变更反映的是软件的价值。
敏捷方式选择了另一个解决方法,让代码以最大的灵活性来适应需求的变更。敏捷的一个含义就是代码的灵活性。如果我的代码可以随时修改,那么我自然不担心需求发生变更,也不太担心需求定义不准确。这样做法的代价是什么呢?我觉得最大的代价是程序员必须用一种全新的观点来看待设计和测试,对于大多数程序员来说这是一个挑战。
让代码随时保持变更的能力的一种方法就是测试驱动开发。首先测试驱动开发的过程本身就让我们不断地修改代码,即使需求是确定的。因为在开发过程中有一个原则——保持未完成的工作最大化。具体的要求是让代码只能通过已写出的测试,令下一次测试就会击败现有的代码。或者说不允许超前设计,哪怕你知道下一分钟就就需要变更需求也不要为这个变更去修改设计。其过程就像雕刻,每一刀要求不多也不少,每一刀之后再回头看看需要再哪里下一刀(测试)。在Robert C.Martin所写的《敏捷软件开发 原则、模式与实践》一书中有一个开发保龄球计分的例子就很好地说明了这个过程。一般的开发方法会在一开始就将保龄球的规则全部考虑进来,如补中、全中等等,然后以此做一个设计。然后编码,最后用一系列测试来验证代码。然而在极限编程(敏捷开发的一种)中则是每写出一个测试就写一段代码实现,和以往的开发方式最重要的区别在于实现的代码只考虑已经写出的测试而不去理会真实的保龄球计分。这就是测试驱动开发中“驱动”两字的含义。代码的变更只受测试变更的影响。这样的代码是很简单的——至少开始是很简单的。
简单的代码和简单的设计可以随时抛弃和变更——这就是敏捷的核心!而对程序员的挑战也在于此。我们——尤其是有经验的程序员总会被设计所诱惑。我们试图在一开始就写出一个包罗万象的架构,其中包含了大量可能并不需要的东西。最终的结果就是这个设计在多次变更后变得僵化,不容易修改,导致我们不愿意放弃,这也是我们抵触需求变更的基础。也许有人会反对,高手们反对会更加的激烈。因为他们知道良好的设计本来就是为了对付变更的,许多大牛可以在项目开始不久就设计出一个灵活的架构,将每个模块间的耦合降到最低,这样的架构对变更也是友好的。我承认这是事实,不谦虚地说我也是这样的人——曾经。
有一句话:把复杂的事情搞简单是一件很困难的事情,反过了则很容易。体会最深的时候就是我被敏捷思路彻底洗脑的时候。首先作为一个老程序员——或者说大龄程序员,我知道实现一个功能通常不止一种方法,每次都能在一开始找出最佳方法的概率太小了。事实上我经常在后来发现有更简单的方法,而这些简单方法基本都是在需求大部清晰的时候才发现的,而我的复杂方法得复杂性在于我过多地考虑了如何应付客户不可能发生的需求上。这些设计的复杂性常常阻止我去对现有代码进行修改,带着遗憾交付或者根本不知道存在遗憾的交付发生的次数是很多的。还有一些情况,这个复杂设计未必能完全覆盖客户的需求变更——这种情况更多,那么我们的选择是什么呢?一般来说我们只能对原有的设计进行修补而不会推倒重来,因为那意味着大量的返工。当这种修补(尤其是在进度压力下)积累到一定程度之后,我们的代码必然变得僵化再难以适应新的变更,最后就陷入了修补——僵化——修补的恶性循环中。有经验的程序员会在适当的时候对设计进行重大修改——这需要勇气。大牛们通常不缺乏这种勇气,事实上有这种勇气的人才叫大牛。因为大牛知道如何最快地完成这种修改。敏捷的模式是不需要大牛的,如果一开始就不做超前设计,而是让设计在构建过程中逐步建立反而容易形成优化的设计。在这个过程中测试驱动开发经常是推倒原有的设计——小范围的推倒,让程序员习惯对自己的代码动手动脚。在潜移默化下水准上的程序员也会具备推倒的勇气和技巧。这样我们的开发团队就逐渐具备了随时修改代码的能力,从而进化到敏捷团队。
也许有人觉得这种不断推倒重来的过程效率很低,但是别忘记再测试驱动开发中,测试是不断建立的,也就是就项目进度而言需求已经是在不断地完善,推倒的主要是一些拙劣的设计而不是项目本身。
以上我讲的范畴基本都局限在如何写代码这个过程上,很少涉及项目、客户的问题。这其实很正常,因为题目是测试驱动,这个过程主要的精力是如何写代码。在敏捷开发中代码是关键,如果不能掌握敏捷代码的编写其他敏捷思路都是空中楼阁。在写了那么多年的代码之后再来学习怎么写代码的确是一件令人汗颜的事情。测试驱动开法还有不少内容,比如我是怎么理解设计、怎么理解测试和进度的关系等等。真是一个没完没了地话题。
本帖一共被 1 帖 引用 (帖内工具实现)
重构一个很重要的目标在于消除代码中的重复:代码重复、结构重复、思路重复等等。而消除重复一个方法在于抽象。Java、C++和C#这类OO语言一个最强的地方就在于抽象能力。动态语言我用得很少,感觉上其抽象能力不如上述语言强,不过,也许是我还没领会其设计思路吧。应该和工具无关。
因为程序执行过程是动态的,你不可能写出全覆盖的测试.还不如把时间花在写好代码,检查代码上面.
个人理解,测试驱动主要还是用在明确需求,验证需求实现上(Tester,Client).
重构,我们每天都在做,只不过不像现在上升到理论高度.
在测试驱动开发中测试还起到其他作用,这些作用和明确需求一样重要。比如设计,你必须设计出一个可以测试架构,否则你无法写出测试代码,这样就迫使你设计出松耦合的结构。重构也是测试驱动的,为什么重构,重构的方向是什么这些都是通过测试指出来的。
由于我用的是极限编程的方法,根本无法绕开测试驱动,不过就我所知其他的敏捷模式也很难绕开。这是保持代码灵活性的基本手段。
我这里讲的都是局部问题,归根到底是程序员的事情。但软件开发不仅仅是程序员的事。一发便携式对空导弹可以干掉一架直升机,但是要对付一枚洲际导弹就不仅仅是一发标准3的事情了。
我来管代码,哈哈。
to do it.
松耦合的结构,模块与模块之间在功能上是独立的,正交的。你在明确需求,明确功能模块的划分之后就可以做到.
敏捷也强调不可过度设计.一开始编码也不可过分追求松耦合,只针对我能看得到的需求变化进行合理的设计.
与其说重构是测试驱动的,还不如说是需求驱动的。假如说程序中原来已经使用一种图像A格式的编码库,现在需求要增加一种新的图像格式B的编码库,不过两者调用接口不一样,那么只有重构,增加一个共同中间层或给B增加一个中间层.
PS:看过几个java程序的代码,对需求的变化十分敏感
,导致中间层一层套一层,接口太多.
软件工程的目的,是将复杂东西分解,做到简单化,模块化。在遇到具体的需求时,具体对待。
有很多原则需要动手实践之后多次对比才能领会到那些敏捷的意思。
敏捷反对过度设计,反对超前设计,但是不反对为了测试而进行设计,而且提倡这种设计。另外要注意测试驱动开发是每写一个测试就写一段实现,而不是将测试全部写出来再写代码。一方面降低设计难度,另一方面细化需求的发掘。也就是设计是逐步变化的。
重构的概念不是因为需求变化而修改程序,重构的意思是在不改变程序运行结果的前提下,修改和优化代码。象你举的例子那样的修改不是重构而是重新设计。重构在测试驱动开发中用来让代码能够灵活地适应新的需求变更,比如根据图像格式B的要求对代码进行修改后。再考虑如果再有新的图像格式怎么办,根据这个思路对代码的变更才是测试驱动中的代码重构。
这两个概念——设计和重构我还没想好怎么写。。。。
什么是改变的大敌?
一,重复,当一个被复制的到处都是的逻辑需要修改时,代价在于找出所有需要改变的地方,这是困难的。
二,复杂,当一个混乱不堪的程序块需要修改的时候,你需要找到合适的需要修改的地方和修改的方法,这是困难的。
三,耦合,如果所有的程序块都高度耦合,你需要找出合适的修改方法完成改变而不影响到相对应的模块,这更是困难的。
困难到了一定的程度,就是mission impossible. refectoring的作用在于在合适的时候,通过修改代码的结构,把复杂性逐渐增高的逻辑分拆,把重复的逻辑合并,把不合适的逻辑划分改变,从而降低你系统的复杂度和耦合度,提高系统的可读性和灵活性,降低变化的代价。如果没有refectoring,只能叫拥抱需求,只有充分的refectoring,才能说是拥抱变化。
完全看不懂是啥意思?偶是工科生,弄机械的,看的晕呼呼的,楼主能弄得更容易点吗?加点糖衣,炮弹就免了:)