答疑:有关3个典型问题的讲解

你好,我是欧创新。

截至今天这一讲,我们的基础篇和进阶篇的内容就结束了。在这个过程中,我一直有关注大家提的问题。那在实战篇正式开始之前啊,我想针对3个比较典型的问题,做一个讲解,希望你也能同步思考,调动自己已学过的内容,这对我们后面实战篇的学习也是有一定帮助的。

问题1:有关于领域可以划分为核心域、通用域和支撑域,以及子域和限界上下文关系的话题,还有是否有边界划分的量化标准?

我在 [第 02 讲] 中讲到了,在领域不断划分的过程中,领域会被细分为不同的子域,这个过程实际上是将问题范围不断缩小的过程。

借用读者“密码123456”的总结,他认为:“对于领域问题来说,可以理解为,对一个问题不断地划分,直到划分为我们熟悉的、能够快速处理的小问题。然后再对小问题的处理排列一个优先级。”

这个理解是很到位的。在领域细分到一定的范围后,我们就可以对这个子域进行事件风暴,为这个子域划分限界上下文,建立领域模型,然后就可以基于领域模型进行微服务设计了。

虽然DDD没有明确说明子域和限界上下文的关系。我个人认为,子域的划分是一种比较粗的领域边界的划分,它不考虑子域内的领域对象、对象之间的关系和结构。子域的划分往往按照业务阶段或者功能模块边界进行粗分,其目的就是为了让你能够在一个相对较小的问题空间内,比较方便地用事件风暴来梳理业务场景。

限界上下文本质上也是子域,限界上下文是在明确的子域内,用事件风暴划分出来的。它体现的是一种详细的设计过程。这个过程设计出了领域模型,明确了领域对象以及领域对象的依赖等关系,有了领域模型,你就可以直接进行微服务设计了。

关于核心域、通用域和支撑域,划分这三个不同类型子域的主要目的是为了区分业务域的优先级,确定IT战略投入。我们会将重要的资源投入在核心域上,确保好钢用在刀刃上。每个企业由于商业模式或者战略方向不一样,核心域会有一些差异,不要用固定的眼光看待不同企业的核心域。

核心域、通用域和支撑域都是业务领域,只不过重要性和功能属性不一样。采用的DDD设计方法和过程,是没有差异的。

从目前来看,还没有可以量化的领域以及限界上下文的划分标准。它主要依赖领域专家经验,以及和项目团队在事件风暴过程中不断地权衡和分析。不要奢望一次迭代就能够给复杂的业务,建立一个完美的领域模型。领域模型很多时候也需要多次迭代才能成型,它也需要不断地演进。但如果是用DDD设计出来的领域模型的边界和微服务内聚合的边界非常清晰的话,这个演进过程相对来说会简单很多,所需的时间成本也会很低。

问题2:关于聚合设计的问题?领域层与基础层为什么要依赖倒置(DIP)?

聚合主要实现核心业务逻辑,里面有很多的领域对象,这些领域对象之间需要通过聚合根进行统一的管理,以确保数据的一致性。

在聚合设计时,我们会用到两个重要的设计模式:工厂(Factory)模式和仓储(Repository)模式。如果你有兴趣详细了解的话,推荐你阅读《实现领域驱动设计》一书的第11章和第12章。

那为什么要引入工厂模式呢?

这是因为有些聚合内可能含有非常多的实体和值对象,我们需要确保聚合根以及所有被依赖的对象实例同时被创建。如果都通过聚合根来构造,将会非常复杂。因此我们可以通过工厂模式来封装复杂对象的创建过程,但并不是所有对象的构造都需要用到工厂,如果构造过程不复杂,只是单一对象的构造,你用简单的构造方法就足够了。

又为什么要引入仓储模式?解答这个问题的同时,我也一起将依赖倒置的问题解答一下。

在传统的DDD四层架构中,所有层都是依赖基础层的。这样做有什么不好的地方呢?如果应用逻辑对基础层依赖太大的话,基础层中与资源有关的代码可能会渗透到应用逻辑中。而现在技术组件的更新频率是很快的,一旦出现基础组件的变更,且基础组件的代码被带入到了应用逻辑中,这样会对上层的应用逻辑产生致命的影响。

