0%

IMPLEMENTING-DOMAIN-DRIVEN-DESIGN

《实现领域驱动设计》

疑问

战术建模和战略建模

战略建模-Strategic Modeling:

  • 限界上下文(Bounded Context)
  • 上下文映射图(Context Mapping)

战术建模-Tactical Modeling:

  • 聚合-Aggregate
  • 实体-Entity
  • 值对象-Value Objects
  • 资源库-Repository
  • 领域服务-Domain Services
  • 领域事件-Domain Events
  • 模块-Modules

领域驱动设计四论

DDD——战略建模(Strategic Modeling)和战术建模(Tactical Modeling)

所有的计算都表明它不工作,唯一的做法是:使其工作。

大泥球和屎山代码的区别

“屎山代码”和”大泥球”这两个词汇通常用于形容质量差的程序代码。

“屎山代码”通常指的是代码质量极低、难以理解、难以维护的代码,通常由于缺乏规范、文档、注释、代码复用等原因导致。这些代码通常很难被其他开发人员理解,也很难修改和扩展。

“大泥球”通常指的是代码中存在大量的冗余、重复、紊乱、无用的代码,这些代码让整个程序看起来很笨重、难以维护、难以扩展。与”屎山代码”不同的是,”大泥球”的代码质量可能并不一定很低,但是整个代码结构松散、缺乏优化,代码变化比较困难。

综上所述,”屎山代码”强调代码的质量低下、难以理解和维护,而”大泥球”强调代码结构的松散、缺乏优化,难以扩展和变化。

领域、子域、界限上下文

通用子域和支持子域的区别

在领域驱动设计(Domain-Driven Design,DDD)中,通用子域和支撑子域也是重要的概念,它们与控制论和系统理论中的术语略有不同。

通用子域指的是与业务领域相关的核心子域,它是一个相对独立的、有意义的领域,有明确的业务边界和业务价值,是业务上最关键的部分。例如,在一个电商平台的领域中,订单子域、支付子域、商品子域等都可以看作是通用子域。

而支撑子域则是指为支持通用子域而存在的、不可或缺的子域。支撑子域并不直接参与业务流程,但是它们提供了通用子域所需要的基础设施和技术支持。例如,对于一个电商平台的领域,用户认证子域、消息队列子域、搜索引擎子域等都可以看作是支撑子域。

一个通用子域可能会依赖于多个支撑子域,而一个支撑子域也可能会被多个通用子域共享。

举例来说,假设我们正在开发一个在线图书销售网站。订单子域、库存子域和会员子域都是通用子域,而邮件通知子域、支付子域、物流子域则是支撑子域。这些支撑子域提供了相应的基础设施和技术支持,以便支持我们的通用子域实现其业务目标。

通用子域一般是与业务弱关联的,支撑子域可以认为跟业务强关联

既然说协作和权限校验、安全不能耦合在一起,那么该如何分离呢?

ChatGPT3认为用另外的service去分离这份逻辑

prompt

在ddd中,在一个业务协作上下文中,权限校验不应该耦合进来,那么应该如何分离呢,如果校验又是必须的。比如如下的代码被认为是不合理的

代码详情
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Forum extends Entity {
public Discussion startDiscussion(String aUsername, String aSubject) {
if (this.isClosed()) {
throw new IllegalStateException("Forum is closed.");
}
User user = UserRepository.userFor(this.tenantId(), aUsername);
if (!user.hasPermissionTo(Permission.Forum.StartDiscussion)) {
throw new IllegalStateException("User may not start forum discussion.");
}
String author = user.username();
String authorName = user.person().name().asFormattedName();
String authorEmailAddress = user.person().emailAddress();
Discussion discussion = new Discussion(
this.tenantId(),
this.forumId(),
DomainRegistry.discussionRepository().nextIdentity(),
author,
authorName,
authorEmailAddress,
aSubject
);
return discussion;
}
}

answer

在DDD中,应该将权限校验逻辑分离到独立的服务中,以确保业务逻辑的独立性和可维护性。可以考虑使用领域服务来实现此目的。

在这个例子中,可以创建一个名为”ForumAuthorizationService”的领域服务,该服务专门用于验证论坛操作的权限。在论坛实体中,可以将”ForumAuthorizationService”注入为一个依赖项,然后在”startDiscussion”方法中使用它来执行权限校验。

下面是修改后的代码示例:

代码详情
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Forum extends Entity {
private ForumAuthorizationService authorizationService;

public Forum(ForumAuthorizationService authorizationService) {
this.authorizationService = authorizationService;
}

public Discussion startDiscussion(String username, String subject) {
if (this.isClosed()) {
throw new IllegalStateException("Forum is closed.");
}

User user = userRepository.userFor(this.tenantId(), username);

authorizationService.checkPermission(user, Permission.Forum.StartDiscussion);

String authorName = user.person().name().asFormattedName();
String authorEmailAddress = user.person().emailAddress();

Discussion discussion = new Discussion(
this.tenant(),
this.forumId(),
DomainRegistry.discussionRepository().nextIdentity(),
user,
authorName,
authorEmailAddress,
subject
);

return discussion;
}
}

