领域驱动设计(Domain-Driven Design,简称DDD)是一种业务建模方法论,它强调在软件项目中建立一套领域模型作为通用语言的核心。这种方法旨在解决领域专家与工程人员之间的沟通问题,通过创建一种共通语言来减少误解和翻译成本。在微服务架构兴起之前,DDD就已经存在,并因其能够与微服务技术架构相结合而受到广泛关注。

  1. 共识成本的降低:在传统的软件开发过程中,领域专家和工程人员往往使用不同的术语和工具来描述业务需求和技术架构。例如,金融领域的专家可能熟悉“回撤”这一术语,但工程人员可能需要将其理解为“跌了”。使用DDD方法,可以在双方之间建立一套共识,从而降低互相翻译造成的误解和成本。

  2. 实体、领域事件和聚合根:DDD引入了一些新的概念,如实体、领域事件和聚合根等。这些概念有助于更好地组织和管理业务逻辑,使代码更加清晰和易于理解。

  3. 面向群体的扩展:虽然DDD主要针对程序员,但它也为产品经理、领域专家等非技术人员提供了帮助。通过学习和实践DDD, 这些非技术人员可以更好地理解软件开发过程,从而更有效地参与到项目中。 总之,DDD是一种有助于降低共识成本、提高沟通效率的业务建模方法论。通过将DDD融入代码设计,可以帮助团队更好地理解和实现业务需求,从而提高项目的成功率。


在实践过程中,工程人员和产品经理之间常常因为技术术语的理解和沟通障碍而产生误解。这种误解主要体现在两个方面:一是工程人员难以清晰描述业务系统的功能和行为,二是非工程人员由于无法感知技术细节而导致语义误解。

  1. 难以描述的业务系统—贫血模式下的失忆症 当产品经理询问工程人员如何用一句话来描述他们的系统时,工程人员通常会使用一些专业术语,如“用户输入xx的时候,会调用一个xxx方法,然后插入到xxx表”。然而,对于没有技术基础的产品经理来说,这些术语可能会让他们感到困惑。因此,工程人员需要尝试用更通俗易懂的语言来解释系统的功能和行为,并结合图文说明来帮助产品经理理解。

  2. 快速定位线上问题 当工程人员作为值班人员处理线上问题时,他们需要快速定位问题所在。传统的业务代码架构通常采用贫血模式,即业务逻辑与数据模型分离。在这种模式下,数据表是现实事物的抽象,而业务代码负责对这些表记录进行增删改查。然而,随着业务的发展,核心事物的状态越来越多,业务代码也变得越来越复杂。这使得新人很难快速熟悉和上手业务,甚至可能导致一些特殊的逻辑分支成为大泥团。因此,工程人员需要学会如何更好地组织和管理代码,以便在处理线上问题时能够迅速定位问题所在。 总之,为了减少工程人员和产品经理之间的沟通障碍,工程人员需要努力提高自己的沟通能力,学会用简单易懂的语言解释技术概念;同时,也需要关注代码的组织和管理,以便在处理线上问题时能够迅速定位问题所在。

业务流程重构经验分享

1. 业务流程梳理的挑战

  • 背景:毕业时接到的第一个任务是对一个业务流程进行重构。
  • 核心问题:核心事物的状态多达十几种,状态转换代码分散在工程各处。
  • 解决过程:花费了一周时间梳理主要业务流程。

2. 术语使用中的误解

  • 问题描述:工程人员和领域专家使用的不是同一套术语,导致沟通障碍。
  • 具体例子
  • 工程人员视角:关注接口、数据库等技术细节。
  • 领域专家视角:关注业务流程和逻辑。
  • 影响:术语不统一导致工程人员难以快速理解业务,领域专家难以理解技术细节。

3. 术语的限界上下文问题

  • 问题描述:同一术语在不同场景下可能有不同的含义。
  • 具体例子
  • 保险单
  • 客户投保场景:表示投保单。
  • 缴费完成后:变成保单。
  • 开发者视角:关注数据表。
  • 业务视角:可能表现为不同的业务意义。
  • 问题影响:如果没有严格区分术语在不同场景下的真实语义,容易对开发形成误导。


