实现领域驱动设计
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

示例上下文

我们的SaaSOvation团队所选择的3个限界上下文最终将与各自所对应的子域形成一对一的关系。然而在项目开始时,他们并没有做到这一点。在汲取了经验教训之后,最终的结果如图2.7所示。

img

图2.7 一个拥有清晰子域的示例限界上下文

接下来我们将讲到,这3个模型是如何形成一个实际的、现代的企业级解决方案的。在实际应用中,一个项目总会有多个限界上下文,它们之间的集成对于当今的企业来说是一个重要的环节。除了限界上下文和子域,我们还需要掌握上下文映射图来解决集成(13)问题。

让我们来看看示例DDD项目中的这3个限界上下文[5]。他们分别是协作上下文、身份与访问上下文和敏捷项目管理上下文。

协作上下文

在当今这个经济急步向前的时代下,业务协作工具对于创建一个协作式的工作环境来说是非常重要的。任何有助于增加工作效率、增进知识传递、促进观点共享的方法都是企业成功的促成因素。企业总想寻找最好的在线协作软件,不管是用于大众社交的协作软件,还是目标用户相对较少的专业化协作工具,而SaaSOvation也希望从这个市场中分到一杯羹。

负责设计和实现协作上下文的核心团队需要在第一次软件发布中包括以下功能:论坛、共享日历、博客、即时消息、wiki、留言板、文档管理、通知与提醒、活动跟踪和RSS订阅。虽然核心团队要开发的功能很多,但是每一个协作工具都可以单独使用。但无论如何,这些工具都属于同一个限界上下文,因为它们都是协作的一部分。遗憾的是,本书不能向读者讲解所有的协作工具,我们将选择性地讲解部分协作工具的领域模型,即论坛和共享日历,如图2.8所示。

img

图2.8 协作上下文。通用语言决定哪些组件应该位于边界之内。有些模型未予显示,用户界面(UI)和应用层组件也未予显示。

在项目启动时,协作项目组成员只采用了战术DDD,当然他们正在学习更好的DDD实践。事实上,他们所使用的DDD只能算是DDDLite,即使用战术模式来解决技术上的问题。诚然,他们正试图在协作项目中使用通用语言,但是他们不知道的是:此时施加给模型的限制太大,而他们还不能突破这些限制。因此,他们错误地将安全和权限相关的逻辑引入了协作模型中。而现在,团队成员已经意识到,他们并不像以前那样期待着将安全和权限逻辑加入模型中了。

早些时候,他们并没有过多地去考虑这种危险。然而,在没有对安全管理进行集中化的情况下,发生这种情况是预料之中的。团队成员们将两个模型混合成了一个,不久之后他们便尝到了苦头。在实现代码中,他们在核心业务逻辑中去检查客户的权限:

我刚才看到火车残骸(Train Wreck)了吗?

有些开发者将同一行中的多个方法调用链称为“火车残骸”,比如user.person().name().asFormattedName();而另外有人则认为这种方式具有很好的代码表现力。我并不站在他们的任何一边,这里我的关注点在混乱的领域模型上,“火车残骸”则是另一个话题了。

以上代码所展现的确实是一种不好的设计。我们不应该在这里引用User,更不用说资源库(Repository,12)了,甚至连Permission都不应该在此出现。原因在于,团队成员错误地将这些概念设计成了协作模型的一部分。另外,这种错误的设计导致他们忽略了本应该存在的建模概念——Author。他们并没有把有关联的属性放在一个值对象中,而是使用一些分离的数据元素来解决问题。此时,团队成员所思考的并不是协作模型,而是一个与安全相关的模型。

这并不是一个孤立的现象,每一个协作对象都存在相似的问题。这时,“大泥球”的风险似乎不可避免,还好,团队成员决定修改代码了。此外,团队成员还希望从权限管理转向基于角色访问的安全管理方式,他们应该怎么做呢?

作为敏捷开发方法的使用者和敏捷项目管理工具的开发者,团队成员并不害怕及时地对代码进行重构。现在的问题是:他们应该采用哪种DDD模式来解决目前所面临的问题?

