对实例化需求方法的整理与思考

引言

“我但愿这里能这样……”,“我但愿这里能再增长点东西……”——在软件开发的世界,咱们永远没法解决的一大难题,是客户纷繁复杂而且不断变化的需求。如何把需求映射为最终的软件交付,是每个软件开发方法都没法回避的核心问题。html

领域驱动设计(Domain-Driven Design)经过专一领域核心、创建通用语言等手段,以及聚合、仓储等战术模式,很好地达到了去伪存真、化繁为简的目的,从而在团队协做、划分边界、创建模型等方面呈现出足够的优点。可是DDD的实践应用,很是依赖与客户沟通的技巧以及对领域知识的掌握。而这两点,都是须要一些实践经验积淀的,因此初入DDD并不容易。数据库

实例化需求(Specification by Example,如下简称S&E),是我在学习BDD的过程当中接触到的开发方法。之因此称其为开发方法,是由于它没法解决如何分析建模的问题,而主要回答了如何梳理用户需求Specification,并最终将其实现为软件交付Delivery的整个过程。其擅长的,是捕获需求、肯定验收标准。编程

那么,为何我要把DDD与S&E相提并论呢?这是由于,DDD能帮助咱们划分讨论的上下文、提取通用语言UL、创建软件模型,S&E则能够帮助咱们梳理讨论的场景、验证模型可否达到交付要求、生成开发的相关文档。因此我我的认为,这两种方法能造成优点互补,帮助咱们更容易地开发出“正确的软件”。数组

关于书籍

DDD之父Eric Evans的《Domain-Driven Design: Tackling Complexity in the Heart of Software》、Vaughn Vernon的《Implementing Domain-Driven Design》、Scott Millett的《Patterns, Principles, and Practices of Domain-Driven Design》,还有Jimmy Nilsson的《Applying Domain-Driven Design and Patterns: With Examples in C# and .NET》为咱们提供了完整的理论和具体的实践。安全

如今经过个人书摘,再看看
Gojko Adzic的《Specification by Example: How Successful Teams Deliver the Right Software 》是怎么经过正反事例的对比,来逐步阐述Specification by Example这一方法的吧。架构

注:《Specification by Example》一书已有中译本《实例化需求:团队如何交付正确的软件》,由人民邮电出版社出版。不过我手里只有这浆糊同样排版的英文原版。因此,下文彻底出自于个人我的理解,各类类比和小结将不断穿插其中。并发

看到这排版,我真心醉了

S&E过程概览

软件开发的压力主要来源于:时间——开发的期限愈来愈短,成本——维护的要求愈来愈高,变化——需求改变的频率愈来愈高。S&E采用一系列彼此衔接的处理模式及其产出的工件(artifact),帮助咱们顺利实现需求。其过程主要包括如下环节,并做为本篇各小节的目录:app

S&E关键过程示意图

S&E的关键过程示意图

创建“以文档为中心”的理念

目前实例化需求的过程如今有两种流行的模型:以验收测试为中心的ATDD,侧重于自动化测试,优势在于使开发目标更明确,并防止功能退化;以系统行为规范为主导的BDD,侧重于制定系统行为的场景,在客户与开发团队之间创建共识。这两种模型,各有长处、各有用途,因此无所谓孰优孰劣。咱们关注的,是它们生成的活文档,这是实例化需求产出的最好工件。书的第三章,率先回答了什么是活文档的问题,并倡导创建“以文档为中心”的理念。异步

为何须要活文档?

维护开发文档老是一件费力不讨好的事情,却又老是不得已而为之,由于未来的重构与维护都须要以这样的一份文档为基础。这份文档须要能快速地勾勒出系统的轮廓,清晰地表达出系统主要的概念,准确地描述系统架构和模型的结构。最关键的,这份文档必须是最新的。因此,这份文档不只要完整,还必须是“鲜活的”。工具

测试为何能够做为文档?