为了解耦应用逻辑和基础资源,在基础层和上层应用逻辑之间会增加一层,这一层就是仓储层。一个聚合对应一个仓储,仓储实现聚合内数据的持久化。聚合内的应用逻辑通过接口来访问基础资源,仓储实现在基础层实现。这样应用逻辑和基础资源的实现逻辑是分离的。如果变更基础资源组件,只需要替换仓储实现就可以了,不会对应用逻辑产生太大的影响,这样就实现了应用逻辑与基础资源的解耦,也就实现了依赖倒置。

关于聚合设计过程中的一些原则问题。大部分的业务场景我们都可以通过事件风暴,找到聚合根,建立聚合,划分限界上下文,建立领域模型。但也有部分场景,比如数据计算、统计以及批处理业务场景,所有的实体都是独立无关联的,找不到聚合根,也无法建立领域模型。但是它们之间的业务关系是非常紧密的,在业务上是高内聚的。我们也可以将这类场景作为一个聚合处理,除了不考虑聚合根的设计方法外,其它诸如DDD分层架构相关的设计方法都是可以采用的。

一些业务场景,如果复杂度并不高,而用DDD设计会带来不必要的麻烦的话,比如增加复杂度,有些原则也是可以突破的,不要为做DDD而做DDD。即使采用传统的方式也是没有关系的,最终以解决实际问题为最佳。但必须记住一点,如果采用传统的设计方式,一定要保证领域模型的边界以及微服务内聚合的逻辑边界清晰,这样的话,以后微服务的演进就不会太复杂。

问题3:领域事件采用消息异步机制,发布方和订阅方数据如何保证一致性?微服务内聚合之间领域事件是否一定要用事件总线?

在领域事件设计中,为了解耦微服务,微服务之间数据采用最终一致性原则。由于发布方是在消息总线发布消息以后,并不关心数据是否送达,或者送达后订阅方是否正常处理,因此有些技术人会担心发布方和订阅方数据一致性的问题。

那在对数据一致性要求比较高的业务场景,我们是有相关的设计考虑的。也就是发送方和订阅方的事件数据都必须落库,发送方除了保存业务数据以外,在往消息中间件发布消息之前,会先将要发布的消息写入本地库。而接收方在处理消息之前,需要先将收到的消息写入本地库。然后可以采用定期对发布方和订阅方的事件数据对账的操作,识别出不一致的数据。如果数据出现异常或不一致的情况,可以启动定时程序再次发送,必要时可以转人工操作处理。

关于事件总线的问题。由于微服务内的逻辑都在一个进程内,后端数据库也是一个,微服务内的事务相比微服务之间会好控制一些。在处理微服务内的领域事件时,如果引入事件总线,会增加开发的复杂度,那是否引入事件总线,就需要你来权衡。

个人感觉如果你的场景中,不会出现导致聚合之间数据不一致的情况,就可以不使用事件总线。另外,通过应用服务也可以实现聚合之间的服务和数据协调。

以上就是3个典型问题的答案了,不知你先前是否有同样的疑惑,这些答案又是否与你不谋而合呢?如果你还有其它问题,欢迎在留言区提出,我会一一解答。

今天的内容就到这了,如果有所收获,也可以分享给你的朋友,我们实战篇见!