团队中有人花额外的时间详细了解了[Evans]中的战术模式,结论是,这些模式并不是他们需要的答案。他们尝试过照着那些模式创建由实体和值对象组成的聚合,同时也使用了资源库和领域服务(Domain Service,7)。然而,他们缺少的是另外一些重要的东西,这可能使他们将关注点转向[Evans]•的后半部分。

最终,他们采取了这种做法,然后他们发现了一些功能强大的技术。当看到“Part III:Refactoring toward Deeper Insight”[Evans]时,他们发现,DDD所能提供的远比他们先前所想象的要多。现在,他们知道应该给予通用语言更多的关注了。通过和领域专家一起工作,他们可以创造更接近自己思维模型的软件。然而,这依然没有解决如何将安全相关逻辑从协作领域模型中分离出来的问题。

[Evans]这本书中进一步讲到了“Part IV:Strategic Design”。团队中的一员从中找到了解决方法。其中一种工具便是上下文映射图,这可以使它们更好地理解当前项目所处的状态。虽然画一个上下文映射图非常简单,但这却是很大一个进步,最终清除了开发团队面临的障碍。

现在,团队成员希望做一些临时性的改进来增强领域模型的稳定性,他们有如下选择:

1.他们可以将模型重构成职责层(Responsibility Layers)[Evans],即将安全和权限功能下放到比当前模型更低的一个逻辑层。这并不是最好的解决办法,职责层主要用于大型模型。虽然这些层得到了正确的分离,他们依然应该保留在模型中,因为他们都是核心域的一部分。另一方面,团队所处理的概念并没有得到适当地定义,况且这些概念原本就不应该属于核心域。

2.另一种方法是采用隔离内核(Segregated Core)[Evans]。要达到这样的目的,我们可以在整个协作上下文中搜索对安全和权限相关逻辑的引用,然后将身份和访问组件分离到另一个单独的包中。当然,这并不能生成一个单独的限界上下文,但是却可以使团队朝着这样的目标更进一步。这种做法正是团队所需要的,因为这种模式就是这样定义的:“使用隔离内核的时机是当你有一个非常重要的限界上下文,但是其中模型的关键部分却被大量的起辅助作用的功能所掩盖了。”这里的安全和权限管理功能显然只是起辅助作用的。团队成员最终意识到,他们需要一个单独的身份与访问上下文作为协作上下文的通用子域。

要着手创建隔离内核并不简单,它可能需要数周的时间。如果措施采取不当,或者未得到及时重构,结果可能导致代码库变得越来越糟糕,修改代码也将变得异常困难。公司的领导层认为,将软件功能分离到新的业务服务可能孵化出一个新的SaaS产品,因此他们对这种做法给予了肯定。

重要的是,该团队现在明白了限界上下文和内聚领域模型的价值。采用额外的战略模式,他们可以将可重用的模型分离到单独的限界上下文中,并在不同的限界上下文之间进行合适的集成。

可以想象,未来的身份与访问上下文和当前嵌入式的安全和权限管理机制是不同的。为了重用而设计将迫使团队将关注点转向更加通用的模型上。开发这个通用模型需要另一个专门的团队,该团队和协作上下文团队是不同的,但在组建之初有可能从限界上下文调入一些成员。这个专门的团队也可以使用不同的实现战略,比如可以采用第三方产品和特定于客户的集成等。但是,对于嵌入式的安全管理机制来说,这种方式太不可达了。

由于开发隔离内核只是一个临时性的步骤,我们这里并不关注它的结果。简单来讲,隔离内核就是将所有与安全和权限相关的类迁移到一个单独的模块中。然后,在调用核心领域逻辑之前,使用应用服务去检查用户的安全权限。这样,在核心域中只需要实现和协作行为相关的功能。应用服务负责安全权限检查和对象转换:

Forum对象变成:

以上代码移除了User和Permission,并将关注点完全限制在协作活动上。当然,这依然不能算是完美的实现,但它至少为将来重构成单独的限界上下文做好了准备。现在,协作上下文的团队将所有与安全和权限相关的模块和类型从该上下文中移除,然后逐渐采用新的身份与访问上下文。他们将安全管理机制集中化和重用化的目标指日可待了。

我们可以做这么一个假设:团队在项目启动时选择了另外一个方向,通过为每种协作工具(比如将论坛和日历分开建模)创建各自的限界上下文,团队得到了一系列的微小型限界上下文。这种情况是如何产生的呢?由于多数协作工具并不与其他工具产生耦合,每一种工具都可以被部署成一个自治的组件。通过将每一种协作功能放在单独的限界上下文中,团队可以创建多个自然部署单元。然而,对于部署来说,创建这么多个领域模型却是没有必要的,这顶多满足了通用语言的建模原则。

事实上,协作项目团队维持了单个模型,而为每一个协作工具创建了单独的JAR文件。使用Jigsaw模块化功能,他们为每种工具都创建了基于版本的部署单元。除了这些自然分离的JAR文件之外,团队还需要另一个JAR文件用于部署共享的模型对象,比如Tenant、Moderator、Author和Participant等。这种方式有助于建立一套统一的通用语言,同时对架构和应用程序管理来说也是有益的。

有了这样的理解,让我们来看看身份与访问上下文是如何产生的。

身份与访问上下文

现在,多数企业级应用程序都需要某种形式的安全和权限组件,这样的组件用以对用户进行认证和授权。正如我们在前面所分析的,一种幼稚的做法是将这样的组件嵌入到每一个离散的系统中,这将导致每一个系统都产生筒仓效应(silo effect)。

牛仔的逻辑

LB:“你的谷仓并没有上锁,但是没有人偷你的粮食?”

AJ:“我的狗Tumbleweed帮我管理着权限呢,这是我自己的筒仓效应。”

LB:“我并不认为你理解了本书。”

一个系统的用户并不能轻易地和其他系统的用户发生关联,即便是同一个人使用不同的系统也是如此。为了避免“谷仓”中的粮食溢出到整个业务范围内,架构师需要对安全和权限的管理进行集中化处理。要达到这样的目标,我们可以购买一套身份和访问管理系统,也可以自己开发一套。至于选择哪种方案,这取决于该系统的复杂性、时间和成本等因素。

要纠正CollabOvation项目的身份和权限管理需要多个步骤。首先,团队需要使用隔离内核[Evans]模式对系统进行重构,请参考“协作上下文”一节。这个步骤确保CollabOvation中已经没有与安全和权限相关的逻辑。然而,他们得出了另外的结论:身份和访问管理最终应该位于它们自己的上下文边界内,这将需要更多的工作。

这构成了一个新的限界上下文——身份与访问上下文。通过采用标准的DDD集成技术,该上下文可以被其他限界上下文所使用。对于消费方来说,身份与访问上下文是一个通用子域,该产品被命名为IdOvation。

如图2.9所示,身份与访问上下文向多租户订阅方提供支持。在开发一个SaaS产品时,这是无须多说的。每一个租户及其拥有的每一个对象资产都有唯一的身份标识,这在逻辑上将不同的租户分离开来。系统的使用者只能在收到邀请时自行向系统注册。系统通过认证服务来保证安全访问,而密码通常是被高度加密过的。用户群和嵌套群可以在整个组织范围之内完成复杂的身份管理。通过基于角色的权限机制来管理对系统资源的获取,这种方式是简单的、优雅的,同时又是功能强大的。

更进一步,当有我们关心的状态由于模型行为而发生改变时,系统将发布领域事件(8)。这些领域事件通常采用“名词+动词”的形式来命名,动词应该是英文中的过去分词形式,比如tenantProvisioned、UserPasswordChanged和PersonNameChanged等。

在下一章的“上下文映射图”中,我们会讲到如何使用DDD的集成模式将身份与访问上下文用于其他两个示例上下文。

img