自动化测试自己是按必定的逻辑编排的,因此具备必定的组织结构性。测试方法的名称,也能够看做是测试的一种“自描述”文本。因此这种结构性与自描述性,与开发文档的需求不谋而合,因此测试能够被看成文档的一种形式——“代码即文档”。

可是要注意,不能所以偏重于测试自己,而忽略了测试与需求之间的联系,使得测试变得臃肿和不易修改。ATDD的方法,每每过于注重编写和执行测试,所以容易写出不易维护的测试,致使有需求变化时,产生牵一发而动全身式的连锁反应,大量的维护与重构工做使得先前的测试工做变得得不偿失,所以要极力避免。

如何从测试获得活文档?

若是把带有Example的Executable Specification比喻为页面,那么整个活文档就是由此构成的一整本书。利用Relish等BDD工具,咱们能够把经过自动化测试验证后的Specification提取为HTML或者PDF等格式的文档系统,这甚至能够稍加修改就做为用户手册使用。

综上所述,由于重构与维护的难度,咱们须要一份组织良好的文档。BDD的自动化测试正符合文档的要求,并且刚好这种文档能够利用一些BDD工具从能够执行的Specification中提取出来,因此实例化需求方法能够视做一种创建在“以文档为中心”理念上的开发方法。

根据业务目标划定问题域

敏捷开发是以用户故事为核心的,因此故事讲得好很差相当重要。那么这个讲故事的责任究竟应由谁来承担?传统的软件开发,认为划分问题域、讲清故事是客户的事。对此,《BDD in Action》和本书的两位做者都反复强调,『不能交由客户去编写用户故事、用例清单等细节,不然就等同于让客户去提供一个具体的、高层次的解决方案了。』因此,划分问题域、讲好用户故事,是开发团队的责任。在划定问题域这个环节,重点应该是引导用户弄清究竟须要什么,进而经过发掘现有业务的潜在,提出新的思路和新的方案。

使用Impact Mapping

划定问题域的具体方法,是理解“Why”与“Who”。这和我在前篇对结合BDD进行DDD开发的一点思考和整理中介绍Impact Mapping这个工具时同样,重点是理解“为何要这样作?”、“谁人将从中受益?”等问题,弄清客户开发系统所能指望的价值到底是从何而来。此时,因为系统轮廓不清不楚,可能会感受无从下手。为此,建议先分清业务目标与要交付的功能,而不是尝试去把每个用户故事描述清楚。对此,我我的认为Impact Mapping提供的Goal-Actor-Impact-Deliverable模型,将是一个很是合适的挖掘工具。咱们能够经过连续的Why提问,来弄清真实的、具体的、有期限的、能度量的业务目标。

Impact Mapping

从客户期待的输出结果推导业务目标

当难以肯定业务目标时,先不要急于讨论须要哪些功能,而是能够从描述客户期待的输出入手,分析为何须要这样的输出,从而概括出业务目标所在。好比对于一个ERP系统,坚持“Report-first”,用各类报表展现客户期待的系统输出结果,由此发掘业务目标,能够帮助咱们把注意力集中在具体的报表项内容上,而暂时把流程、处理等功能性需求放在一边。

使用“As a - In order to - I want”描述目标

  • As a stakeholder
  • In order to achive something valuable
  • I want some system function

这样三段式的描述,能够与Impact Mapping的内容有机地联系起来,至关于根据Actor-Impact-Deliverable直接转译而来。

Melvin PerezCx之映射图

询问的技巧

  • 为何这东西有用? 经过提问,引导客户用具体的事例,来回答为何某个功能有用?是如何给他的业务带来帮助的?“为何须要这些东西”带有诘问的口气,所以并不推荐。
  • 有什么可替代的方案? 经过寻找能够替代方案,能够帮助客户从另外一个角度去思考和认识本身的业务目标,同时也给团队的实现提供新的思路、决定当前提议的是否已是最佳方案。

经过沟通协做来制定需求