在这个修改后的代码中,”ForumAuthorizationService”被注入到”Forum”实体中。在”startDiscussion”方法中,我们使用”authorizationService”来检查用户是否具有启动讨论的权限。通过这种方式,权限校验逻辑被成功分离,从而使领域模型更加专注于业务逻辑。

DDD入门

痛点:不同角色的困惑

  • 新手,初级开发者:找还年轻,有很多点子,对写代码充满热情。但是我所在的那个项目简直让人崩溃,我才不想一毕业就被那些重复性的工作纠来缠去。这个项目的架构为什么如此复杂?这到底是怎么呢?我修改了一点代码,却破坏了更多的代码。有人知道这本来应该是什么样子吗?现在,我还得添加一些复杂的新特性。我在遗留代码之上添加了一个适配器来屏蔽那此难看的遗留代码,但是没有用。我相信除了整天写代码和调试外,还有更好的办法。于是有人问我介绍DDD,听说DDD是领域模型中的“四人帮(GangofFour),不错不错。”
  • 中级开发者:“在过去的几个月中,我加人了一个新的项目,这次轮到我来做出改变了。那时,当我和高级开发人员在一起工作时,我发现我缺少对事物的洞察力。有时团队非常涣散,但是我又不知道其中的原因,我决定改变团队成员们的做事方式。我需要一种能够助我成功的软件开发技术。一个高级架构师向我推荐DDD,我打算了解了解。
  • 高级开发者,架构师:“我曾在多个项目中都使用过DDD,不过目前所在项目还未使用。我喜欢DDD战术模式的威力,但是我还打算应用更多的DDD模式,比如战略设计等。在阅读[ Evans ]时,我发现通用语言的功能非常强大。我已经与团队成员和管理层讨论过采用DDD的事情了,但其中有些中高级开发者对DDD在项目中的前景并不看好,而管理层也不是那么热裹于DDD。我是最近才加入公司的,虽然我是团队带头人,但是整个团队似乎并不愿意被一些新奇玩意儿打断了开发进度。不管如何,我是不会放弃的。虽然其他开发者对DDD没有信心,但是我是能做到的。我决定将领域专家引人到团队中来,使他们跟技术人员一起工作。”
  • 领域专家:“我已经在部门帮他们解决业务问题好长一一段时间了,我希望开发者们能够更好地理解他们所做的事情。他们总是认为我们业务人员是愚蠢的。但是他们不知道的是,如果不是我们,他们早就丢了工作。开发者总会以一些奇怪的方式来讨论我们的软件,如果我说这是A,他们却说这是B,好像我们需要有本词典才能交流一样。如果我们试图正他们的错误叫法,他们就不愿意合作了。我们在这上面浪费了太多的时间。软件为什么不能像真正的业务专家所想象的那样工作昵?”
  • 项目经理:“我们要交付软件,但结果并不总是让人舒心,有时开发时间拖得太长了。开发者在谈论到领域时总是各执一词,莫衷一是。我不确定我们是否需要另一种银弹般的技术或者方法。之前我们不是没有尝试过,但是每次都失败了,结果还是得回到原来的位置。我总是说,我们应该放弃想,准备战斗,但是团队成员并不这么认为。他们工作非常努力,我总觉得欠他们些什么,于是听听他们的意见吧。他们都是聪明之人,并且都希望做出些改变。对于我来说,如果我上面的管理层同意,我是完全允许开发者们花时间学习的。团队成员们希望有个集中化的业务知识体系,我想我可以以此说服我的老板。这样一一来,我自己的工作也将变得简单,我还可以促进团队和业务专家之间的合作与互信。”

[ Evans ]表示他那本经典的《领域驱动设计》
[ Gamma ] 表示四人帮的《设计模式》
[ Flowler,P of EAA ] 表示Martin Flowler的《企业应用架构模式》

痛点:难以提摸的业务价值

开发能够传递真正业务价值的软件和开发普通的软件是不同的。具有真正业务价值的软件能够很好地符合业务战略,并且可以将竞争优势融合到解决方案中。此时的软件并不是关于技术的、而是关于业务的。

业务知识从来就没有被集中对。开发团队须在多方之权各种需求,并确定其中的优先级。同时,团队成员的技能也是良募不齐的。在获得所有的信息之后,团队所面临的问题在手:如何确定某种需求确实能够传递 $\color{green}{\text{真正的业务价值}}$ ?还有,我们如何去发现并暴露出这此业务价值,如何安排它们之间的 $\color{green}{\text{优先级}}$ ,并且 $\color{green}{\text{如何实现}}$ 它们?

