我们在男生厕所,都会看到类似与“向前一步,走向文明”的标语。为的是不要让小便滴到地上。这个问题说大不大,说小不小,经常会被上升到道德、全民素质。。。等的高度。一旦提升到这个高度,问题就会变得很大,而大的问题,大家都知道,很难解决。 上图是UrinalFly公司的小便器, 不同之处是在小便池的里面多了一个黑点,靠近一看,原来是一个苍蝇形状的小装饰。只是一个小小的黑点,就能让我们的厕所干净85%。方案很聪明,而且超便宜。问题很“大”,解决方案却很小。
把问题与方案分离其实贯穿整个产品开发的整个活动中,从需求到实现,再到测试。 用户故事是最流行的敏捷需求管理工具,用户故事从表面上看,尽管只是那一张小卡片,上面有一句话”As a Who, I want What, So that Why”。其实与传统的需求管理方式不单单是表面的不同,同时也是用户故事一个很大的优点,就是让我们在讨论需求的时候关注问题,而不是一下子跳到方案。而对同一个问题总是能找到多种方案(像学校考试中那样一个问题只有一个标准答案的情况在现实生活中可谓少之又少),然后去鼓励由团队提出多个解决方案(不论是业务的还是技术的),多个方案互相比较,选出最优(根据情况不同,可能是最用户体验最好、也可能最便宜、也可能实现速度最快、也可能性能最好)的。因此好的用户故事应该是关注问题的。引用Lasse Kosella的一个很不错的例子
第一种写法的问题是它“暗示”了一个方案-“铲”,而第二种写法强调了,关注的问题应该是“雪被清除掉”,和问题解决后的状态,而解决方案可能有自己去铲雪、雇人铲雪、用除雪剂。。。 同时,尽管用户故事是关于问题的,但是背后蕴含有关于可能的解决方案域(也就是所有可能解决方案的集合)的假设。PO要做的事情就是维持住这个方案域(否决任何超出方案域的方案),同时鼓励团队近一切可能提出好的方案。 还是上面那个用户故事的例子,“雇外星人来铲雪”肯定不在我们的解决方案域内。
把问题与实现分离,也是架构设计中的一个重要的原则。问题意味着不变,而实现总是要变的。面向对象技术提供了很多把问题与实现分离的方法,比如通过接口, IoC等。而如何去发现,很多情况下需要利用重构这个工具,去发现重复,然后去抽象。如果用TDD,有时候会发现几个类或者方法,你需要不断地去写类似的单元测试,而自己也不断纠结,在其他的方法的单元测试已经覆盖了这些情况,现在要另外实现一个新方法的时候,到底要不要再写一遍,很多时候,这意味着代码中有些重复的东西,该重构了。比如下面一段代码checkin
在写这个Controller类的时候,在Create/Edit/Update方法中都需要在领域模型和表现模型之间映射,尽管每一次映射不仅完全相同,但是逻辑上是做的类似的事情,在不同的模型之间映射。所以很自然地把具体映射这部分功能独立出去,八行代码(具体映射的实现, How)变成了一行代码(做映射这件事情, What),而在Controller中再也没有必要去了解如何做映射,也就是具体实现。在代码中把问题和具体实现分离,代码也会更容易维护。
TDD的另外一个好处也是让你产生具体实现方案之前有一个机会去从客户角度思考一下,解决的问题是什么?它最简单的问题是什么?
前一阵子读Gojko Azoic的“Specification by Example”,感受最深的同样也是把问题与实现分离的问题。写得好的测试与难以维护的测试一个很显著的区别就是只见流程,不见动机(Intent,也就是问题)。Gojko在书中强调了式样(Spedification)与脚本(Script)的不同。式样其实描述的是我们系统应该做什么(What),而不是怎么做(脚本, Script, How)。不幸的是我们的测试用例里面充斥着,
“在第一个页面,做什么事情;第二个页面,做什么事情,然后等待X秒钟。。。”
这样的脚本。单单看这个测试用例,读者仅仅知道自动化测试会做些什么步骤,但是不好好的看几分钟,很难去猜出这个测试做了一件什么事情。而具体操作的步骤是不断演变的,任何页面的变化,都会导致我们的测试失败,所以我们的自动化测试成本总是很高。而下面的测试用例却很容易读懂,也很容易维护。