系统需求,须要由客户与开发团队达成一致,确保系统的各个方面的功能都被包括其中,并有明确具体的验收指征做为约束。这和DDD中分享消化业务知识,获得领域的通用语言是一致的。在DDD中,也提倡专一于最有意思的对话上,并从用例开始,从一个系统行为做为起点,组织开发人员、业务人员和业务专家,围绕一个特定的场景进行讨论,由此发现这一场景内的领域概念和业务知识。这一点也正是我很是珍视的,实例化需求和BDD这一类方法,楔入DDD的关键点。

这种协做,不只发生在开发人员与客户之间,一样也在开发人员与测试人员之间。若是开发人员与测试人员没有围绕Specification达成一致,那么双方就会各行其是。开发人员看到的是一堆的需求,而测试人员看到的是一堆的测试用例。若由开发人员撰写Specification,它会由于过于贴近模型设计而充斥大量的模式、架构元素,从而变得难以理解。若改由测试人员独立撰写时,可能又会由于太过琐碎零散而变得难以维护,最终迷失在各类测试细节的汪洋大海之中。测试人员编写的测试,没办法帮助开发人员去组织整个系统的各个部分,也没法经过自动化测试驱动整个开发过程。测试人员编写的测试,也无法被看成Specification再被开发人员利用,由于这些测试都是站在测试人员的立场,用测试人员的方言、专业术语编写和描述的,因此没办法用于双方的沟通。对于测试人员,则会在每次系统需求改动时,面对一大堆的测试重构。由于这些测试都不支持自动化测试,或者不容易被其余人理解。因此,协做是普遍的、多重的、具体的。

视协做的规模不一样,能够分为:

  • 大型的全体工坊: 适合项目刚开始的阶段,增进彼此的了解,并划定足够大的范围。可是要协调如此多人员的日程安排在某一天达到一致,是一件很是困难的事。
  • “三剑客”式的小型工坊: 开发人员、测试人员、业务人员组成的小型团队,主要负责勾勒具体场景,产出Given-When-Then三段式的Feature文件。
  • 结对编程: 分析人员与开发人员的结对,这是一种高效的方式。为了不开发人员站在本身的视角采用TDD的方法编写用户故事而有失偏颇,因此转由分析人员编写测试,好让分析人员掌控Specification的全貌。但这又会产生另外一个问题,分析人员编写的故事可能会影响到许多已有的测试,而他本身根本没法预见。同时,分析人员习惯于一个故事对应一个流程,从而产生大量重复。因此最后可行的方案,是由分析人员制定测试计划,并与开发人员一块儿编写feature文件,防止遗漏可能的需求。
  • 非正式会议: 由分析人员、编程人员、测试人员和业务相关人员采起非正式的聚会形式,目的在于统一理解、消化知识,发如今各自独立工做时未能发现的内容与细节。

用事例阐明需求的具体内容

只有当场景描述具备很强的带入感时,才能激发客户参与讨论的热情,才更容易达成共识,并发掘潜在的概念和需求。传统的基于平面文档的平铺直叙的方式,在向用户展现系统场景、捕捉系统需求时,可能会由于词不达意,而致使不一样的人产生不一样的理解,因此描述性的文档始终没法与清晰的代码媲美、也远没有代码直接。然而清晰的代码并不是一朝一夕,让用户直接面对系统代码也彻底没有意义,因此咱们退而求其次,改用一个特定场景下的具体事例来表述系统的行为,达到与客户有效交流的目的。因此,举例说明的方式,对于共同认识和理解某个场景是很是有益的。

