Home

TDD笔记3

16 Jul 2014 by LelouchHe

重要的一句话

TDD allows you…no, it requires you to make conscious design choices at all times

以前很少思考过关于设计方面的东西,一来独立设计系统的机会很少,最多就是观摩下大牛们的设计思路,然后感叹下好美,二来是自己这方面的水平,基本还是停留在鉴赏这个阶段

但这句话给我的震撼很大

首先,设计不是某个人的工作,而是一个团队的工作.当然,前期设计或者更高层级的系统设计,还是需要有更高级别的牛人进行完工的,但对于小范围,比如服务级别或小系统级别上面,再到细化到具体代码层面上的设计,还是需要每个开发者自己來cover的

其次,设计是一项有意识的活动,是需要思考的,而人是由惰性的,所以只要能不思考,一般人是不会主动想设计的问题.TDD的主要成就在于,会”require”,甚至”force”我们做有意识的设计,不论是编写test(熟悉需求),还是编写实现(代码完成),或者代码重构(设计系统),都在强迫我们主动思考这个问题.设计毕竟还不是一门工程,没有固定的算法,只有不断的一小步一小步的做起,才能有经验和感觉,才会在适当的时候做出合适的选择.而这一点,我感觉是我日后工作的一个重要方向

最后就是,”all times”的影响.设计应该是随着开发不断演化的,初期的总体设计只是为完成大部分已知确定功能而提供的设计初稿,在此基础上,通过TDD循环,不断的学习理解需求,并适应需求的变更,來完成系统的设计演化.重构,或者说重新设计,不应该作为系统开发中一个单独的过程,而是每次引入新功能,就需要完成的任务

回顾过去两年的工作,主要的精力放在了功能的实现上了,工程能力尚可,但设计这方面,没有锻炼,或者没有意识到需要锻炼,或者意识到了但不晓得如何入手,这个需要从这个角度加强了

渐进设计和前期设计

TDD遵循的是渐进设计风格(Incremental Design),即在TDD循环(Red/Green/Refactor)中不断的逐步推动设计的演化,力求在每个循环中都是*simple and clean的开发设计.在重构阶段,利用test集來保证系统原有行为不会被破坏

与此对应的是前期设计风格(Up-Front Design),即将项目的某一个阶段专门拿出来,分析需求并进行系统设计

个人感觉,前期设计在TDD中也是非常重要的,对于一个从零开始的新系统,进行前期的调研,并在此基础上进行系统主体设计,是系统开发一个非常好的开始.很难想象,一个非常复杂的系统,没有任何预备,就直接开始TDD了,这就相当于徒手伐木,却忘记了磨刀之功

前期设计最大的问题,在于开发者对于前期设计的预期和本阶段和其他阶段的配合.如果开发者只是将前期设计当作设计草稿,并且利用test-before和unit test來规范开发的流程,这压根就是TDD的另一种样子.相反,如果一昧的坚持原有设计不变动,而且将测试放在了test-after和功能验收的阶段,这就成了传统的瀑布模型,那很大程度上是无法适应变动的开发过程的

TDD并不排斥前期设计,只不过前期设计只是草稿,只是手段,在开发的初期帮助我们了解需求,并提供一个可行的前进方向.比如系统基本功能点的确定,基本接口和模块交互的确定,系统分层或通信协议的确定等等,这些如果一抹黑的从下到上TDD的开发设计,一来浪费了我们开发者的经验(见多识广的开发者可能开发过类似的东西),二来琐碎的支线功能,很有可能屏蔽掉了主要功能点,三来很难完成任务的分工和团队合作

所以,前期设计是TDD的一部分,但是要记住一点,accept that your models will almost always be wrong.纲领性的东西,领会精神就好,把握方向,脚踏实地的前进,才是真的

简单设计(Simple Design)

想完成简单设计,有以下几个建议的原则,当面对设计没有头绪的时候,可以从这些角度思考

确保代码可读性高(expressive)

代码的可读性是代码最为重要的特征,因为不论算法多么精妙,设计多么神奇,最后都要有人來进行维护和升级,或者重构,有可能是其他组员,但更大的可能性还是自己.如果代码高度可读,非常的简洁明了,就能大大加速这一过程

有句话说的好,”好的代码不需要注释和文档”.当然,我们并不是排斥注释和文档的作用,这些在某些情况下是有用的(比如阐释why和复杂算法how的时候),但对于大多数代码,最好是要做到”文如其意”,即利用抽象的代码,将概念或者操作明确的表达出来

要做到这点,最主要的手段就是抽象,就代码实现细节来说(我也不知道更高层次的说法了),有以下2个原则:

单一职责原则(SRP)

不管是一个类,还是一个函数方法,重要的一点是确保SRP,即只有一个职责,或只有一个可变因素.这样,我们通过定义合适的名称,就能非常清楚的表达出类/方法的作用,后来者甚至不用查看实现,就能明白其行为

话虽好说,但实际运用其來,并不是很直接明了,比如什么叫做职责/可变因素,哪些可变/不可变,怎么判定.结合自己的工作经验,大概有以下几个迹象,可以说明此处可以使用SRP:

对于函数方法而言,感觉还有一种情况比较常见