在最近的产品需求中,提出了一个获取产品发行时间的功能。从用户体验的角度来看,这个功能要求前端根据产品的特性和投放规则生成相应的文案,并且该文案需要包含产品的发行时间信息。 然而,在技术实现上,我们将整个交互流程拆分成了两个独立的服务通路:

  • 第一条通路直接由前端调用以获取展示所需的数据。

  • 第二条通路则是为投放系统设计的,用于评估和应用相关的业务规则。 起初的设计方案里,这两条服务通路都复用了相同的代码逻辑,即直接将从数据库层获得的产品发行时间传递给各自的请求者。 但在上线后的回测过程中遇到了一个问题:内容显示通路所指的产品发行时间实际上对应的是产品首次发布的时间点;而投放系统所需的产品发行时间则意指产品距离当前已经发布了多久。由于这种理解上的偏差,导致了原本预期中的文案模板无法正确显示相关信息。最终经过问题排查,确认是由于投放系统不支持直接解析原始的产品出现时间造成的。为此,团队不得不紧急发布了修复版本来解决这个问题。

技术细节的误解与限界上下文(BC)

在软件开发过程中,非技术人员或不负责当前服务的人员可能由于对技术细节的不了解,导致对实际开发工作的误导。为了解决这一问题,领域驱动设计(DDD)提出了限界上下文(BC, Bounded Context)的概念。我个人更倾向于称之为上下文语义。在不同的限界上下文中,同一事物可能有不同的定义。通过划分限界上下文,开发者可以更清晰地区分事物在当前语境下的真实含义。

如何理解限界上下文

  • 定义: 限界上下文是DDD中用于定义特定领域内概念和术语的边界。

  • 作用: 它帮助团队成员理解在特定上下文中,某一概念或术语的具体含义。

  • 应用: 在建模时,通过明确限界上下文,可以避免概念混淆,提高开发效率。

达成共识的策略

DDD不仅是一种业务建模的方法论,它还贯穿了从时间风暴到代码落地的整个流程。以下是一些关键步骤,可以帮助团队达成共识:

  1. 事件风暴: 通过事件风暴,团队可以共同识别和讨论领域中的事件和业务流程。

  2. 领域故事分析: 通过领域故事分析,团队可以深入理解业务需求和领域知识。

  3. 提取领域对象: 在这一步骤中,团队成员共同识别和定义领域对象,确保对领域的理解一致。

跨领域合作的重要性

  • 领域专家的参与: 领域专家的参与对于确保领域对象和概念的准确性至关重要。

  • 工程人员的协作: 工程人员需要与领域专家紧密合作,以确保技术实现与业务需求的一致性。

  • 共识的达成: 通过跨领域的合作,团队可以达成对业务需求和技术实现的共识。


在传统的数据表设计方式中,行为通常是围绕着数据库表结构来组织的,这种方式对于领域专家来说可能不够直观。领域驱动设计(DDD)则提供了一种不同的视角,它更注重业务模型中实体的设计,这些实体并不一定与数据库中的表结构一一对应。 DDD提倡使用充血模式,这意味着将属于某个实体的行为直接归集到该实体本身,这样可以使得代码更加符合人们的直觉,并且更好地反映现实世界的情况。 例如,考虑一个Person(人)对象和Walk(走路)方法。如果我们想要表达一个人在特定道路上行走这一概念,按照DDD的原则,我们应该定义为Person.Walk(Pavement)这样的形式。这比在一系列分散的业务逻辑代码中更新Person记录的状态到Walk,然后保存到数据库,而在另一个地方通过调用DAO函数将其状态改为Swim要更为自然和易于理解。

通过这种方式,DDD帮助开发团队创建出更能体现业务逻辑的软件系统,同时也能让领域专家更容易参与到软件开发过程中。

在这样一种符合人类直觉的共同语言下相互磨合,领域专家和工程人员最终会形成一套设计大纲,在描述业务的同时能够指导代码落地。