在开发过程中,最大的鸿沟之一便存在于领域专家和开发者之间。通常来说领域专家将关注点放在交付业务价值上,而开发者则将注意力放在技术实现上。当然,并不是说开发者的动机是错误的、而是说开发者的眼光被自然而然地吸到了实现层面上。即便让领域专家和开发者一同工作,他们之间的协作也只是表面的,这时在所开发的软件中便产生了一种映射:将业务人员所想的映射到开发者所理解的。这样一来,软件便不能完全反映出领域专家的思维模型。随着时间的推移, $\color{green}{\text{这种鸿沟将增加软件的开发成本}}$ 。而随着开发者 $\color{green}{\text{转到其他项目或者离职}}$ , $\color{green}{\text{本应驻留在软件中的领域知识}}$ 就丢失了。

另一个问题发生在当多个领域专家之间存在分歧的时候。这是很有可能发生的,因为每个专家只是熟悉某个或者某些特定的领域。另外,在某个领域单找不到真正的专家是可能的,此时有人可能对该领域有所了解,但是他更像一个业务分析员。 $\color{green}{\text{这些问题将导致相互矛盾的软件模型}}$ 。

更糟的是,软件的技术实现可能错误地改变软件的业务规则。比如,ERP软件通常需要 $\color{green}{\text{修改业务操作以满足某个特定用户的需求}}$ ,因此ERP的成本不能单以使用许可和维护费用来计算,对业务规则的修改所产生的成本远远大于前两者。另外一个相似的例子是当开发团队将业务需求翻译成软件功能的时候。这对于业务、用户和合作方来说都是一笔很大的成本。还有, $\color{red}{\text{技术上的翻译和解释是没有必要的}}$ ,并且在使用适当开发方式的情况下是可以避免的。解决方案才是主要的投入。

DDD如何帮助我们

DDD作为一种软件开发方法,它主要关注以下三个方面:

  • DDD将领城专家和开发人员聚集到一起,这样所开发的软件能够反出领域专家的思维模型。领域专家将和开发人员一起创建一套适用于领域建模的 $\color{green}{\text{通用语言}}$ 。通用语言以须在全队范围之内达成一致,所有成员都使用通用语言进行交流,通用语言也是对软件模型的直接反映。请注意,虽然团队中同时包含领城专家和开发人员,但并不是“我们”和“他们”的关系,团队中只有“我们”的概念。通用语言也有助于促使原本存在分歧的领域专家们达成一致意见。
  • DDD关注业务战略。虽然说战略(Strategic)设计自然地包含了战术设计,但是战略设计关注更多的则是业务的战略方向。它帮助我们定义不同团队之间的组织关系,并在这些关系有可能导致项目失败的时候提供早期预警。DDD的战略设计用于清楚地界分不同的系统和业务关注点,这程可以保护每个业务层面的服务。更进一步,这将指我们如何实现面向服务架构(service-oriented architecture)或者业务驱动(business-driven architecture)架构。
  • 通过使用战术设计建模工具:DDD满足了软件真正的技术需求。这战术设计工具使开发人员能够按照领域专家的思维模型开发软件。同时,所开发出来的软件是可测试的,能够尽量避免错误,能热行服务层面协议(Service-Level Agrcement,SLA),具有很好的伸缩性,并且允许分布式计算。DDD的最佳实践后时包含厂高层的架构性实战和底层设计实战,关汗业务规则利和数据不变性,并且可以对业务规则起到保护作用。通过这种方式开发软件,你和你的团队将能成功地交付真正的业务价值。

【从初学到放弃】Ruby On Rails

领域对象健康检查

  • 你的领域对象中是不是主要是些公有的getter和setter方法,并且几乎没有业务逻辑,或者甚至完全没有业务逻辑一一对象嘛,主要就是用来容纳属性值的?
  • 软件组件经常使用的领城对象是否包含系统主要的业务逻辑,并多数情况下你需要调用那些getter和setter?你可能会将这样的客户代码称为服务层(ServiceLayer)或者应用层(ApplicationLayer)(4,14)代码。也或者,如果这描述的是你的用户界面,请回答“Yes”,然后好好反省一下,告谦自己一定不要再这么做了。
  • 如果你对以上两个问题的回答都是“No”,表明你的领域对象是健康的。
  • 如果都是“Yes”,表明你的领域对象已经病得不轻了,这便是贫血对象。好消息是,你是可以获得帮助的,继续往下读吧

正如[ Fowler,Anemicl ] 所说,贫血领域对象是不好的,因为你花了很大的成本来开发领域对象,但是从中却获益基少。比如,由于存在对象-关系阻抗失配(Object-Relational Impedance),开发者需要将很多时间花在对象和数据存储之间的映射上。这样的代价太大,而收益太小。我得说,你所说的领域对象根本就不是领域对象,而只是将关系型数据库中的模型映射到了对象上而已。这样的领域对象更像是活动记录(Active Record)Fowler.PofEAAl,此时你可以对架构做个简化,然后使用事务脚本(TransactionScript)[ Fowler,PofEAA ]进行开发。