精选留言

  • 吃饭饭

    2019-11-06 11:42:28

    事件风暴?为什么一有关键词总会出现这个,它不就是一个集中讨论定需求的动作?陌生词汇太多,如果夹杂一些白话最好了,感觉这一套字眼真的是越说越迷糊了,DDD 我感觉说白了就是一种划分手段,核心最终都会落在为服务上
  • C J J

    2020-01-15 20:00:27

    对失血,贫血,充血解释得很不错的文档。https://www.infoq.cn/article/alibaba-freshhema-ddd-practice
    作者回复

    感谢,写的很好。

    2020-01-17 14:27:08

  • hunter

    2019-11-07 19:22:25

    基础层依赖领域层。能录个例子吗?如何反转依赖的,还是不明白
    作者回复

    一个非常简单的例子,有Person聚合根,Person仓储接口和仓储实现。
    /**
    * Person聚合根
    */
    public class Person{
    private String id;
    private String name;
    private int age;
    private boolean gender;
    }

    /**
    * Person仓储接口
    */
    public interface PersonRepositoryInterface {
    void save(Person person);
    void delete(String id);
    }

    /**
    *Person仓储实现
    */
    @Repository
    public class PersonRepositoryImp implements PersonRepositoryInterface {
    private PersonMapper mapper;
    public void save( Person person) {
    mapper.create(person);
    }
    public void delete((String id) {
    mapper.delete(id);
    }
    }
    在应用逻辑中直接用仓储的接口就可以了,数据库相关的逻辑在PersonMapper里面实现。
    PersonRepositoryInterface personRepos;
    personRepos.save(person)

    2019-11-07 21:25:10

  • 小超人

    2020-05-29 10:40:17

    “依赖倒置”不看定义我还一直因为是基础层依赖领域层。看了定义才知道,原来是基础层和领域层互相不依赖,而是共同依赖一组抽象接口。我觉得“面向接口编程”听起来更舒服。
    作者回复

    是的。依赖倒置的定义就是面向接口编程,这样不同层之间的服务在实现逻辑发生变化的时候,就不会相互影响了。

    2020-05-29 16:22:00

  • Paul

    2019-11-07 18:22:31

    老师,您好!有一些设计实现上的疑问:
    领域服务的CRUD是不是都是操作聚合根或整个实体对象,比如我只想根据ID判断记录是否存在,或者返回个别字段,需要返回整个实体对象吗?
    作者回复

    其实查询类业务可以不必经过聚合根和仓储。传统方法也可以了。
    如果聚合数据比较多,会有延迟加载影响性能。
    聚合根的主要目的是为了保证数据的一致性,这些场景一般在CU的场景。

    2019-11-07 18:40:35

  • 南山

    2019-11-06 16:35:22

    老师,实体内可以调用他所在聚合的仓储吗?
    作者回复

    一般通过聚合根来做。

    2019-11-06 18:00:57

  • 骆驼1089

    2020-04-06 23:47:40

    老师,有个问题请教一下,充血模式的优势是什么,比现有的贫血模式的优势是什么?
    作者回复

    先只从领域建模的角度来说优势吧。因为DDD是一种面向对象的编程方式,采用充血模型,每个实体就会有自己的业务行为,在领域模型设计时,每一个实体除了自己的属性外,还会有自己的业务行为,而不会将所有的业务逻辑放到业务逻辑层。这样就有利于实体、聚合的解耦。当你需要进行再次拆分的时候,就会很容易。

    2020-04-07 09:42:36

  • Jade

    2019-11-06 08:21:53

    事件总线 实现思路?用到什么技术或来源组件?还是说事件总线就是消息队列呢
    作者回复

    事件总线就是一个带发布和监听功能的jar包,直接跟你的微服务代码放一起就行了,它属于基础层的代码。提的比较多的是EventBus。你可以去网上找找资料。

    2019-11-06 10:19:34

  • 密码123456

    2019-11-06 08:03:53

    感觉有点像,设计模式中核心的一句话。封装变化。变化是无穷的,但是我们尽可能的把,变化引起的改动降到最小。
  • 祥敏

    2019-11-06 06:29:59

    您好,领域层提供仓储接口,基础层实现仓储接口,依赖倒置的设计是非常好的,能够拆除领域层对基础层的依赖。
    上层向下依赖领域层,领域层通过依赖倒置,让基础层也依赖领域层,这样就实现了领域层为核心的设计理念。
    领域驱动设计提出了全新的设计思路,讲述偏抽象,重点在如何落地,期待实战篇。
  • hunter

    2019-11-07 19:20:45

    微服务内不使用事件总线,如何保证两个聚合操作的一致性?
    作者回复

    这个需要权衡,看看引入事件总线后,这个复杂度可不可以接受。通过应用服务加事务机制应该也可以解决,在同一个进程内的事务应该比跨微服务的事务相对来说还是好控制,对性能影响也会小一些吧。

    2019-11-07 20:02:42

  • 秋雨

    2020-03-26 10:15:59

    老师你好,我有一个问题想问下:
    现有事件风暴再有领域模型还是先划分好领域,再通过事件风暴建立领域模型?
    作者回复

    领域太大的话,建议先划分子域。这是因为领域太大,不好开展事件风暴。在将领域划分成合适大小的子域后,进行事件风暴完成领域建模就比较容易了。

    2020-03-26 11:34:43

  • Geek_dn82ci

    2020-02-14 14:37:47

    子域划分的过程更偏业务侧工作,然后基于每个子域进行事件风暴(讨论)的时候,就是在梳理领域对象的过程,且发掘领域对象之后就可以分清楚哪些是值对象,哪些是实体哪些是聚合根?是这样理解吗
    作者回复

    是的。

    2020-02-15 09:36:22

  • 夙梦流尘

    2019-11-21 09:51:55

    有个问题想请教下。现在A聚合里面有B、C实体,是通过A的主键去关联的(我知道这样不好但是目前老项目就是这么搞的,重新定义实体标识风险有点大)。导致现在我用工厂创建A聚合的话,要先把A的数据落地,才能拿到A的主键,然后才能创建A聚合里的BC实体。这样的话只能 1.在工厂里就把A落地,然后填充BC实体返回完整聚合。2. 工厂只返回A聚合,然后在填充BC实体。这样工厂就没返回一个完整聚合。
    这两种我觉得都不好,麻烦指导下
    作者回复

    A的主键是由数据库的序列号生成的吗?
    如果是这样,在A的方法里面增加一个生成主键的方法呢。这样就可以在形成PO前,拿到所有实体的数据了。

    2019-11-21 13:56:47

  • 瓜瓜

    2019-11-07 20:49:54

    使用充血模型,比如RPC的接口包,接口包的实体为充血模型,该实体的很多功能需要引用很多第三方jar包,这样会不会导致接口包很沉重,比如说一个图片上传服务,客户端和服务端交互是通过图片实体来进行交互的,而图片的下载和上传是属于图片实体的内部功能,这样图片实体就会引用apahce.httpclient的很多jar包,该怎样解决这个问题呢
    作者回复

    你这种方式可能就不适合充血模型了。
    DDD用充血模型的主要目的是为了在领域模型中体现实体的业务行为,而不是所有实体的行为混杂在一起。但是这只是一个建议的设计原则,贫血模型有时候也是不可避免的。

    2019-11-07 21:46:00

  • Geek8815

    2021-07-10 13:20:37

    欧老师,倒数第三段的这句话”不会出现导致聚合之间数据不一致的情况,就可以不使用事件总线“能不能再解释下,我理解是“不会出现导致聚合之间数据不一致的情况,就可以使用事件总线“
  • Hans

    2020-07-29 15:50:11

    聚合之间如果存在聚合根吗,如果业务交互只需要属性值,是否可以直接返回属性值。
    作者回复

    不好意思啊,没明白您问的问题。
    你是不是想说聚合之间通过聚合根ID来访问得返回对象。聚合之间的访问一般放在应用层,在应用服务中它们通过聚合根ID来访问,返回的值可以是一组属性组合出来的临时对象,也可以是聚合根或实体等DO对象。

    2020-08-06 08:39:49

  • moming

    2020-07-06 20:29:59

    微服务能不能共用一个业务数据库。
    作者回复

    按照DDD的设计方法,在聚合和限界上下文之间已经做好了对象、服务和数据的解耦工作,所以基本不会出现多个微服务共用一个库的情况。

    2020-07-07 10:34:46

  • Tumayun

    2020-06-03 22:09:35

    太过抽象了,可以多举例,举一些大家都容易理解的实例
    作者回复

    后面案例里面会有详细介绍。

    2020-06-04 11:27:46

  • 剑八

    2020-05-24 18:08:19

    领域就是一个组织为了达到某个业务目的,而做的一系列业务活动的集合。
    划分子域则是为了能够从组织架构上以及软件设计上能够更加的内骤与解耦。方便进行维护。
    而子域中更细分的单位是聚合,一个聚合代表了若干能够协同工作的实体集合,一个聚合所做的事情是比较聚焦的。
    在领域驱动设计中,对于一个未知领域往往是采用从下到上的设计方法,即先根据用例,场景分析得出实体,值对象然后划分聚合以及子域,再组合子域成为一个域。