降低扩展成本业务总是不断变化的,系统也在不断的扩展进化。一方面需求的增加最终会体现在代码变动上,另一方面子模块的功能会不断膨胀直到最后被拆分成更小的子系统或微服务。具有DDD风格的代码一般是“高内聚低耦合”的,模块间松耦合的意义一个在于能够将改动点聚焦到子模块内,减少不同模块变更时的相互影响;另外,当模块需要独立成一个服务时,DDD架构可以用更低的成本进行迁移。依赖反转——理解DDD与三层架构的差异相信不少程序员在看过各种DDD的文章仍是一头雾水的时候,会抄起一些Demo开始阅读,并分析项目架构。java出身的开发者应该会拿较为熟悉的经典的Controller-Service-Dao三层架构和DDD架构做对比。在看过下图之后很容易会得出这样一个结论:DDD应该就是把Service层的代码拆分到了应用层和领域层。 aa

然而这个观点只关注到了形式上的差异,并没有说出DDD本质——依赖反转。在跟别人分享DDD的时候,我更倾向于使用下面这个图。


在软件架构设计中,传统三层架构与领域驱动设计(DDD)有着不同的关注点和依赖顺序。 aaaaaaa### 传统三层架构的依赖顺序

  • Controller层:首先定义需要提供的功能。
  • Service层:基于Controller的需求来提供对应的方法。
  • DAO层:Service层的功能由DAO层的具体实现决定,通常直接映射到数据库表的操作。
  • 数据库表:最后,根据业务需求设计数据库结构。 这种设计下,如果库表结构与实际业务紧密相关,那么大部分行为会体现在DAO层接口上,比如PersonDao.updateBalance(personId, newBalance)等方法。随着业务复杂度增加,Service层和Controller层可能会变得臃肿,成为简单的事务脚本,主要负责对数据进行CRUD操作。此外,一旦底层存储技术发生变化,如从关系型数据库切换至NoSQL解决方案,可能需要重构整个系统。

DDD的设计哲学

  • 领域优先:在DDD中,首要任务是分析并确定聚合根、实体等关键领域对象,并将业务逻辑集中于这些对象内部。
  • 数据库作为数据持久化手段:DB仅被视为一个保存数据的地方,而不是业务逻辑的一部分。
  • 接口层的作用:对外部交互时,将领域层的结果转换成适合外部使用的格式。 通过这种方式,可以使得领域模型更加健壮且易于维护,同时支持灵活的数据存储选项。

新需求上线如何又快又好?

为了快速而有效地应对新需求,并保持系统的可测试性,以下几点建议至关重要:

  • 核心聚焦于领域:确保领域层是最先被考虑和设计的部分,其他如存储或外部调用应围绕它构建。
  • 面向接口编程:对于任何外部依赖,包括存储服务或第三方API,都应首先定义清晰的接口规范,然后开发具体的实现。
  • 本地模拟实现:在正式联调之前,可以通过创建简单版本的服务实现来进行早期自测,以减少后期集成时可能出现的问题。
  • 模块化设计:鼓励采用高度模块化的架构风格,这样即便是在大型项目中也能相对独立地工作于不同组件之上,从而加快迭代速度。 遵循上述原则可以帮助团队更高效地交付高质量的产品。


在开发过程中,如果遇到需要依赖其他模块的情况,而该模块尚未完成开发,可以采取以下步骤进行独立开发和测试:

  1. 接口定义:首先,确保与下游模块的接口定义清晰,并有样例数据。

  2. 本地模拟:创建一个MockDataRepositoryImpl类来模拟下游模块的接口响应,返回预先定义的样例数据。

  3. 独立开发:在本地使用MockDataRepositoryImpl进行开发,确保自己的模块能够独立运行和测试。

  4. 接口解耦:设计DataRepository接口和DataRepositoryImpl实现类,确保远程调用与业务逻辑的解耦。

  5. 单元测试:编写针对远程调用的单元测试用例,确保每个测试覆盖的粒度尽可能小。

  6. 集成测试:在单元测试通过后,将远程调用集成到整体代码中,进行集成测试。 通过以上步骤,可以在下游模块开发完成前,先行开发和测试自己的模块,从而提高开发效率。

领域驱动设计(DDD)的优势

1. 业务行为聚焦

DDD将业务行为聚焦到领域层,使得业务逻辑更加集中和清晰。

2. 外部依赖的松耦合