代码中很多的.set的代码。我得说,以上方法还算不上槽糕到了极点。很多时候数据-映射(data mapping)代码将变得非常复杂,此时大量的业务逻辑便不能反映在代码里了。

1.saveCustomer()业务意图不明确
2.方法的实现本身增加了潜在的复杂性。
3.Customer领域对象根本就不是对象,而只一个数据持有器(dataholder)。

让我们暂时撇开关于实现细节的讨论,现在来看看DDD最具威力的特性之一:通用语言。通用语言限界上下文(BoundedContext,2)同时构成了DDD的两大支柱,并且它们是相辅相成的。

  • 通用语言是团队自己创建的公用语言,团队中同时包含领域专家和软件开发人员。

团队成员们妥协的绝对不应是通用语言的质量,而是概念,术语和含义。然而,最初的一致开不表示始终一致,就像其他语言一一样,通用语言带会随看时间推移而不断演化改变。

如何掌握通用语言

  • 同时绘制物理模型图和概念模型图,并标以名字和行为。虽然这些图并不是正式的设计图,但它们却包含软件建模的某些方面。即使你的团队在使用统一建模语言(Unified Modeling Language,UML)来完成正式建模也不要得意忘形,因为这样可能反而不利于团队的讨论,最终将阻碍通用语言的产生。
  • 创建一个包含简单定义的术语表。将你能想到的术语都罗列出来,包括好的和不好的,并注明好与不好的原因。在你给术语下定义时,你在不经意间就会创造出一些可重用的词汇,因为此时你使用的是领域中的通用语言。
  • 如果你不喜欢术语表,可以采用其他类型的文档,但记得将那“不止式的模型图也包含进去。同样,这里最终的自的也是发现通用语言中的术语和词组。
  • 由于团队中有些人工作在术语表上,还有些人工作在文档上,此时你需要我到团队的其他人员来检查你的成果。分歧背定是有的,你应该对此有所准备。

由于团队交流和代码才是对通用语言的持续表达,你应该试着抛弃那些模型图、术语表和文档。虽然这并不是DDD所要求的,但是这样做的确很实用,因为我们很难将项目文档和软件系统保持同步。

使用DDD的业务价值

软件开发者不应该只是热裹于技术,而是应该将眼界放得更宽。不管使用什么技术,我们的目的都是提供业务价值。而如果我们采用的技术确实产生了业务价值,人们就没有理由拒绝我们在技术上的建议。

如果我们提供的技术方案比其他方案更能够产生业务价值,那么我们的业务能力也将增强。

  • 你获得了一个非常有用的领域模型
  • 你的业务得到了更准确的定义和理触
  • 领域专家可以为软件设计做出责献
  • 更好的用户体验
  • 清晰的模型边界
  • 更好的企业架构
  • 敏捷、送代式和持续建模
  • 使用战略和战术新工具

你获得了一个非常有用的领域模型

DDD强调将精力花在对业务最有价值的东西上。我们并不过度建模,而是关注业务的核心域。有些模型是用来支撑核心域的,它们同样是重要的。但是,这些起支撑作用的模型在优先级上没有核心域高。

当我们将关注点放在自已的业务和别人业务的区别上时,我们便能更好地理解自已的任务所在,同时我们将更具竞争优势

你的业务得到了更准确的定义和理解

业务人士能够更好地理解业务本身。我基至听说通用语言曾经出现在某些公司的市场营销材料中.

随着业务模型的不断改善,人们对业务的理解也将更加深刻。在团队讨论的过程中,一此业务细节被不断地暴露出来,这些细节有助于掌握业务价值

领域专家可以为软件设计做出贡献

当人们对自已的核心业务有了更深的厂解时,业务价值自然就出来了。领域专家并不总是同意某些概念和术语。有时,分歧源自于领域专家们在其他公司工作时所积累起来的经验,而有时分歧则源自于公司内部。不管如何,当领域专家们在一起工作时,他们最终将达成一致意见,这对于整个公司来说都是件好事。

开发者和领域专家共享同一套交流语言,领域专家将知识传递给开发者。开发者总是会离开的,有可能去接触一个新的核心域,也有可能跳槽到其他公司,这时培训和工作移交也将变得更加简单,而“只有少数人才了解模型”的情况将大大减少。领域专家,剩下的开发者和新进人员可以继续使用通用语言进行交流

更好的用户体验

用户体验可以更好地反映出领域模型的好坏。

如果软件留下太多的地方让用户自己去理解,用户往往需要经过培训才能做出操作决定。实际上,用户只是将他们所理解的转移到表单(form)中的数据而已。数据将被存储起来,如果用户不知道数据的用途,那么结果也将是错误的。

