领域驱动设计

2025-05-25

领域驱动设计

你的公司建立在一个单体架构之上。这个单体架构或许是你最宝贵的资产,因为你的业务知识都分布在内部。然而,它也背负着多年的技术债务,团队之间缺乏沟通,代码开发也因此变得肮脏不堪。

你的单体应用运行缓慢、不透明、容易出错,而且未经测试。你的开发人员和系统运维团队害怕发布新代码,最终构建和定义了繁重的流程,并延长了发布周期,同时还要进行冗长的手动测试。这是因为我们需要安全地发布新版本,不能中断生产环境,因为恢复或回滚都很困难。

然而,单体架构仍然存在,它创造了大部分收入,但也扼杀了团队的绩效。如何才能提升主要收入来源,并优化团队,实现业务的长期可预测性和演进?这时,领域驱动设计 (DDD) 就派上用场了。

但在深入探讨领域驱动设计 (DDD) 之前(抱歉😊),我们需要理解为什么单体应用仍然能够运行并承载巨量流量。单体应用本身并非错误的软件蓝图,问题在于“大泥球”。那么,让我们开始讨论单体应用吧。

单体应用极其便宜且功能多样。单体应用之所以能长期屹立不倒,是因为在单体应用中做出的决策在中期是可逆的。由于数据和代码都集中在一起,重构起来更简单(可以用你最喜欢的IDE完成),而且数据传输成本低廉。例如,让我们从以下用例开始:

我们是一个类似亚马逊的在线购物平台,主要销售书籍。在产品的首次迭代中,由于我们收到的采购订单数量不多,因此我们不会验证仓库中的书籍库存,因此我们可以手动修复损坏的订单。最终我们得到了以下架构图。

你的单体应用

几个月后,我们的业务开始增长,每分钟都有几笔订单,尤其是在黑色星期五和圣诞节期间,订单量达到峰值。由于图书缺货,我们无法处理日益增多的缺货订单。我们决定实现一个 StockService 服务,用于在结账时验证我们想要购买的图书是否还有库存。

整体式架构的第二次迭代

如你所见,添加新服务和业务规则的成本非常低:只需添加几个新类以及对其他服务的依赖即可。我们没有做出艰难的决定,只是遵循了单​​体应用中已有的模式。我们之所以能这样做,是因为:

  • 在整体系统中移动数据很便宜
  • 单体应用中的决策仅限于单一流程
  • 整体式架构具有明确且通用的模式
  • 可以在 IDE 的帮助下重构整体

因此,我们所做的是推进项目,而不是做出复杂的设计决策,然后交付新功能,从而增加技术债务。这使得小型团队能够快速迭代产品,但当团队数量增加时,就会出现问题。原因是不同的团队需要来自不同服务的数据和逻辑来满足用户需求。

如您所见,团队 A 和团队 C 在 UserService 上存在重叠,因为他们都需要用户数据来保证功能正常。处理这种情况有三种常见方法,下表将其分为三类:所有权、协作和效果。

所有权 合作 影响
其中一个团队负责 UserService 当其他团队需要功能时,询问所有者团队 由于团队有共同的积压工作,因此团队效率会降低
其中一个团队负责 UserService 当其他团队需要功能时,发起 PR 减慢编写 PR 的团队的速度,因为依赖于其他团队来审查功能
共享所有权 需要常规沟通和协作来实现新功能 由于团队有共同的积压工作,导致团队效率降低

由于这个问题没有简单的解决方案,因此解决方案是拆分单体应用。要理解不同团队处理同一段代码的复杂性,可以参考两个线程处理同一组内存中数百个变量的复杂性。

因此,经过数月甚至数年的努力,我们将单体应用拆分成多个服务。我见过的最常见的拆分单体应用的策略是定义数据边界。例如,所有与用户相关的数据最终都会放在 UserService 中,股票信息放在 StockService 中,等等。

这种方法的问题在于:

  • 它可能看起来像领域驱动设计,但事实并非如此,因为它基于数据,而不是基于业务知识。
  • 它可能看起来像一个微服务架构,但事实并非如此,因为服务之间高度耦合,所以服务和团队都不是自主的。