图2.9 身份与访问上下文。边界之内的所有模型都遵循通用语言。在该限界上下文中还存在其他的组件,有些位于模型层中,有些位于其他层中,这里未予显示。同样,用户界面(UI)和应用层组件也未予显示。

敏捷项目管理上下文

敏捷开发提倡的一些轻量级方法使其迅速地流行起来,特别是2001年敏捷宣言出来之后。当初,SaaSOvation公司决定在CollabOvation项目之后再开发一套敏捷项目管理系统,下面是该系统的项目进展情况……

在过去的三个季度中,CollabOvation产品取得了可观的销售业绩,SaaSOvation公司的收入也好于预期,此时公司启动了ProjectOvation项目。这是一个新的核心域,CollabOvation团队的顶尖开发人员将被调到ProjectOvation项目中以传授DDD经验。

ProjectOvation产品关注于敏捷项目的管理,使用Scrum作为迭代和增量式的项目管理框架。ProjectOvation遵循传统的Scrum项目管理模型,其中包括产品、产品负责人、团队、待定项、计划发布和冲刺等。对待定项的评估通过对业务价值的分析来确定。

CollabOvation和ProjectOvation并不会朝着两个完全不同的方向发展下去。SaaSOvation公司及其董事会非常看好在敏捷软件开发中引入协作工具的前景。因此,CollabOvation的功能将作为附加服务加到ProjectOvation中,此时的CollabOvation便是ProjectOvation的一个支撑子域。Scrum的产品负责人和团队成员将对发布和冲刺计划进行讨论,同时他们还将使用共享日历等。预计在将来,ProjectOvation项目还会加入协作资源计划功能,但是首先要满足的是那些首要的敏捷管理功能。

ProjectOvation的技术负责人一开始打算将ProjectOvation功能作为CollabOvation模型的扩展,采用的方法是在源代码控制系统(Revision Control System,RCS)中新建一个分支(branch)。这种做法是一个很严重的错误。

幸运的是,技术人员从先前混乱的“协作上下文”中得到了经验教训,他们认为将敏捷项目管理模型和协作模型合并在一起是一个很大的错误。可以看到,此时的团队已经开始采用DDD的战略设计来思考问题了。

如图2.10所示,在采用了战略设计之后,ProjectOvation团队认为软件的消费方应该是产品负责人和产品团队,这是合理的。毕竟,这些才是Scrum参与者所扮演的项目角色。用户和角色在另外的身份与访问上下文中进行管理。通过使用该上下文,订阅者通过自助式的服务来管理他们自己的身份和访问信息。软件中的管理功能使得项目管理者,比如产品负责人,确定项目的团队成员。在角色管理适当的情况下,产品负责人和团队成员都可以在敏捷项目管理上下文中进行创建。

对ProjectOvation的一个需求是:通过一系列自治的应用服务来完成业务操作。开发团队希望ProjectOvation对其他限界上下文的依赖程度尽可能的小。总体上来说,ProjectOvation是可以自行完成操作的,即便是IdOvation和CollabOvation由于种种原因不能工作了,ProjectOvation是可以自行运行的。当然,在这种情况下,有些信息可能无法得到及时同步,但系统至少是可以工作正常的。

上下文赋予每个术语特定的含义

一个基于Scrum的产品可以包含多个待定项实例。这里的产品和我们在电子商务网站上购买的产品是不同的。我们是如何知道这一点的呢?因为有上下文。在敏捷项目管理上下文中,我们知道产品所表示的意思,而在在线商店上下文中,我们知道,产品表示另外的意思。我们并不需要将产品命名为ScrumProduct,因为我们已经处于敏捷项目管理上下文中了。

img

图2.10 敏捷项目管理上下文。此时的通用语言主要关注于基于Scrum的产品、迭代和发布等。出于可读性,有些组件,比如UI和应用层组件并未予以显示。

基于SaaSOvation开发团队所获得的经验,PorjectOvation的核心域已经可以开始开发了,该核心域中包含了产品、待定项、任务、冲刺和发布等。此外,我们感兴趣的还有他们在对聚合(10)进行建模时所学到的经验教训。