当用户体验是按照领域专家心中的模型来设计时,就不会出现以上的间题了。这时软件本身便能对用户起到培训作用,而不需要业务人员来提供培训。效率提高了培训减少了一一这就是业务价值

清晰的模型边界

我们并不鼓励技术团队将精力单纯地放在编码和算法上,而是期望他们能够面向业务。明确的目标产生高效的解决方案,而要达到这样的目的往往需要更好地理解项目的限界上下文

更好的企业架构

一旦限界上下文得到了较好的理解和仔细的划分,那么团队的所有成员应该知道限界上下文间的集成是必要的。上下文之间的边界和关系是明晰的。当不同上下文的模型间存在依赖关系时,我们将使用上下文映射图来集成不同的限界上下文,而这又有助于我们全面地了解整个企业的架构。

敏捷、选代式和持续建模

“设计”这个词可能并不能取悦业务管理层。然而,DDD并不是一个重量级的设计方法和开发过程。DDD并不是画模型图,而是将领域专家的思维模型转化成有用的业务模型。DDD不是创建一个真实世界的模型,而是模仿现实。

团队的工作遵循敏捷方法一一迭代式的,增量式的。任何一种敏捷方法,只要团队认为合适,都可以用于DDD项目。通过DDD创建出来的模型便是可工作的软件。团队会对模型做持续的改进,直到业务层没有新的需求为止。

使用战略和战术新工具

限界上下文为团队创建了一个建模边界,成员在边界内部为特定的业务领域创建解决方案。在单个限界上下文中团队成员共享一套通用语言。不同的团队有时各自负责一个限界上下文,此时可以使用上下文映射图在战略层面上对限界上下文进行界分和集成。在某个建模边界内部,团队将使用战术建模工具:聚合(Aggregate,10)、实体(Entity,5)、值对象(ValueObject,6)、领域服务(DomainService,7)和领域事件(DomainEvent,8)等。

实施DDD所面临的挑战

  • 为创建通用语言腾出时间和精力
  • 持续地将领域专家引入项目
  • 改变开发者对领域的思考方式

改变开发者对领域的思考方式

在DDD中,我们会谈及到对概念的命名。对于概念命名而言,我们有更高层面的要求。当我们对一个领域进行建模时,我们需要仔细地考虑什么样的对象做什么样的事情,这是关于对象行为设计的。我们希望对对象行为的命名能够传达准确的业务含义,也即反映通用语言。要达到这样的目的,肯定不是先在类上定义属性,然后向客户端代码暴露getter和setter那么简单。

这种方式同时也暴露了Backlogltem的数据结构,并且将关注点集中在数据属性上,而不是对象行为。你可能会反驳道:“setSprintld()和setStatus()就是行为啊。”问题在于,这里的“行为”没有真正的业务价值,它并没有表明领域模型中的概念一一此处即“将待定项提交到冲刺中”。开发者在开发客户代码时,他并不清楚到底需要为Backlogitem的哪些属性设值,而这样的属性有可能存在很多,因为这是一个以数据为中心的模型。

对于你目前正在工作的业务领域,思考一下模型中的通用术语和业务操作。

将术语写在白板上。

然后,将项目中所用到的短语也写下来。

与真正的领域专家交流一下,看看哪些词汇是可以改善的(记得带上咖啡哦)。

为领域建模正名

如果说开发一个业务子域(Subdomain,2)就像攀岩一样难,那么我需要随身携带DDD的战术模式来武装自己。

  • 如果一个限界上下文被当成核心域来开发,我们还是建议在该限界上下文中使用战术模式。
  • 一个领域,对于消费方来说有可能成为通用子域(Generic Subdomain.2)或者支撑子域,但是却有可能成为你自己的核心域,但是却有可能成为你自己的核心域。
  • 如果你正开发一个支撑子城,但是由于种种原因,该支撑子域不能从第三方的通用子域直接获得,那么此时战术模式将帮上你大忙。

你最终得取悦你的客户,而不是技术开发者,所以你得慎重地做出选择。

在你开发一个新的领域对象时,比如实体或值对象,你可以采用以下步骤进行:

  • 编写测试代码以模拟客户代码是如何使用该领域对象的。
  • 创建该领域对象以使测试代码能够编译通过,同时对测试和领域对象进行重构,直到测试代码能够正确地模拟客户代码同时领域对象拥有能够表明业务行为的方法签名。
  • 实现领域对象的行为,直到测试通过为正,再对实现代码进行重构。
  • 向你的团队成员展示代码,包括领域专家,以保证领域对象能够正确地反映通用语言。

在这之后,你会添加更多的测试,从多个角度确保新建领域对象的正确性。此时你关注的是领城对象对于领域概念的表达力,而测试代码本身便是通用语言在程序中的表达。

虚构的案例,真实的实践

SaaSOvation决定采用DDD一Lite,然而不久之后他们就遇到问题了,因为他们开不了解子域和限界上下文。

