Skip to content

29 限界上下文(中):限界上下文怎样影响架构设计?

你好,我是钟敬。

上节课我们学习了“限界上下文”和“上下文映射”两个模式。

今天我们继续完成“工时管理”上下文,帮你进一步深化这两个概念。然后,我们会根据限界上下文来完成架构设计。由于这个迭代出现了多个上下文,所以架构设计的时候,我们首先要讨论的就是使用单体还是微服务。

为“工时项管理”上下文建模

沿用上节课的假设,你是开发组长,我是技术骨干,比你先学了一些DDD,我们都是这个项目的第一批成员,共同承担着架构师的职责。

在为“工时管理”上下文建模之前,我们先回顾一下之前的模型。

你思考了一下,问:“工时管理和其他上下文的有一个区别,就是这里多了泛化。那么拆成上下文以后,是不是还要原样保持这个泛化呢?另外,考虑到请假信息也要报工时这个新需求,是不是说请假记录也要作为工时项的子类呢?”

我说:“让我先试着画一画,再讨论吧。”于是我为工时项管理上下文画出了后面的模型图。

看到这张图,你有些疑惑地问:“泛化跑哪儿去了?”接下来,让我们一步一步看一下。

“员工”的上下文映射

先看个简单的,和“项目管理”上下文一样, 员工 也是从“基础信息管理”上下文映射过来的。

“工时项”实体

下面看比较重要的 工时项 实体。

请注意,由于现在我们是专门考虑“工时管理”上下文,所以我们不必过多地受到“项目管理”和“假期管理”中概念的“拖累”,而是聚焦在什么样的模型对实现工时管理功能最有利就可以了。

对于报工时的需求而言,项目、子项目、普通工时项等除了用来表示不同工时项的分类以外,并没有其他意义。所以,这时候用特性值来区分就可以了,而不需要泛化。

我们在工时项上加了一个指向自身的关联,表示工时项的父子关系。增加这一关联有这样几点考量。

第一,它可以用于表示项目和子项目的父子关系。

第二,就算不是项目的工时项,也可能有层次关系,现在的设计更有普适性了。

第三,这个关联可以表达任意深度的层次关系,而不是像项目那样只能表达两层关系。

我们再看看工时项实体里的这三个属性: 是否可在本级报工时是否要分配成员状态

这三个属性都是可以从“项目管理”上下文中 项目 实体的 工时粒度是否要分配项目成员状态 转换来的,而转换逻辑又不全是直接对应的。现在把这三个属性放在 工时项,就意味着,有的工时项,即使不是项目,也可能只能在下一级工时项上报工时、也可能要分配成员、也可能在工时项失效后就不能报工时。

总之,这个设计比原先更灵活,能用在更多的场景,对于 SaaS 系统更加适用。

“工时项”的上下文映射

再看看工时项的上下文映射。

这个映射的注解比我们之前遇到的要复杂一些,分成了三段。

第一段说的是普通工时项直接在本上下文中定义,也就是这种情况下,其实并不存在上下文映射。

第二段说的是和“项目管理”上下文之间的映射。由于逻辑比之前的映射情况更复杂,所以我们为<>增加了一个 “map logic” 属性,在这里可以用任何形式表示任何你想说明的映射逻辑。目前,我们说明了,项目映射成第一级的工时项,而子项目映射成第二级工时项。前面说的那些属性的转换关系,也发生在这个映射过程中。

第三段是和“假期管理”上下文的映射。我们目前的策略是,建立一个虚拟的一级工时项“假期”,用来把所有假期工时项归到一起。然后具体的假期种类,比如“年假”“病假”等等,作为“假期”工时项的二级工时项。当然,也可能有些企业不想这么做,而是想直接把“年假”“病假”作为一级工时项。这也没关系,因为目前的模型是支持这种灵活定义的。

我们对比一下 员工 实体的上下文映射和 工时项 的上下文映射。 员工 虽然在两个上下文之间有映射关系,但至少还是一一对应的。但是这个上下文中的 工时项 实体和“项目管理”上下文里的 项目 实体的关系就不是简单地一一对应了,而且属性的转换也不是那么直接。

所以,“工时项管理”上下文里的 工时项 和“项目管理”上下文里的 项目 是有转换关系的两个不同的概念。换句话说,这两个概念是不一致的。但是在两个上下文内部,各自的概念则是一致的。说到这里,你对 限界上下文内部概念保持一致,上下文之间的概念不必一致 这句话,是不是理解得更深刻了呢?

“工时项成员”的上下文映射

最后再看看工时项成员的映射关系。

工时项 的映射类似,对于普通工时项,如果也要分配人员的话,那么就直接在本上下文里定义。而对于项目,则从“项目管理”上下文的 项目成员 实体映射过来。

这里我们还要注意,在“项目管理”上下文里,保存了项目成员关系的历史,也就是说,哪怕一个员工已经退出项目了,仍然可以查到他曾经参与过这个项目。但是,这种历史信息对报工时是没有意义的,所以本上下文的 工时项成员 里只保留当前有效的关系就可以了。从这里,我们再一次看到了不一一对应的映射关系。

至于 工时记录 和请假记录之间的映射也是类似的,这里就不多说了。

限界上下文的概念映射图

好,我们已经把每个限界上下文内部的领域模型梳理过一遍了,如果还想在全局上描述一下各个上下文的映射关系,那么可以用下面的 上下文概念映射图

由于《领域驱动设计》原书没有给出正规的表示法,所以这张图也是我根据 UML 的原理自行设计的。

