您的位置:知识库 » 软件设计

基于上下文图的策略性领域驱动开发

作者: Alberto  来源: InfoQ  发布时间: 2012-05-30 22:38  阅读: 2558 次  推荐: 0   原文链接   [收藏]  

  英文原文Strategic Domain Driven Design with Context Mapping

  作者:Alberto Brandolini 译者:韩锴 发布于 2010年4月6日

  简介

  当应用程序逐渐变得庞大和复杂后,很多面向对象建模的方法都达不到非常好的可伸缩性。上下文图(Context Mapping)是一种通用目的的技术,作为领域驱动开发大家族的一名成员,它帮助架构师和开发人员管理他们在软件开发项目中不得不面对的形形色色的复杂性。与其他广为人知的DDD模式相比,上下文图可以应用在任何软件开发的场景中,在开发者进行策略性决策时,为他们提供一个高层视图,比如是采用全套的DDD实现,还是根据项目的特定条件进行取舍等。

  在这篇文章中,我们将探讨界限上下文,以及如何在构建上下文图时应用它们,来支持软件开发项目中的关键决策。

  多个模型共存

  领域驱动开发花了很大力气强调一件事,即维护应用程序模型的概念完整性。要做到这一点,需要很多因素:

  • 一种敏捷的流程,确保能从用户和领域专家那里频繁地获得反馈,
  • 确保有若干领域专家在场,并且与他们开展创造性的合作,
  • (在应用和测试代码中)维护单一的、可共享的模型,并用通用语言精确地进行定义它,
  • 营造一种开放透明的环境,鼓励学习与探索。

  这些对于营造一个可以让高质量的设计繁荣发展的环境至关重要。即使是这样的环境,那些常见的DDD元素,比如实体、值对象、聚合,也会逐渐地形成一个复杂领域模型。所以,如果不对模型的概念完整性进行妥协的话,传统的DDD方法也不能盲目地应用在一个无限大的领域模型中。

  如图1所示,在DDD中,通用语言的关键责任是对模型进行完整性检查。无论是与领域专家的讨论,还是最终的实现代码,都可以通过使用相同的术语,并将领域知识清晰准确地进行定义,以此来保证团队中的每位成员可以分享都相同的领域知识和软件。

  图1. 通用语言应该是用于表达模型的唯一语言。团队中的每位成员应该对每个特定术语达成一致。这些术语不能有歧义,也不允许在不同角色间进行翻译。

  代码是表达模型的主要形式。虽然其他一些工件或许也能捕获需求或者局部设计,但是只有代码自身才会与应用程序的行为永远保持一致。不过这种看上去美妙的建模方式其实非常脆弱:如果满足了前面提到的四条要求,它能做到,但是不能被无限地扩展。真正让模型得以最大程度地扩展,并且不必牺牲其概念完整性的方法叫做“上下文”。

  了解“界限上下文”

  在领域驱动设计的世界里,“上下文”是这样定义的:

  “一个单词或一个句子所出现的环境,这个环境会反过来影响它们的含义。”

  这段定义初看起来相当含糊。它并没有直接告诉我们“上下文”的大小、形状或者其他什么特性。但是最后我们又发现这个定义其实非常准确地描述了“上下文”是什么,如果要想窥得其全貌的话,大概还需要一些具体的例子。

  示例1:术语相同,含义不同

  第一个例子很简单,它示范了在术语层面出现歧义的情况。有些词汇根据不同的使用场景,会有不同的含义。

  假设我们正在开发一个基于Web的个人金融管理程序(PFM)。该程序可能用于管理银行帐户(Account)、股票、储蓄,未来可能用于追踪预算和开销记录等等。

  在这个程序中,领域术语“帐户”可能是指不同的概念。谈论银行的时候,一个“帐户”在逻辑上其实是“金钱的容器”;于是我们自然而然地为相应的类加上“余额”、“帐户号”等属性。但是,在“Web应用程序”的上下文中,术语“帐户”会有非常不同的含义,它和用户的认证、可信度有关。如图2所示,相应的模型将是完全不同的。

  图2. 一个出现歧义的简单场景:当术语“帐户”应用在不同的上下文时,它的含义可以是完全不同的。

  这应该是我们在对应用程序建模的时候,所遇到的最简单的歧义场景了:一个术语,有两个与上下文相关的含义。这个问题的解决办法通常是在类的全名(类名或者包名)前面加一些前缀,以此来划分名字空间。但是在概念层面上,必须清楚我们在和两个上下文打交道,有时不同上下文之间的区别很大,足以防止开发人员犯错误,但有时这样的区别却非常微妙。

  不过,在类名层面上解决问题的方式并不能用在所有的情况下:在银行的领域里,术语“银行帐户”或许已经存在了,但却有不同的含义;或者领域专家坚持使用“帐户”作为术语。此时切记不要发明一个特殊的两头折衷的术语,或者在专家术语和代码之间引入一个“翻译层”。否则你将不得不面对两个独立的上下文。

  绘制第一份上下文图

  当歧义真的到来的时候,我们需要一种工具来让开发团队明白,应用程序中正存在着两个不同的上下文。“歧义”是通用语言的头号大敌,我们必须铲除它。消除歧义的最好办法就是在上下文图中,将领域结构分拆在多个界限上下文中。

  图3. 包含两个领域上下文的上下文图

  按照领域驱动设计一书的描述,上下文图是用于将上下文边界变得更清晰的重要工具。其基本的想法是在白板上画出上下文的边界,然后选择性地将相关类的领域术语填写在上面。它不是一幅绘制精美的UML图:它是一个可用的工具,允许我们描绘那种模糊不清的状况,因此它自身看上去模糊不清也就不足为其了。

  示例2:概念相同,用法不同

  还有一种更加令人困惑的情况,就是底层的概念相同,但是使用的方式不同,最终导致了不同的模型。银行帐户的模型是一个BankingAccount类,如图4所示。

  图4. 精简版本的BankingAccount类

  通常,有些PFM应用也允许我们管理支付行为,并且持有一个收款人(Payee)注册器。在这个场景中,收款人可能与一个或者多个银行帐户关联,但是对于收款人来说,我们既不能获取其银行帐户的内部情况,也不能在该帐户上触发任何操作。那么将“收款人帐户”与刚刚定义的BookingAccount类关联在一起是否正确呢?

  图5. Payee类与BankingAccount类

  恩......这听上去有些道理:毕竟它们都是相同的概念,在现实世界中,我们的帐户和收款人的帐户甚至会处在同一个物理上的银行里。另一方面,这样做似乎又不完全正确:因为我们不允许调用收款人银行帐户的任何操作,也不能追踪他们的任何信息。更糟的是,这样做了后,可能会在我们的程序中埋下一个概念的错误。

  我们应该如何做?我们应该再一次回到应用程序的两个不同的上下文里去:这一次我们可以采取两种不同的方式对同一个领域概念进行建模,因为对领域概念的两种使用场景明显不同,每一种都需要一个不同的模型。BankingAccount类仍然允许我们执行(或者跟踪)特定的操作(比如存款与取款),同时另一个独立的PayeeAccount类可能有一些和BankingAccount相同的通用数据(比如accountNumber),但是有一个简化的模型和完全不同的行为(比如我们不能访问收款人的余额信息)。图6所示的正是这种场景:尽管“银行帐户”有着清晰的含义,其底层概念也是惟一的,但是在应用程序中却以不同的方式被使用着。

  图6. BankingAccount和PayeeAccount类

  这看着似乎挺明显的,其实不然。当你设计类图,或者使用UML建模工具时,你可能很自然地让收款人具有一个bankingAccount属性,而且会庆幸“我刚好有一个这样的类”。Pavlovian试图去除代码中的重复,有时,它的作用会弊大于利。

  如图7所示的上下文图,可以用于表述上面讨论的示例。注意,只要我们关于环境的知识在增加,就将它反映在图中。在这个例子中,我们将PFM的应用上下文分成了“银行”和“开销跟踪”。

  图7. 非常简单的上下文图:画上了领域模型区域间的轮廓,可以看出在这些区域内保证了概念的完整性

  在这个例子中,两个上下文拥有一些逻辑上重叠的区域,即“银行帐户”的概念,它在应用程序的不用区域中,使用方式也不同,这意味着我们要使用不同的模型。但是两个模型又可能有非常紧密的交互。上下文图除了能帮助我们保证模型的概念在不同上下文边界内的完整性,它还能帮助我们关注在不同上下文之间出现的情况。在这个例子中,假设同一个团队正在两个上下文上同时工作,我们就需要让团队中的每位成员的明确两个上下文的区别,并且就两个上下文中出现的术语和概念,分享同一个转换的映射关系。

  示例3:外部系统

  再来考虑一下PFM。很多这种应用程序都需要与某些金融在线服务进行数据交换。在这个例子中,银行会为家庭银行服务提供实时的访问。其他的例子还包括允许用户下载通用标准格式(比如Money或者Quicken格式)的银行对帐单。但是从上下文图的视角来看,无论是交互活动还是通讯的方向(单向或是双向),并不重要。有一件事是要关注的,我们有了不同的模型。图8展示了PFM与在线银行应用程序的交互行为。

  图8. 在上下文图中,与外部应用的交互行为很自然地需要独立的界限上下文

  即使设计两个模型之初是让它们展现相同的数据(至少在一定程度上),但随着时间的推移,它们还是会受到不同因素的影响,而且它们也会用于不同的目的。因此,分离上下文边界是必须的。如果假设用户档案(User profiling)模块是由第三方库实现的,那么示例1也能够归入到这一类中。

  管理多个上下文

  当应用程序跨越了多个上下文后,我们必须管理上下文之间的关联。不同的界限上下文之间的关系,通常是我们深入观察项目的线索。

  有一件事情非常关键,即两个上下文之间的联系是有方向的。DDD用两个专门的术语表述它们:“上游(upstream)”和“下游(downstream)”,一个上游上下文会影响到相应的下游上下文,但是反过来就不一定了。这不仅体现在代码上(一个库依赖于另一个),还体现在技术含义较少的因素上,比如进度、对外部请求的响应,比如,当在线银行服务更改了API或者其他什么原因,我们的PFM银行应用程序都必须要快速地更新。所以我们的PFM上下文应该是下游的,而在线银行服务很明显就是上游的了。图9演示了这两种领域上下文的关系。

  图9. 分离的上下文之间的Upstream-downstream关系

  如果外部系统发生变化,我们可以接受这种变化,来更新与外部系统通讯的方式。不过我们仍然需要一些保护措施隔离来自上游上下文的变化,保证我们自己的“银行”的上下文的概念完整性。DDD包含了几种组织模式,帮助我们描述和管理不同的上下文交互方式。最适合我们在这里使用的是模式叫做反腐化层(Anti-Corruption Layer,ACL),它会在代码层面上实现显式的转换,转换可以在两个上下文之间,或者在“银行”上下文的外部边界上完成。这不仅局限于技术上的转换,比如Java转化为XML,同时也是一个很合适的机会,能够管理各个模型之间的所有微妙的不同。如下面的图10所示,我们在上下文图上添加了ACL。

  图10. PFM程序边界上的反腐化层,防止在线银行服务的变化影响到我们的边界上下文

  很明显,一个外部系统需要一个独立的上下文。然而对于一个已有的遗留组件,通常也伴有一个非常难以修改的模型。尽管遗留组件是在我们组织内部来维护的,甚至这个模型也会受到不同因素的影响,它会被其他的上下文所使用。如果必须和遗留系统进行交付,不同模型之间的转换应该放在一个不同的界限上下文里。

  上下文图中还有其他的关系吗?我们能够根据相关的DDD模式对它们进行分类吗?如果假设开发活动是在单一的团队内进行的,那这里的模式就不会引起太多的关注。但是,如果“银行”和“开销”是由不同的团队来维护的话,团队之间应该是一种合作关系:他们的开发会朝向一个共同的目标(这里谈论上游和下游没有意义,因为他们处于同一级别)。如果Web用户档案模块来自于外部,我们将会作为下游的上下文。

  图11. 加入了关系模式后的上下文图

  示例4:向组织扩展

  到目前为止,我们只考虑了包含一个开发团队的简单场景。在这种场景下,我们可以忽略沟通的开销,假设团队中的每位开发者都很明确“模型将会如何发展”(也许有些乐观)。更复杂的场景中还可能包含下面的影响因素:

  • 领域复杂度(需要很多不同的领域专家)
  • 组织复杂度
  • 项目跨时很长
  • 项目需要大量的人天
  • 涉及到很多外部的、独立的或者遗留的系统
  • 大型团队,多个开发组
  • 分布的、离岸的团队
  • 个人因素

  每个因素都会影响开发团队和组织的通讯方式,并最终影响到要交付的软件。

  每个独立的团队,尤其是一个处在敏捷环境的团队,团队内的成员间有很多共享信息的方式:面对面的交谈,多人参与的设计讨论、结对编程、会议、信息散播装置(information radiator)等等。但问题在于,当团队规模、人数增加后,这些技术很难再继续使用了,跨团队地共享模型的概念完整性也非常困难。

  毕竟,能够对模型保持统一看法,是沟通中相当成熟的方式,这涉及到对问题具有一致的理解,以及对可行解决方案大致相似的看法。在那些沟通不顺畅的场景下,“埋头干”很容易取代了“识别和确认”。这种沟通瓶颈带来的典型后果就是在同一个代码库中的不同地方散布着不同的类,它们做着基本上同样的事情。

  总有一天PFM应用会变得更大,这样就要有另一个团队(团队B)和我们一起工作(显然我们是团队A),他们开发一个名为“交易”的新模块。团队B可能在一个不同的房间、建筑物、城市、公司里,他们全心投入到新模块的开发工作上。如下图所示,团队A与团队B共享了一些代码,虽然他们很可能会使用彼此独立的代码。最后,团队B会写一些类(比如图12所示的A')来实现自己所需的功能,不过这些功能已经存在于类A了。

  图12. 当不同的团队访问相同的代码库时,他们会去关心模型上的不同部分。物理上的团队分割会令信息共享的效果大打折扣

  这是重复代码,万恶之源啊!在一个独立的、良好定义的、有界的上下文内,这是毋庸置疑的。但是由于某些原因,这种现象几乎会出现在所有复杂的项目中。这通常是个信号,告诉我们在项目的同一个区域内,或许存在没有恰当隔离的上下文。不过在有些时候,使用两个独立的上下文是组织领域模型更加有效的手段,而不会强迫两个不同的团队不断地去整合他们的模型。

  那么,我们如何在图上画出这些呢?上下文图反映了当前我们对整个系统的理解水平。一旦我们学到了更多东西,或者环境发生了改变,还会马上更新它。现在,我们还不能准确地预知接下来会发生什么,所以这就是“我们当前的理解水平”。

  图13. 尚未很好划分的“交易”上下文,它还需要进一步探索或更切合实际的设计决策

  图中的危险警告符告诉我们那里有些问题:两个上下文有局部的重叠,它们的关系还不是非常清晰。这可能是需要解决的第一类问题,可以尝试着在上下文内设置一个被广泛认可的、合理的关系,比如消费者-供应者、持续集成或者共享内核(Shared Kernel)。不过,这是明天的工作。上下文图是为今天准备的工具,而问题在今天还存在着,所以我们还把警告符号留在图中。

  不要被图中的颜色和阴影搞迷惑了。我不过是想让上下文图的打印效果更好一些。一个真实的上下文应该是很乱的,起码和你的项目一样乱。不过这个警告符提醒我们这里有一个危险区域,此处的上下文尚未被清晰地分离,事态很容易朝着“一团大泥球”发展(最有弹性的DDD组织模式),除非我们采取行动。

  一种非传统观点的视角

  上下文图迫使我们将非软件的因素也包含在整体考虑中,这样我们更能识别出一些污点热区,而这些热区在传统架构分析的观点中是“不在范围内”的。

  比如,组织内部的信息流通方式会在很大程度上影响最终的软件。通常,在小型组织中,组件自身的用途是定义上下文边界的主要因素,而在大型组织中,这个关键因素变成了沟通效率和项目组织方式。像Wiki、email或即时消息软件会给我们一种假象——团队中每位成员的知识都不断地保持着同步。但是我们都知道这只是个梦想罢了:在一个典型的大型项目中,我们不是Borg人(译注:源自《星际旅行》中的外星生物,所有Borg人的思想是互联的,可以完全共享知识)那样的智能联合体,很多人对于自己团队以外的情况知之甚少。

  在大型组织中定义上下文边界是一项颇具挑战的任务,但回报却也相当丰厚。很多时候,各个团队并不清楚多个模型存在的事实;之所以概念的完整性会频遭破坏,是因为只有很少人或者没有人看到完整的图景。绘制上下文图是一个不断探索的过程,很多部分的内容在首次尝试时都是不正确的,边界在初期也是很模糊的,还需要很长的路要走,才能获得一个更清晰的完整图景。

  图14. 上下文图的最新版本。不要指望它是“最终”的,我们总是会学到一些新的东西。

  涉及到的上下文还可能更多,比如“交易”模块可能需要链接到一些在线股票价格服务,但这是交易模块的事!这个上下文图是关于我们(团队A)的,我们的工作内容是“银行”和“开销跟踪”模块:我们只对直接关联的、会影响到自身软件的那些上下文感兴趣。

  一旦我们收集到更多的信息,上下文图就会变得更加清晰。正如前面提到的,只要认识到应用程序中存在着各种不同的模型,而且这些模型的完整性可以在一个良好定义的有界上下文中得以保存,这会为我们的领域建模的视角提供诸多益处。很多模型都在成长的过程中逐渐失去完整性,上下文图会在这个方面给予我们很多帮助。

  谈谈策略性DDD模式

  此处我们使用模式的方式略有不同:尽管定义是一样的——为一类反复出现的问题提供解决方案——但这些模式很少能展现出可供我们选择的解决方案。更可能的场景是,组织架构会决定模式,我们惟一的希望就是在事态走到死胡同以前识别出它们。有些时候我们有机会选择最好的选项,或者改变现有的状况,但是我们必须清楚的是,在组织级别的改变所需的时间可能已经远远超过了项目持续的时间,或者这个改变根本就是不可能的。

  如果你还在犹豫应该从那里开始,那么就从开发团队开始吧。对于有效地共享模型信息来说,一个团队应该是最大的组织单元。当识别出多个上下文后,可以由一个团队管理它们,这样很大程度上将问题归结为架构的抉择。

  每一种模式都有不同的开销:即使它们解决的是类似的问题(相近的上下文),也不能简单地交换。比如,反腐化层会在代码层面(一个额外层)上留下痕迹,并在组织里留下很小的痕迹。尽管Partnership或者“客户-供应者”模式可能需要更少的代码和一个单独的代码库,但是如果没有有效的沟通渠道和良好的过程的话,也很难应用起来。企图在没有合作的环境下安排执行Partnership模式,无异于自寻死路。

  结论

  让我们在回到“上下文”最初的定义上来——“一个单词或一个句子所出现的环境,这个环境会反过来影响它们的含义”——它非常准确,而且可以应用在设计层面、架构层面乃至组织层面上,却没有损失其准确性和有效性。尽管有些“对统一性的期望”是合情合理的,但是模型不能被无限地扩张。界限上下文提供了一种非常安全的机制,它允许模型在其内部不断变得复杂,同时又不牺牲概念的完整性。

  当把上下文图应用到大型的项目上后,它还可以显示出当前组织内的隐式边界,并提供一个来自第一手的、没有PS过的项目境况的快照。一个好的上下文图能让你看到所面对的不利条件的大致状况。可能你已经知道——但通常都是不知道——组织是否在扮演项目成功的绊脚石,即使项目还没有开始。

  作为一名顾问,我发现上下文图能够奇迹般地让我快速获取客户项目的细节。上下文图还充当了策略决策的支持工具(毕竟,这是“图”的本意)。上下文图提供了系统的全局视图,帮助我们关注在选择那些能在你的环境中真正可行的方案,而不是把钱浪费在对系统不切实际的构想中,这是UML或者架构图所做不到的。

  关于作者

  Alberto是来自Avanscoperta的一名咨询顾问和培训师。他致力于帮助客户管理软件开发的复杂度。他坚信只有那种全盘考虑的软件开发方法才能开发出有用的软件。

  英文原文Strategic Domain Driven Design with Context Mapping

0
0

软件设计热门文章

    软件设计最新文章

      最新新闻

        热门新闻