领域、子域和限界上下文

从广义上讲,领域(Domain)即是一个组织所做的事情以及其中所包含的一切。

在正确实施DDD的情况下,这种简单的子域可以以模块(Module,9)的形式从核心域中分离出来,而不需要包含在笨重的子系统组件中。

现在,看看图2.2上半部分的领域边界,你会看到一个叫核心域的子域。对于核心域,我们在前面的章节中已经讲到,它是整个业务领域的一部分,也是业务成功的主要促成因素。从战略层面上讲,企业该在核心域上胜人一筹。我们应该给予核心城最高的优先级,最资深的领域专家和最优秀的开发团队。在实施DDD的过程中,你将主要关注于核心域。

图2.2中还展示了另外两种子域:支撑子域通用子域。有时,我们会创建或者购买某个限界上下文来支撑我们的业务。如果这样的限界上下文对应着业务的某些重要方面,但却不是核心,那么它使是一个支撑子域。创建支撑子域的原因在于它们专注于业务的某个方面,否则,如果一个子域被用于整个业务系统,那么这个子域便是通用子域。我们并不能说支撑子域和通用子域是不重要的,它们是重要的,只是我们对它们的要求并不像核心域那么高。

什么是支撑子域:支撑子域通常包括诸如博客、论坛、API、管理控制台、付款网关等功能。它们在技术上可能独立于主要网站或应用程序,具有自己的服务器、数据库、代码库等。

界限上下文和子域的区别:可以将子域看作是界限上下文的一个部分,一个界限上下文可以包含多个子域,而一个子域通常只包含一个界限上下文。界限上下文是一个更大的概念,用于划分应用程序中的不同业务领域,而子域则是在界限上下文内部对业务领域的更细粒度划分。

chatGPT:通用子域和支撑子域的区别是什么
1
2
3
4
5
6
7
8
9
在领域驱动设计(Domain-Driven Design,DDD)中,通用子域和支撑子域也是重要的概念,它们与控制论和系统理论中的术语略有不同。

通用子域指的是与业务领域相关的核心子域,它是一个相对独立的、有意义的领域,有明确的业务边界和业务价值,是业务上最关键的部分。例如,在一个电商平台的领域中,订单子域、支付子域、商品子域等都可以看作是通用子域。

而支撑子域则是指为支持通用子域而存在的、不可或缺的子域。支撑子域并不直接参与业务流程,但是它们提供了通用子域所需要的基础设施和技术支持。例如,对于一个电商平台的领域,用户认证子域、消息队列子域、搜索引擎子域等都可以看作是支撑子域。

一个通用子域可能会依赖于多个支撑子域,而一个支撑子域也可能会被多个通用子域共享。

举例来说,假设我们正在开发一个在线图书销售网站。订单子域、库存子域和会员子域都是通用子域,而邮件通知子域、支付子域、物流子域则是支撑子域。这些支撑子域提供了相应的基础设施和技术支持,以便支持我们的通用子域实现其业务目标。

领域中还同时存在问题空间(problemspace)解决方案空间(solutionspace)

  • 问题空间是领域的一部分,对问题空间的开发将产生一个新的核心域。对问题空间的评估应该同时考虑已有子域和额外所需子域。
  • 解决方案空间包括一个或多个限界上下文,即一组特定的软件模型。

为了平衡各个知识点,接下来我们将讲到在解决方案空间中,限界上下文作为一种建模具的重要性。在上下文映射图(3)中,我们将主要讲解如何通过集成限界上下文的方式来完成不同通用语言间的映射。

上下文才是王道.

在金融领域,我们经常谈到证券(security)。证券交易管理委员会(Securitiesand
ExchangeCommission,SEC)限制证券只能和股票(equities)一起使用。现在让我们考虑这种情况:期权合同(futurescontract)作为一种商品,它并不被SEC所管理。然而,有些金融公司却将期权当作一种证券,并且用标准类型(StandardType,6)Futures来表示。
这是表示期权的最好式吗?这取决于它所处的领域,有人认为期权显然是一种证养另有人则持反面意见。上下文同时也是具有文化属性的。对于一个经营期权的公司,在通用语言中以证券来表示期权在文化上是合理的

为了解决这个间题,我们应该为每个阶段创建各自的限界上下文。在每个限界上下文中,都存在某种类型的图书(Book)。在几乎所有的上下文中,不同类型的图书对象将共享一个身份标识(identity)。

通常情况下,你是可以识别出那些概念分离正确的情况的,因为有些相似的对象拥有不同的属性和行为,此时我们可以认为上下文边界的划分是合理的。然而,如果你在不同的限界上下文中看到了完全相同的对象,这通常意味着你的模型是错误的,除非这些限界上下文使用了共享内核(SharedKernel,3)