在选择和描述每个例子时,做者提出要坚持“例子四原则”:

  • 例子老是明确的。

    • 不要使用"yes or no"这样的问卷调查,应更注意彼此的沟通。
    • 不要使用“小于10”这样的『比较性』描述,而使用“9”、“11”这样明确具体的值,来对应不一样条件下的场景。由于比较性的描述,老是意味着一个值域,而不是单个的值。相比引发变化的单个临界值,这样的值域对于咱们理解事例并无更多的帮助。
  • 例子老是完整的。

    • 要用具体的数值去表述不一样条件下的不一样场景,特别是要注意正反两方面的数据输入组合。对负数、0这样的边界值给予足够重视。当涉及的条件是对象时,则要留意无效的引用、null等。
    • 使用替代的方法进行验证。这个小节在原书的描述里很是晦涩。我大意理解为,对于一些新旧数据并存的系统,咱们很容易只惦记着新环境下的各类例子,而遗忘了旧数据也应该当被考虑在内。
  • 例子老是现实的。

    • 建议直接使用真实的数据,而不用费心为测试专门编造数据。这样能够充分利用旧数据,减小将来可能的数据兼容风险,保证新旧数据在系统中的一致性。
    • 直接由用户提供基础的例子,而不要本身臆造。
  • 例子老是易于理解的。

    • 不要拿一堆的参数组合表格给客户,咱们重点关注的应当是全部的临界点。对每个临界条件,都应当进行认真讨论。若是双方对该条件是否确系临界条件有争议,那说明双方对例子的理解自己就是有误解的。这彷佛又回到全部问题都要达成共识的这个原点上来了。
    • 注意寻找潜在的概念。在面对与一个功能联系的一大摞事例时,解决事例过于细碎、不易理解的方法,在于对事例适当进行抽象和概括,而后再转头分析此前那些琐碎的下层概念,如同是切碎了再进行二次理解,这样可能会发现一些潜在的概念。

在安全、性能等非功能性的需求方面, 当其重要性已经达到影响业务价值或者业务目标实现程度的时候,那就清晰地表达出来。这与《UML精粹》中的观点是一致的,一切需求的重要性视其对实现业务价值、业务目标的影响程度而定。在性能、响应时间这些没法准确表述的需求方面,做者引入了QUPER模型。对这个模型,我我的理解是预估这些指标对应的障碍,以及由此产生的开销,而后再展开讨论。每一个问题会被分红三个方面:

  • 可用性:是否有功用性——“能不能用?”
  • 分化性:是否有市场占有能力——“相比其余产品是否是更具优点?”
  • 饱和性:过分设计没有意义——“弄得再好一些有没有实际意义?”

做者使用了“手机开机速度”做为例子:不能开机,手机就没用;开机速度太慢,就没市场竞争力;开机很是快了,再快就没有意义了。

提炼需求

好记性不如烂笔头。交流的结果必定要以某种形式记载下来。原始的例子就象未经雕琢的钻石,只有提炼后才是关键的、易理解的、方便转换为可执行Specification的、能予以自动化测试的Key Example。

Specification应该是明确的、可测试的

这一点,和Example的”明确的”是一样的含义。要尽量消除描述上的模棱两可,而且要保证全部参与讨论人员认识上的一致。

Specification应当是真实的互动,而不是简单的脚本

脚本一般更侧重于描述一个事物是如何变化的,更多的倾向于流程方面的内容。这也是客户在描述的时候,容易掉入的一个陷阱——“先这样,而后那样,接着再怎么样,最后又是什么样”。这种脚本或者说是流程形式的表述,缺少一个系统的视角,缺乏对系统与用户交互状况的表达,并且流程自己很容易改变而且难以维护,因此并不适合直接做为Specification。正确的方式,应该按”在这样的状况下,系统会作出那样的反应“的形式进行表述。重点是”系统应该作什么“,而不是”系统应如何工做”。

Specification应该是业务功能相关的,而不只仅是软件设计意义上的结果

Specification不要与代码、与UI等技术实现细节耦合太紧。技术层面的难题,以及流程等细节,留待再下层的自动化测试去解决。

Specification应该是自解释的、不言自明的

为了提升Specification的可阅读性,能够给它增添一段描述文本,而后交给其余人看,静静地观察对方的反应。若是对方无需额外提问就能理解并达成一致,那说明这个Specification符合预期。不然就把回答对方的解释,也写进开头的这段描述性文本里。

在筛选关键事例时,应优先把握全部成功的场景,而把可能失败的情景先放在一边,由简入繁地先把功能正常地展示出来。在具体筛选时,能够从如下几个方面着手:

  • 描述了业务功能的某个方面
  • 描述了重要的业务边界或者业务规则
  • 描述了可能致使失败的某种情形