最后一种情况的话,就看个人喜好了,我是建议最好调整调用关系或设计结构,尽量让函数方法在一个抽象层面上处理问题,这样不仅便于阅读理解,应该也是一种良好的设计风格

命名规范

这点放在这里可能对某些人颇为搞笑,但个人认为,好的命名规范,不比好的代码实现差.没有实现,我们仍然可以通过mock的方式來定义需求和逻辑,但没有好的名字,干什么都会差了一截

但这个就属于风格问题了.个人推荐的是google代码规范中的命名,对于历史代码的话,还是应该遵循历史,尽量维持一致风格的好

需要不断提醒自己的一点是,不论是类,还是方法,或者文件,模块名称,重点永远是行为的结果(输出,偶尔包含输入,因为输入明显可以另外包含在声明原型中),让别人看到名字就知道是做什么的.至于内部如何实现,则尽量不要提及

一个建议是勤用辞典,一边命名,一边就趁机学习了英语了

消除重复(duplication)

代码上的重复,是最显而易见的,一般来说,一段代码,出现一次,是可以的,一旦再次重复出现(或类似代码),就需要思考是否需要重否來消除重复了.当然,必要的重复是可以的,比如为了增强可读性,需要保留多个地方的简单代码,这个是可控的,因为这样的特例不能太多(太多说明系统设计有问题,此时可读性肯定也有问题了)

更严重的是信息的重复,这个是个新的说法,在@GeniusVczh 的知乎日报靠谱的代码和DRY中提到的,个人感觉这个才是真正的重复,代码重复只是其中的一个部分.其中提到3种常见的信息重复场景:

  1. 魔数
  2. 代码的复制-粘贴-修改
  3. 耦合非正交的逻辑

信息重复,归根到底,还是没有控制的变化造成的

其实就V大提供的例子而言,如果只有这么一个应用场景,我觉得完全可以认了,只是代码的感觉不是很好(有通常意义的bad smell),所幸不会有新的变化了.一旦引入新的变化,那么V大做的考虑和修改就是非常正常的了

这里就涉及到一个问题,即如何识别变化.如果分隔符不会变化,代码上也没有重复,解析的格式也固定死了,自然就不需要要V大介绍的手段

但这个我们事先并不知道,什么要变,什么不变,并不是可控的,有可能这个变化是开发人员自己提出的,这个还好,毕竟自家事(大不了不变了呗);也有可能是第三方,比如pm/客户/其他开发提出的,这个就比较难办了

此时,一个解决方案是假设最坏情况,即所有可能变化的,都会发生变化.此时,分隔符的魔数(分割符会变),重复的代码(配置格式会变),解析配置的逻辑和处理配置的逻辑的耦合(配置格式会变,配置解释会变),就需要重点的进行处理

这个方案的缺点在于需要考虑的地方太多了,对于这个简单的例子而言,还可以接受,但对于复杂系统而言,可变的地方太多了,如果假设最坏情况的话,估计系统的复杂度会上一个层次,感觉对于项目而言不是很可行(就好像STL中的Allocator,就变的很鸡肋了)

第二个方案就是利用经验判断,即看历史上哪些确实在变,或结合项目需求,某些确实可能会变,然后再对这种可变性进行控制,不让信息重复(此处应该是指可变性的处理逻辑)

这个的好处在于简化了系统复杂度,但缺点也很明显,如果没有很好的经验的话,或辅助一定的手段,很可能将设计引向偏途.我自己在开发中就遇到类似情况,错误的假设了可变的因素,自认为不会变的结果在变,导致系统复杂,但又没有很好的解决问题

个人倾向于第2种方式,其实这里的经验发挥了在类似与前期设计中的作用,确保了开发的方向大体上是没有非常大的偏差的.当然,还是需要通过TDD的方式,來确保当原先假设果然出问题之后,能非常迅速而且正确的调整设计

这也可以看到,虽然新的技术可以解决一些问题,但经验在开发过程中是非常重要的.看一眼就能发现bad smell,这个确实需要长久的锻炼和不断的积累,在开发中慢慢培养吧

不引入不必要复杂性(unnecessary complexity)

“enough”是设计中很关键的一个要素,如何权衡并适可而止是件比较抽象的问题.TDD遵循的简单设计,着重强调了不需要,不实现的原则,任何不必要的,在不违背上述二原则的前提下,都是无须实现的

我看来主要是2点,一个是不需要或没有提到的需求,不要实现,此时不能主观臆断项目需求,而是要真真正正通过需求文档,进而通过相应的test來物化需求,无需求,无test,无实现;另一个是过早的优化(万恶之源),不论是性能方面,还是结构方面,只要尚可,就不要过分优化

总结

上述3条原则优先级从高到低,首先要保证的是可读性,其次是消除重复,最后是避免不必要的复杂性

在没有头绪,或者面临诸多方案无法选择的时候,可以参考这三点來帮助判断

阻碍重构

从前文可以看到,渐进设计,主要存在于TDD循环中的Refactor阶段.如果发现重构很困难,或者压根不想重构,就说明了当前的开发出现了问题,需要先彻底解决了问题,再进行下一步的开发工作

一般来说,问题主要有以下几种征兆需要注意

优秀的test

FIRST原则