通常情况小,一个系统/应用程序的使用者并不只是人,还可能是另外的计算机系统。系统中有可能存在诸如Web服务(Webservices)之类的组件。我们也可以使用REST资源来与模型交互,此时的REST资源即被称为开放主机服务(OpenHostService,3,13)。或者,我们可以使用SOAP或消息服务端点。在以上所有情况中,那些面向服务的组件都应该位于上下文边界之内。

用户界面和面回服务端点都会将操作委派给应用服务(14)。应用服务包含了不同类型的服务,比如安全和事务管理等。对于模型来说,应用服务扮演的是一种门面模式Facade[Gamma etal.]。同时,应用服务还具有任务管理功能,它将来自用例流(Use Case Flow)的请求转换成领域逻辑的执行流。应用服务也是位于上下文边界之内的。

限界上下文中可以包含多少领域模型中的基础部件呢,比如模块(9),聚合(10),领域事件(8)和领域服务(7)等?这好像是在间“一个符串应该有多长?”一样。限界上下文应该足够大,以能够表达它所对应的整套通用语言。

核心领域之外的概念不应该包含在限界上下文中。如果一个概念不属于你的通用语言,那么一开始你就不应该将其引人到模型中。此外,如果有外部概念“偷偷潜入”了你的限界上下文,你需要将其清除,它们可能属于另外的支撑或者通用子域,或者根本就不属于某个模型。

在每个迭代中,我们都应该对先前的假设提出挑战,这使得我回模型中态加或珊除一些概念,或者改变概念的行为和协作方式等。但是主要的间题是:我们意是面临这样的挑战。在使用DDD原时,我们会认真地思考应该添加哪些概念,又应该删除哪些概念。使用限界上下文和上下文映射图这样的工具可以助我们分析出哪些概念的确应该属于核心域。我们并不随意地采用非DDD的分离原则。

如果我们的模型是音乐,那么它所表现的则是完整性、纯洁性、力量、优雅和美。

那么,哪些因素会导致我们创建大小不正确的限界上下文呢?

  • 我们可能错误地采用架构来指导设计开发,而不是通用语言。一些平台、框架或者基础设施通常是用来打包和部署组件的,它们可能影响我们对限界上下文的设计,此时我们会从技术层面而不是语义边界来考虑问题。
  • 另一个可能的陷阱是:根据开发任务的分配来拆分限界上下文。技术带头人和项目经理可能会想,小规模任务对于开发者来说将更加容易完成。这可能是有道理的,但是,为了分配任务而拆分限界上下文是一种错误的上下文建模方式。事实上我们没有必要为了管理技术资源而创建一些假(fake)的上下文边界。

项目的源代码可以只包含领域模型,也可以包含一些周边的(4)或六边形(4)区域等。

两个团队可能会合作设计一个共享内核,而共享内核并不是一个典型的限界上下文这种上下文陕射模式使两个团队产生紧穿的联系,进而需要两个团队对模型的改变进行不断交流。这种建模方法并不常见,并且我们应该尽量避免这种情况。

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

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

协作上下文

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

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

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

  • 他们可以将模型重构成职责层(Responsibility Layers)[ Evans ],即将安全和权限功能下放到比当前模型更低的一个逻辑层。这并不是最好的解决办法,职责层主要用于大型模型。虽然这些层得到了正确的分离,他们依然应该保留在模型中,因为他们都是核心域的一部分。另一方面,团队所处理的概念并没有得到适当地定义,况且这些概念原本就不应该属于核心域。
  • 另一种方法是采用隔离内核(Segregated Core)[ Evans ]。要达到这样的目的,我们可以在整个协作上下文中搜索对安全和权限相关逻辑的引用,然后将身份和访问组件分离到另一个单独的包中。当然,这并不能生成一个单独的限界上下文,但是却可以使团队朝着这样的目标更进一步。这种做法正是团队所需要的,因为这种模式就是这样定义的:“使用隔离内核的时机是当你有一个非常重要的限界上下文,但是其中模型的关键部分却被大量的起辅助作用的功能所掩盖了。“这里的安全和权限管理功能显然只是起辅助作用的。团队成员最终意识到,他们需要一个单独的身份与访问上下文作为协作上下文的通用子域。

身份与访问上下文

通过基于角色的权限机制来管理对系统资源的获取,这种方式是简单的、优雅的,同时又是功能强大的。

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

敏捷项目管理上下文

上下文映射图

就是把downstream,upstream画出来

上下文映射图并不是一种企业架构、也不是系统拓扑图。但是,它可以用于高层次的架构分析,指出诸如集成瓶颈之类的架构不足。上下文映射图展现了一种组织动态能力(organizationaldynamic),它可以帮助我们识别出有碍项目进展的一些管理问题。

有这么一种说法,wiki是葬送信息的地方。不管你将这些框图放在什么地方,上下文映射图都只是默默地呆在那里,除非团队成员经常去关注这些框图并围绕着展开讨论。