我们可以用 UML 的依赖,也就是虚线箭头表示映射关系。使用时要注意箭头的方向。UML 里的依赖是由依赖方指向被依赖方。所以,如果一个概念从 A 映射到 B,那么是 B 依赖 A,所以是 B 指向 A。然后,我们给依赖也加上 <> 衍型。由于上下文已经由箭头指明了,因此注释里不必指出上下文,只写映射逻辑就可以了。

限界上下文驱动的架构设计

好,到这里我们已经完成了限界上下文的建模,接下来,探讨实现问题。注意,截止到前面上下文映射的内容,仍然是领域专家能够理解的业务概念,所以仍然需要IT人员和领域专家达成一致。但是从现在开始,后面的内容就只是技术人员关注的内容,和领域专家无关了。

单体还是微服务

首先要考虑的问题是架构设计。在前两个迭代,我们都是在一个上下文里工作,用分层架构就可以了。现在有多个上下文,问题就复杂一些了。我们第一步要决定,对于多个上下文的情况,是采用单体还是微服务架构。

虽然现在微服务比较流行,不过单体应用并没有“原罪”。到底采用哪种架构还是要综合考虑。比如说,一般在满足后面这些条件时,才应该使用微服务,否则还是用单体架构好些。

第一,收益大于成本。使用微服务的好处包括容易横向扩容、独立部署、避免系统腐化等等。代价是提高了运维的成本、远程调用增加了性能损耗以及维护最终一致性的复杂性等等。只有在收益大于成本的情况下,才值得使用微服务。

第二,只有在团队有足够的运维技能和基础设施支持的时候,才能使用微服务。

第三,只有团队具有一定的开发微服务的技能和经验时,才能使用微服务。

假如决定使用单体,可以像后面这样画架构图。

UML组件图

这里我们用到了一种之前没用过的 UML 图,叫做组件图(component diagram)。

在 UML 里,一个组件符号是一个方框,方框的右上角有一个嵌入了两个垂直摆放的小矩形的小方块。

UML里对“组件”的定义是比较宽泛的。只要是对外暴露接口,对内封装实现的软件单元,都算组件。并没有规定两个组件到底是运行在不同进程,还是同一个进程,也没有规定是运行时动态链接,还是编译时静态链接。

虽然写得比较好的“类”也符合对外暴露接口,对内封装实现的要求,不过一般来说,组件的颗粒度比类要大。我们可以把组件分成进程内的和进程间的,下面举例说明一下常见的组件类型。

进程间组件类型可以分为单体应用或者微服务。

进程内组件类型的例子包括微软早期的 COM 组件、Java 的公共 Jar 包、Java 的 OSGi 模块、Maven 多模块项目中的模块、Java 9 开始提供的内置模块等等。注意,这里说的“模块”,和领域模型图里用来给领域对象分组的“模块”,不是一个层面的概念。前者是实现层面,后者是概念层面。

UML 里,一般也把系统和子系统当成组件,并且用带有 <> 或 <> 衍型的组件符号来表示。

不过,UML 里并没有规定到底什么是“系统”和“子系统”,而是把对它们的理解留给了 UML 的用户。

在前面的单体架构图里,“Unjuanable”(不要卷了)作为一个单体应用,用一个组件符号来表示。为了强调这是一个单体,我用了一个自定义的衍型 <>。

组件里面是表示限界上下文的 3 个包。我们假定在这个组件的代码里,没有用任何专门的模块机制,所以里面的限界上下文用普通的包图来表示。这也意味着,在代码的目录结构中,在根目录下面,首先为这 3 个上下文建立 3 个包,每个包内部,再按分层架构来进一步分包。

如果我们用了 Maven 的多模块或者 Java 9 的模块机制来封装各个上下文的代码,那么组件图里可以像这样表示。

这个图里 unjuanable 组件里嵌套的不再是包,而是 3 个子组件了。这些子组件的衍型是<> ,以便强调这是 3 个模块。

至于外面的 leavemng (假期管理)组件,表示一个外部的系统,我们并不关心它是单体还是微服务,所以也就不用泛型了。

总结

好,这节课的内容先讲到这,下面来总结一下。

今天我们讲了两个内容,一个是继续完成“工时项管理”上下文的建模,另一个是进行架构设计。

通过“工时项管理”的建模,我们深入理解了不同上下文之间的概念是怎样“不一致的”。另外,也进一步学习了这种不一致,是如何通过上下文映射来转换的。

我们还发现,原来工时项管理中的泛化不见了。事实上,原来的泛化逻辑隐含在了 工时项类别 实体以及“项目管理”和“工时管理”两个上下文的映射逻辑中。

通过聚焦“工时管理”上下文的设计,不受其他上下文概念的干扰,我们得到了一个更加简洁和灵活的设计。

基于划分出的限界上下文,就可以设计系统的架构了。到底使用微服务还是单体架构需要综合考虑。通常要根据投入产出、人员能力、基础设施等因素做出权衡。

另外,在 UML 方面,我为你提供了一种在全局上表示上下文间映射关系的方法,还为你演示了如何利用 UML 组件图表示架构设计。你可以课后尝试结合自己的项目画图练习一下。

下节课,我们会继续讨论微服务的划分。

思考题

最后我给你留两道思考题。

1.还记得吗?在之前的模型,如果要查询某个员工可以报工时的工时项,需要 3 条 SQL 语句,而根据现在的设计,只需要 1 条就可以了。你觉得大概应该怎么写这条 SQL 语句呢?

2.“工时管理”上下文中,工时项的“可否在本级报工时”以及“状态”两个字段,和“项目管理”上下文里的相应字段的映射关系并不那么直接,你觉得转换逻辑应该是怎样的呢?

好,今天的课程结束了,有什么问题欢迎在评论区留言,下节课,我们来探讨微服务的划分以及限界上下文集成的问题。