对结合BDD进行DDD开发的一点思考和整理

时间:2022-08-31 13:02:56

引言

二十年前的我,还在学校里抱着一台DIY机(德州486+大众主板+16M内存+3.5inch软驱+昆腾320M硬盘,当时全校最快主机没有之一),揣着一本《Undocumented DOS》,沉迷在Pascal、C以及汇编混合编程的世界里无法自拔。二十年后,软件开发已远不是当初几张简单流程图可比,软件开发的方向由简至繁,各式的开发工具更是层出不穷,不仅让新出道的人们深感乱花渐欲迷人眼,也让我等备感跋涉之不易。所幸爱好不外有二,读书实中其一。也唯有不断学习,才不至被拍死在沙滩之上。

学习BDD,实属偶然。在学习ES+CQRS的过程中,我认识到必须要转变观念,把传统单一结构的领域模型一分为二,将其中反映系统状态发生变化的部分封装在写模型里,而将查询或呈现的部分封装在读模型里,分别设计、分别实现,再以领域事件为纽带,实现整个业务的最终一致性。因此,发掘领域事件、理解最终一致性成为个中的关键。因此,我开始寻求发掘领域事件的方法学支撑。

在搜索引擎帮助下,我很快找到了一些社区和Blog上关于发现和挖掘领域事件的文章,而它们的观点最终都归结为了一点:讲好故事。讲好了故事,才能清楚我们究竟要什么,才能帮助我们划清边界,才能发现边界间的联结关键。于是,很自然地,BDD进入了我的视野,然后是Specification by Example,继而是Impact Map。它们都可以帮助我们把故事讲好。

这一篇,先总结一下这段时间的学习成果。之后再争取学以致用,用一个具体的项目进行DDD+BDD的综合实践。

回首过往

既然是要在原有开发方法里掺入新的东西,那就说明现有方法必定有其缺陷,需要加以完善和改进。所以,先说说我们在此之前是怎么做DDD的。

  • 刚开始,我们会有一个很原始、很初级的需求说明,大致说明项目的背景和主要需求。而这样的需求不外有二,或是改进现有业务流程,或发现新的价值增长点。接下来,是若干次的见面会,通过讨论和沟通,从客户处得到的用户故事越来越详尽,业务流程因此得到逐步细化,一些关键的概念也得以逐步澄清。于是,用文本描述的角色、用例以及领域概念成为这一阶段的主要产品。
  • 然后,大家开始讨论如何为整个系统划定核心域和各子域,并尝试用UML类图和顺序图建立系统的模型、切分BC。最后,我们开始尝试用TDD实现系统原型,并借由一些简单的测试脚本,通过命令行窗口模拟系统的行为,帮助客户更好地进行反馈。在此之后,由获取反馈-改进模型-重构实现构成的每个迭代小节,成为我们的主要工作,也使得系统逐渐丰满和完整。
  • 当应用层接口和系统模型相对稳定后,我们才开始着手UI和持久层的设计实现。在MVP、MVVM模式和ORM工具的帮助下,这个阶段通常没有太大的难度。随后,是以应用层接口为单位的集成测试和性能分析,期间还会穿插若干次的重构和单元测试,通常这是最恼人的一步——眼看胜利在即,仍只能望梅止渴。接下来,是完整系统的模拟运行,一帮人整天想着法子折腾它,并记下每一次的错误和异常,然后又进入新一轮的重构和测试。
  • 最后,大家伙再加把劲,编制用户手册、完成代码和资料存档、再完成生产环境的部署,就算万事大吉了。

在整个开发流程里,如果说建模、实现、测试等等,都还在我的控制之中,那么在与客户交流时,我不知道有多少人象我一样,时常感到引导话题或者讨论方向时那种深深的无力。客户总会认为你是专家,他说的你都能理解、都能实现,无论其表述是如何的天马行空。而这种信马由缰式的讨论,也对我划分子域、切分BC带来了很大的困扰。可能有的人会说,你为什么不每次都拟定一个讨论的主题和大致的提纲呢?而我能说的是,嗯,是的,我准备了,可是客户思维的发散性和跳跃性永远会给你带来意外的“惊喜”。另一方面,是伴随系统模块逐渐增多后迅速膨胀的各类测试,以及繁琐的UI测试,给我们的维护与迭代带来的巨大心理和工作压力。