产品和组织关系

  1. CollabOvation一一一款社交协作产品。该产品允许注册用户发布对业务有CollabOvation价值的内容,发布方式是一些流行的基于Web的工具,比如论坛、共享日历、博客和wiki等。这是SaaSOvation公司的旗舰产品,也是该公司的第一个核心域(2)(即使当时他们还并不知道“核心域”这个DDD术语)。开发团队后来从CollabOvation中提取出了ldOvation模型(第2点)。对于CollabOvation来说,IdOvation是一个通用子域(2),而CollabOvation本身文作为ProjectOvation(第3点)的支撑子域(2)。
  2. Idovation一一一款可重用的身份和访问管理产品。IdOvation为注册用户提供安全的、基于角色的访问管理。这些功能一开始和CollabOvation(第1点)混合在一起,这样导致的问题是:实现受到了限制,功能不可重用。SaaSOvation对CollabOvation进行了重构,引人了一个新的、清晰的限界上下文。SaasOvation公司决定支持多个租户,这种功能对于SaaS产品来说是至关重要的。对于消费方来说,IdOvation扮演着通用子域的角色。
  3. ProjectOvation一一一个款敏捷项目管理产品。这是SaaSOvation公司的新核心域。ProjectOvation采用基于Scrum的项目运行框架,用户可以创建项目管理资产,同时对项目资产进行分析和设计,还能跟踪项目进度。和CollabOvation一样,ProjectOvation将IdOvation作为一个通用子域来使用。该产品的一大创新是将CollabOvation(第1点)引入了敏捷项目管理,这样用户可以围绕Scrum的产品,发布、冲刺和待定项展开讨论。
  • 合作关系(Partnership):如果两个限界上下文的团队要么一起成功,要么一起失败,此时他们需要建立起一种合作关系。他们需要一起协调开发计划和集成管理。两个团队应该在接口的演化上进行合作以同时满足两个系统的需求。应该为相互关联的软件功能制定好计划表,这样可以确保这些功能在同一个发布中完成。
  • 共享内核(Shared Kernel):对模型和代码的共享将产生一种紧密的依赖性对于设计来说,这种依赖性可好可环。我们需要为共享的部分模型指定一个显式的边界,并保持共享内核的小型化。共享内核具有特殊的状态,在没有与另一个团队协商的情况下,这种状态是不能改变的。我们应该引入一种持续集成过程来保证共享内核与通用语言(1)的一致性。
  • 客户方-供应方开发(Customer-Supplier Development):当两个团队处于一种上游-下游关系时,上游团队可能独立于下游团队完成开发,此时下游团队的开发可能会受到很大的影响。因此,在上游团队的计划中,我们应该顾及到下游团队的需求。
  • 遵奉者(Conformist):在存在上游-下游关系的两个团队中,如果上游团队已经没有动力提供下游团队之所需,下游团队便抓军无助了。出手利他主义,上游团队可能向下游团队做出种种承诺,但是有很大的可能是:这此承诺是无法实现的。下游团队只能盲地便用上游团队的模型。
  • 防腐层(Anticorruption Layer):在集成两个设计良好的限界上下文时,翻译层可能很简单,甚至可以很优雅地实现。但是,当共享内核、合作关系或客户方-供应方关系无法顺利实现时,此时的翻译将变得复杂。对于下游客户来说,你需要根据自己的领域模型创建一个单独的层,该层作为上游系统的委派向你的系统提供功能。防腐层通过已有的接口与其他系统交互,而其他系统只需要做很小的修改,基全无须修改。在防腐层内部,它在你自已的模型和他方模型之间进行翻译转换。
  • 开放主机服务(Open Host Service):定义一种协议,让你的子系统通过该协议来访问你的服务。你需要将该协议公开,这样任何想与你集成的人都可以使用该协议。在有新的集成需求时,你应该对协议进行改进或者扩展。对于一些特殊的需求,你可以采用一次性的翻译予以处理,这样可以保持协议的简单性和连贯性。
  • 发布语言(Published Language):在两个限界上下文之间翻译模型需要一种公用的语言。此时你应该使用一种发布出来的共享语言来完成集成交流。发布语言通常与开放主机服务一起使用。
  • 另谋他路(Separate Way):在确定需求时,我们应该做到坚决彻底。如果两套功能没有显著的关系,那么它们是可以被完全解耦的。集成总是昂贵的有时带给你的好处也不大。声明两个限界上下文之间不存在任何关系,这样使得开发者去另外寻找简单的、专门的方法来解决问题。
  • 大泥球(Big Ball of Mud):当我们检查已有系统时,经常会发现系统中存在混杂在一起的模型,它们之间的边界是非常模糊的。此时你应该为整个系统绘制一个边界,然后将其归纳在大泥球范围之列。在这个边界之内,不要试图使用复杂的建模手段来化解问题。同时,这样的系统有可能会向其他系统蔓延,你应该对此保持警觉。

pp108