Specification应该是专一的

使用Given-When-Then三段式表述Specification,而且尽可能避免考虑动做或者事件之间的依赖关系,最好就专一于一个动做、一个事件。对于新旧数据共存的系统,好比ES+CQRS里新旧版本的领域事件,为了保持专一,建议把这种兼容性问题压入自动化测试层去解决。对于系统当中的缺省值,虽然能让Specification更易读,但考虑到这种缺省值若是表述在Specification中,将会致使过强的依赖性,因此也建议移入自动化测试层。在这个问题上,能够参考“魔数”。当咱们把魔数显式地定义出来时,才更有助于咱们消除误解。将其移动到自动化测试层,或者放入一个全局的配置当中,都是相对更灵活的方法。

Specification应该是具有领域意义的

这一点又转回到DDD了,即Specification中引用的概念、关系,都应该与当前上下文中的通用语言保持一致。

用自动化测试验证需求

随着软件规模的逐渐增加,测试的数量、大小、应对变化的能力,都要求咱们采起自动化的测试方式。在这个过程当中,必须以预约的Specification再也不修改做为前提,不然咱们的自动化测试只能是以讹传讹了。换个角度看,虽然自动化测试增长了学习的成本,要引入额外的BDD工具,编写额外的Feature与Specification,获得的倒是先后一致的需求表达和更加轻松的后期维护,代码的实现也会更天然。这一点,我认为和DDD里的从UL到代码的展开是一致的。由于有统一的UL和清晰的模型,因此直接映射到代码也就更直观和天然了。

在具体实施自动化测试时,应该由简入繁、从易到难,事先作好规划。由于构建整个自动化测试的上下文环境是相对比较耗时费力的,这个上下文还要集成到必定的系统环境中才能执行,未来需求发生变化时这个环境也能被重用,因此很是有必要在对待整个测试环境规划时更慎重一点。

在具体的实现环节,将自动化测试与编写业务代码同步,好比采用TDD的方法,能让全部人把精力集中在测试上,保证Specification顺利实现。这就如同公交车,若是中途没有乘客上下,天然会跑得很快。但事实并不是如此,测试的职责就是保证Specification的实现、业务代码的正确,因此自动化测试的规划与实现,必需要由开发团队承担起来,不能交给其余人。

手动测试与自动化测试的区别在于,手动测试侧重于准备上下文,而后测试是否经过,关注的是成功与否;自动化测试则关心致使测试失败的缘由。特别的,手动测试一般是脚本化的,一个步骤紧跟一个步骤,每一次测试都重复这个过程。若是测试失败,那么手动检查其中的每一个步骤也在情理之中,反正都是手动的。自动化测试则必须消除这种测试步骤之间的依赖性,不然当测试失败时,没法肯定到底是哪一个步骤出了问题,自动化测试将所以退化成手动测试。若是遇到这种状况,能够将其切分为若干个小的测试,好比每一个步骤对应一个小的自动化测试,改由测试上下文经过准备测试条件将这些小测试联系起来。由此引伸出一个问题:测试代码要不要良好的组织与设计?答案是确定的。由于良好编码的测试代码,才能方便维护和阅读,并做为活文档的提炼来源。

Executable Specification一般是用文本或者HTML格式进行描述的(想一想Cucumber的Step或者Spock里的测试方法的描述式命名)。这样当这个能够执行的需求说明发生改变时,一般不须要从新编译业务代码。而自动化测试是代码,并负责对Specification的验证,因此当需求说明改变时,要从新编译。此时,为了不在Specification中混杂太多计算、判断的逻辑,要把这些验证逻辑放在自动化测试里,而不要表述在Specification里。由于对于Specification而言,在转换为Executable Specification时应当关注的是“测试什么”,而把“如何测试”的责任交给自动化测试。