所以,我希望有一种方法学的指引,帮助我们更加专注于每次讨论的主题,帮助我们更好地发现和切分BC。在张逸的《如何识别Bounded Context》一文中,我找到了方向。在文中,他倡导以领域中的Who-What-Why-When-Where-How为媒,以Actor为驱动,不断堆砌出系统的关键用例,再以对用例的分类划定问题的边界,最终由此催生不同的BC切分。

这更加深了我对“讲好故事、划好边界”的认识,并由此引导我迅速地转入了对BDD、Specification by Example的学习。

初识BDD

关于BDD,我在前一篇《行为驱动开发BDD概要》中已经做了一份《BDD in Action》的书摘,并按图索骥尝试了.NET平台下的Specflow+NUnit组合。由于有TDD的基础,所以这个过程的后半实现部分并没有想象中的困难,反而是在前半分析部分——理清Specification并写出Feature文件,由于找不准Why-Who-How-What,而让我犯了不少迷糊。

起初,我的想法很简单,只要能搭出这个Why-Who-How-What的框架,剩下的工作将只是往里填充内容而已。所以我在使用Impact Map这个脑图工具时,一开始就拘泥于BDD in Action作者在第三章的阐述,只能教条式地从该书第72页列举的4点主题去寻找Why:

  • Increasing revenue 增加收入
  • Reducing costs 减少支出
  • Protecting revenue 保护收益
  • Avoiding future cost 避免将来的开销

于是,我得到了一张象下面这样的图。这显然与我们的目标相距甚远,因为这样的Business Goal并不够明确具体,不是完全可量化的,也无法直接作为验收标准。但这并不是说这样的做法是错误的,只能说是仍不够精确的。因为它对于我们理解用户需求还是有一定帮助的。

对结合BDD进行DDD开发的一点思考和整理

而且在这个过程中,如果说找出Why和Who还不算难事,那么要挖出How与What则让我大费周张。在书中,作者对How的部分引入了Capability的概念,并建议参照Liz Keogh的观点,以”to be able to”的形式来描述。而对What的部分,作者则引入了Feature的概念,强调这里描述的应该是系统能以何种方式帮助特定角色实现相应的Capability。

于是当我按图索骥时,却再一次陷入了教条主义的泥沼,搞不清什么应该放在How、什么又该放在What里。而致使我陷入被动的另一个因素,则是我发觉中英文在表达How与What时语义上存在的混淆。比如”它会怎样改变?(How should it change?)“与”它会发生什么样的改变?(What should be the change in it?)”,你能分得清这两者有什么真正的区别吗?

尽管我反复阅读BDD in Action第3章和第4章,但我那可怜的英语阅读能力并未能帮助我顺利摆脱How与What的纠结。

柳暗花明

好在网络是强大的、墙外的风光总有美好的。在email、twitter以及group的轮番轰炸下,一切开始出现转机。首先是Julian May向我推荐了Gojko Adzic所著的《Specification By Example》一书,然后是作者Gojko、网友ChrisMatts等人的回复,为我指明了学习的方向。

在此有点题外话,一是BDD in Action著书于Specification by Example之后,其第3、4章的内容已经撷取了后者的精华,却不知为何没有使用Gojko所提倡Impact和Deliverable的概念。二是Specification by Example已有中译本《实例化需求:团队如何交付正确的软件》,由人民邮电出版社出版,我的英文书摘亦将另行整理,以作对照。

Specifications by Example为我呈现了一个全新的视角:以Specification、Automation Test以及Living Documentation等文档为中心的交付模型(Documentation-centric model),以及一个由下图所示的交付模式。

对结合BDD进行DDD开发的一点思考和整理

随着阅读的不断深入,Why-Who-How-What被Goal-Actor-Impact-Deliverable所代替,其含义也越发地清晰起来:

  • Why – Goal - Why do you want this product? 为什么会需要这款产品?
  • Who – Actor - Who can help or obstruct us? 谁会帮助或妨碍我们?
  • How – Impact - How will they be impacted? 这会对他们产生什么样的影响?
  • What – Deliverable - What will the feature do to support the desired impact? 这个功能将如何产生期望中的影响?

之前那种依赖于语言本身对How和What进行的讨论,此时便显得如此拙劣。对此,Gojko有这样一段精彩的阐述:

But now I advise people not to think about that, instead to think about impacts and deliverables. Deliverables are stuff you do in your zone of control, impacts are how those things influence actors in your sphere of influence. You can call these questions "what/how" or "how/what" or not think about the questions at all, depending on what the group in the room understands better.

它回答了之前一直困扰我的问题。即What是在开发团队控制范围内的,能对Actor产生影响的那些事物。这些是我们努力就一定能做到的,比如系统能提供的具体功能。而How则是我们期望的、能对Actor产生的影响。这种影响相对开发团队而言,是间接和被动的。对此,Gojko用一个手机App进行了形象的比喻:

Impacts - this is your sphere of influence - things you don't control directly, but things you think you can influence. Deliverables - this is your zone of control. The big question to differentiate between the two is: is there an assumption here, or are we guaranteed to achieve it if we decide to do it. Eg if we decide to build an iPhone App, we will. But if we decide to get people to buy the app on the appstore more, regardless of all our efforts that might not happen. The first one is in our zone of control, the second is in the sphere of influence.

期间我对Business Goal也重新进行了认识。BDD in Action曾用Specific(特定的)、Measurable(可量化的)、Achievable(可实现的)、Relevant(相关的)、Time-bound(有期限的)对其进行说明,而发掘Goal的工具真的是如此简单——不停地问Why……然后,我又重绘了之前的图:

对结合BDD进行DDD开发的一点思考和整理

再补上MelvinPerezCx对Impact Map与各种产出件之间关系的一张好图:

对结合BDD进行DDD开发的一点思考和整理

继续前进

解决了Impact Map的问题,下一步就是思考如何将DDD的战略思考与BDD的战术实现结合起来,引导整个开发的方向。这一切当然地需要用实践来解决,在前进中才能解决遇到的问题,所以暂且只得到以下的一个思路。

DDD+BDD的开发流程:

  1. 项目伊始,使用Impact Map勾勒出系统的功能轮廓。
  2. 找出Key Example,编写相应的Specification及Feature文本。
  3. 对领域进行划分,将Specification划入不同子域。
  4. 统一Specification中的概念,建立特定子域内的通用语言。
  5. 结合Specification的描述和子域的划分,定义最初的BC结构。
  6. 选择核心BC,利用SpecFlow+NUnit等工具,用代码实现Specification描述的Feature。
  7. 通过捕捉Feature描述中的Given-When-Then,得到领域事件列表。
  8. 逐个BC重复第6、7两步,得到领域模型的原型。
  9. 不断审视和完善Specification,中间可能夹杂着BC的调整,使领域模型不断演进和丰满。
  10. 利用SpecLog等工具生成领域模型的Living Documentation。
  11. 通过自动化测试,审视业务流程是否达到预期。
  12. 从领域模型由内而外,编写应用接口层、UI层和基础设施层的Specification。
  13. 使用BDD工具,实现这些外围Specification的自动化测试和文档生成。
  14. 经过若干次迭代,所有核心和外围Specification都得到完美实现。
  15. 系统通过集成测试和试运行,顺利交付并完成归档。

对这个流程中,有几点需要说明:

  • 第3、第5步,对子域的划分和BC的切分是难点,因为两者并不一定是一一对应的关系。只是因为Specification的出现,可以让我们用更具体的示例来进行这种划分,而不是凭空地臆想。
  • 第6、第7步,在实现Specification的自动化测试时,应高度关注各种领域事件及其触发的结果,还要从saga的角度去思考这些事件之间的协调配合。这在CQRS Journey一书中有相应的例子可作参考。
  • 第6、第7、第8步,在编码的同时配合不断完善的UML类图和顺序图,应该能更好地反映系统全貌,或者帮助我们发现遗漏。
  • 第12、第13步,UI的自动化测试一直是难点,也是我的弱项。尽管Specification by Example中有对UI自动化测试的专门阐述,但仍需要实践才能真正变成自己的东西。
  • 在任何时候,专注核心域都是要放在第一位的。包括Specification的编写和Example的选择,都要以此为原则,才能避免陷入实现细节的泥沼。

写在最后

这是我近期学习的一些收获,不仅思考很不成熟,亟待实践的检验,而且文中也难免有个人认识上的谬误之处,还请各位看官批评指正。另一方面,由于BDD是一种战术意义的Development方法学,需要时间去掌握相关的一些工具,所以我会尝试用一个相对简单的项目练手,届时再来补充完善此篇所述之内容。