我们构建了一个分布式单体应用,它不利于轻松移动数据,也无法使用 IDE 进行重构,而且基础设施成本也更高。那么,我们如何才能避免这种情况呢?

我最基本的建议是,根据知识而不是数据来划分你的架构。一家公司如何构建知识完全取决于员工及其业务,但有几种模式可以尝试,而且成本低廉。

要应用这些模式,我们需要将我们的业务视为一个业务平台:我们没有单一的产品,而是一组产品。这些产品是一组适用于特定用户画像的功能。例如,基于此模式,我们可以如下图所示定义我们的购物平台:

购物平台

每个产品的成功都应该独立衡量和发展。然而,正如您所注意到的,某些跨产品模块可能存在依赖关系。例如,一键购买可能依赖于库存和用户信息,就像普通购买产品一样。我们如何确保这些依赖关系不会影响团队绩效,并且不会重复逻辑?

首先,我们需要将产品分成多个模块,以了解耦合可能发生在哪里:

产品之间耦合?

如您所见,和都1 click purchase需要Purchase来自相同来源的信息。但是,如果我们深入研究,就会发现差异:

  • 使用1 click purchase和的买家是standard purchase同一个吗?
  • 在这两个过程中我们需要的有关书籍的信息是否相同?
  • 这两种产品中的股票信息是否以相同的方式相关?
  • 这两种产品的装运信息使用方式是否相同?

如果这些问题是yes,那么我们很可能构建的是两次相同的产品,因此很可能其中一些问题至少是no, they are different。让我们仔细看看:

数据源 一键购买 购买
买家 仅限之前已购买过其他书籍的读者 每个人
图书 我们需要所有可能的信息 我们需要所有可能的信息
库存 我们只需要知道是否有足够的库存 我们需要知道什么时候库存低,以推动用户购买
运输 仅限送货上门 送货上门公司

在我们的案例中,只有书籍具有相同的特征,它们不是行为,而是数据。这意味着我们的产品是有界上下文的,其中的知识和对用户问题的理解是不同的。这是有道理的,因为我们将知识与产品联系起来,将产品与用户画像联系起来

当我们在有界上下文之间共享信息时,我们应该尽可能地以团队绩效为先。这意味着有时我们需要重复知识。这在其他系统中很常见:我们的浴室和厨房里都有水槽。跨有界上下文共享数据的方式有很多种,我个人更喜欢使用基于事件的架构(例如 SQS)或数据流平台(例如 Kafka,用于状态溯源)进行数据流传输。您也可以使用更简单的工具(例如数据库视图)共享信息(如果您拥有 Yugabyte 或 AWS RDS 等分布式数据库)。

即使这些模式看起来很浪费,但想想我们的身体是如何运作的。我们的身体一直在向肌肉和器官输送血液,以确保血液的供应和健康。现在想象一下,在你的身体里,每当一块肌肉想要运动时,都需要向心脏请求血液,因此心脏也需要向肺部请求氧气。现在,每块肌肉每秒重复一次。

很久很久以前,人体

然而,这些信息需要来自其他有界上下文(例如,新买家的注册流程),并且它们需要所有者。我们可以反复修改、拆分更多产品,直到拥有更小、更易于团队处理的模块。例如,下图展示了一个虚构的图书购物平台上的产品及其依赖关系:

基于产品的依赖关系

如果我们发现大多数相关信息都暴露给其他产品(例如,可能发生所有信息都以同样的方式暴露在其他产品中Express Sign Up并被Profile Sign Up读取的情况),我们可以将产品集中到更通用的产品(对于角色来说是通用的,而不是对于企业来说是通用的)并公开更简单的服务(如 UserService)。

总而言之,我想分享一些我认为有用的观点:

  • 平台化思考可以让我们更好地拆分业务。
  • 将产品与人物角色以及有界上下文联系起来使得边界变得明确。
  • 状态源和事件驱动架构对于构建分布式可用平台至关重要。
  • 团队不应该共享代码,而应该共享一个共同的平台。

感谢阅读!

文章来源:https://dev.to/kmruiz/to-domain-driven-design-6ao
PREV
💪使用这些超棒的工具成为前端大师🖱
NEXT
HTTPS In Development: A Practical Guide