通过保证外部依赖的松耦合,实现了可插拔性,从而使得各个部分可以独立开发和测试。

3. 提高开发效率

这种设计模式可以极大提高整个需求完成的进度,因为它允许团队并行工作。

7. 降低架构演进成本

微服务拆分的挑战

在进行微服务拆分时,需要考虑如何低成本地进行拆分。这与软件设计中的“单一责任”原则相似,目标是降低服务的复杂性和发布流程的难度。

拆分实践

大多数服务拆分是基于技术功能或业务形态进行的。例如,将通用查询模块抽象成一个DAO服务。

架构的持续演进

随着时间的推移,即使是拆分后的小单体服务也可能再次变得庞大,需要重复进行业务梳理和拆分。

架构的灵活性

一个好的项目架构应该能够像细胞裂变一样灵活地进行拆分。 注意: 请将 ’ ’ 替换为 ‘aaaaaaa’


在DDD(领域驱动设计)架构中,聚合是一个关键概念,它定义了最小粒度的业务一致性边界。每个聚合内包含一个或多个实体,这些实体不仅封装了数据,还拥有自身的行为。领域服务负责协调这些实体的方法,为外部提供基本的功能单元。聚合的设计保证了它们之间是松耦合的;当需要处理跨聚合的操作时,这样的复杂逻辑会在应用服务层进行编排,通过调用不同聚合内的领域方法来实现业务流程。

为了保持系统结构清晰和业务逻辑的一致性,DDD强调通过聚合来组织模型,确保变更被限制在一个小范围内,并且能够独立于其他部分进行开发和部署。


在前文的讨论中,我们强调了领域驱动设计(DDD)的核心思想是将业务行为聚集到特定的领域中。当一个拥有多个聚合的服务需要拆分成不同的微服务时,可以直接将整个聚合迁移到新服务的项目目录中。这种方式与传统架构相比,可以以更低的成本进行服务拆分。 然而,领域驱动设计并非银弹。在实践中,可能会遇到各种问题,甚至可能发现DDD并不适合某些业务场景。首先,学习DDD的成本非常高,对于非技术人员来说,需要学习大量的新概念和术语;对于工程师来说,这是一种颠覆传统设计思想的方法论。此外,DDD在事件风暴期间需要领域专家的参与,而领域专家需要对业务的全貌有一个清晰的认识。然而,对于许多互联网业务来说,它们往往是在摸索中前进,无法预先知道整个业务的全貌。因此,系统架构和业务都会随着时间的推移而发展。 尽管如此,学习和拓展事业的目的是在遇到糟糕实现时能够意识到有更好的解决方案。在实际设计过程中,确实会遇到沟通不畅、概念误解、难以扩展等问题,但仍然可以参考DDD的思想。例如,最近开发的项目需要将RPC的stub桩统一到一块,因为实现使用了Repository模式进行隔离,使得桩文件的相关实现不会渗透到业务逻辑中。这样,无论stub桩是如何生成的,都可以快速且稳定地切换到新的调用风格。再如上述提到的产品发行时间问题,可以通过复用同一套领域代码来解决,只需在接口层将所需的结果翻译成上游所需的格式即可。

设计原则:高内聚低耦合

在软件开发中,设计原则至关重要。保持模块间的高内聚和低耦合,有助于适应业务变动和扩展。

领域驱动设计(DDD)

领域驱动设计(DDD)是一种流行的设计方法论,它强调降低沟通成本和系统扩展成本。通过领域专家和工程人员的合作,形成一套共同语言,指导设计出可进化的系统。

DDD的优势

  • 促进领域专家与工程人员之间的沟通。

  • 指导设计出易于变更和扩展的系统。

DDD的挑战

  • 学习成本高。

  • 业务全貌缺乏。

工程实践

工程人员应借鉴DDD的设计思想,提升代码质量。同时,关注系统的可扩展性和业务建模的可理解性。

重要技能

  • 系统设计。

  • 业务建模。

  • 技术架构适应性。

结论

成为一名优秀的工程人员,不仅要关注技术细节,还要考虑整体的系统架构和业务需求。这些技能对于快速稳定迭代和适应新技术架构至关重要。