尽管测试不能是脚本,容易变化的流程应尽可能放在自动化测试层。可是不管怎样,业务流程老是客观存在的,经过When-Then的事件驱动方式,咱们能够借由若干个Specification的组合,展现完整的业务流程。而具体的业务逻辑,则放在Specification里。因此,不要在测试代码里重复业务流程或业务逻辑。

UI的自动化测试

依赖UI与数据库的测试,是自动化测试面临的最大困难。书里提到了许多建议,但都很是须要实践进行验证,才能深入领会。至少我只理解了皮毛,因此这一段的内容暂时没有办法总结。尽管如此,大量地引入Stub与Mock进行测试、隔离UI与业务模型、进行持久化无关的设计、创建统一的应用服务层、在Specification里竭力避免引入UI与存储相关的元素等等,都是可行的方案,相似MVC、MVP、MVVM等模式也将成为咱们解耦的利器。

即便要对UI进行自动化测试,也建议使用针对UI编写的Specification进行验证,并且不要使用“录制-回放”工具。由于这类工具生成的脚本一般会增长必定的学习成本,并且会很是难以理解,不便于维护。

从需求说明到UI的自动化测试,能够从如下3个抽象力度逐渐弱化的不一样层次进行实现。其中,需求说明应该在第一个业务规则层中描述,自动化层则应该经过组合第三个技术行为层来表达第二个业务工做流层。这样分层实现的从需求到测试的描述,更易于理解,也更高效。

  • 业务规则层 :测试要展现或者操做的是什么? 对一条业务规则进行描述:对购买了5本书的客户提供包邮服务。
  • 业务流程层 :如何经过UI使用某个功能,以更高的抽象级别进行描述? 对一个业务流程进行描述:放5本书进购物车,而后验证是否提示已包邮。
  • 技术行为层 :在一个流程的某个环节,须要哪些技术性步骤?对实现一个业务流程的具体UI操做步骤进行描述:点击5本书的"放入购物车"按钮,而后点击“下单”,页面显示“已包邮”。

持久层的自动化测试

用数据库做为自动化测试的数据来源,是一个比较便利的方式,可是如何管理这些数据却成为一个难题。要避免直接使用旧存的数据做为测试的数据源,由于这种数据与如今的需求可能存在冲突、不易理解。对于构造过程相对复杂的对象图,能够尝试在数据库里预置相关数据,以提升自动化测试的速度。

在重构需求的同时,频繁地进行同步验证

这一部分的内容,与重构、持续集成紧密相关,所以提到的也可能是化整为零、“不要想一口吃成一个胖子”、用Mock隔离故障点、以事件驱动测试、先保证同步再尝试异步测试等等建议,而且提倡引入并发测试、快慢分组等方式尽快提升测试的反馈速度。

“业务时钟”

对于那种周期性执行的测试,引入天然时钟显然是不合适的,因此新增“业务时钟”的概念去控制这个周期,使之能够根据测试要求随时执行这一类的测试。个人理解,它等同于一个虚拟的时钟,基本原理仍是触发一个时间事件,而后利用这个事件去驱动测试。

提取活文档

因为提取活文档更多的是BDD工具的使用,因此只要有编写优雅的Specification和自动化测试做为基础,文档的生成是一件水到渠成的事。在这个部分,主要的建议包括:

  • 以UI导航流程、功能结构、业务过程等组织文档内容。
  • 即使是虚拟的角色,也要保证完整的角色信息。
  • 合理使用Tag、WiKi等一些文档组织技术,提升可阅读性。
  • 在文档中始终保证领域专用语言DSL的统一。

写在最后

实例化需求的实践,重点和难点都在其中的4个环节:制定需求、描述需求、提炼需求和自动化测试。而《实例化需求》这本书自己的内容,也更偏重于用正反两方面的具体事例来引导咱们的思考,涉及具体操做步骤的内容相对较少。因此和DDD同样,掌握实例化需求的方法,也须要大量的实操和经验积累。整理出这篇书摘,附上本身阅读时的注解,但愿能够抛砖引玉、温故而知新。

相关文章
相关标签/搜索