@Lenciel

Bounded Context

定义

Bounded Context 是领域驱动设计中战略设计的重要组成部分,一定程度上决定了系统的逻辑架构以及集成方式。

基于康威定律,Bounded Context 的划分还可能会影响进行项目开发实施的组织的结构。

DDD 社区将 Bounded Context 定义为:

应该显式地定义某个模型所应用的上下文。还应该在团队组织、应用中特定部分的使用以及像代码库和数据库模式等物理表现等方面显式地设定边界。要保持边界中模型的严格一致,而不要受外界问题的影响与干扰。

这段话在说的无非是「边界」,通过为领域模型划定合理的边界,就可以降低设计与开发的复杂度。此外,边界还能够划分知识的层次,例如对外而言,可以只保障暴露在边界外接口的一致性,以及关注它们之间的集成方式。边界之内则自成一体,可以独立演化,甚至可以包容一到多个遗留模块。

常见问题

正是因为 Bounded Context 带来的隔离性,Juelin Lerman才认为:「把一个将大量的类放在一个上下文中的独立模型分解为多个较小的模型是有好处的。Bounded Context 创建的模型较小,而且内聚性更高,同时维持了模型之间的边界。」

好处听起来都是好的,但是难免会有下面这些问题:

如何确定或划分 Bounded Context? Bounded Context 是否具有层次? Bounded Context 划分的边界是逻辑的,还是物理的? Bounded Context 之间的通信方式?

也难怪在文章《DDD: The Bounded Context Explained》中 Mike 要说,Bounded Context 是 DDD 中最难解释的原则,但或许也是最重要的原则。可以说,没有 BC,就不能做 DDD。在了解 Aggregate Root、Aggregate、Entity 等概念之前,需要先了解 BC。

Vaughn Vernon 的Implementing Domain-Driven Design解释如下:

Bounded Context是一个显式的边界,领域模型便存在于这个边界之内。领域模型把通用语言表达成软件模型。创建边界的原因在于,每一个模型概念,包括它的属性和操作,在边界之内都具有特殊的含义。

这里是从设计原则上来规约出 Bounded Context 的定义:

  • 它是模型概念,与实现无关,是高层的抽象机制
  • 具有自己独立的边界,是自治的,遵循高内聚、松耦合
  • 不同的 Bounded Context 之间的关系决定它们之间的协作与通信方式
  • 它与 Domain 应为一一对应关系
  • 一个上下文意味着一个专有的职责

Reference

  1. BoundedContext by Martin Fowler
  2. Implementing Domain-Driven Design
  3. Shared Data Model using Bounded Context

CQRS

什么是CQRS

CQRS 是 Command Query Responsibility Segregation 的缩写,翻译过来是「命令查询职责分离」。

它的概念源于 Betrand Meyer(Eiffel 语言之父,开-闭原则 OCP 提出者)在《Object-Oriented Software Construction》这本书中提到的「命令查询分离 (Command Query Separation,CQS) 」:把改变对象状态的命令(Command),与获取对象状态的查询(Query)拆分。

Greg Young在 2010 年自己的一篇博客中对 CQS 进行了改进和简化,提出了 CQRS:最核心的改动是,不仅仅对方法进行拆分,而是从数据模型上进行隔离。

实施方式

因为查询操作不会造成数据的修改,所以它属于一种幂等操作,可以反复地发起,而不用担心会对系统造成影响。基于这种特性,我们还可以为其提供缓存,从而改进查询的性能。

命令操作则与之相反,它会直接影响系统信息的改变,因此,和查询操作相比,对事务的要求也不一样。

从请求响应的角度来看,查询操作常常需要同步请求,实时返回结果;命令操作则不然,因为我们并不期待命令操作必须返回结果,这就可以采用 fire-and-forget 方式,而这种方式正是运用异步操作的前提。

在实践上,因为两个数据模型分开了,存储也可以分开,所以可以使用主从数据库。主数据库处理 CUD,从库处理 R,从库的的结构可以和主库的结构完全一样,也可以不一样,从库主要用来进行只读的查询操作。

在数量上从库的个数也可以根据查询的规模进行扩展,在业务逻辑上,也可以根据专题从主库中划分出不同的从库。从库也可以实现成ReportingDatabase,根据查询的业务需求,从主库中抽取一些必要的数据生成一系列查询报表来存储。

适用场景

把系统建模成领域对象状态迁移的一个状态机,可以让系统从 Data-Driven 演进成 Task-Driven 甚至是 Event-Driven,这听起来很美。

但 CQRS 解决的,主要还是业务复杂度和性能方面的问题。它的引入是会带来极大复杂度的。因此,以下场景中,可以考虑使用 CQRS 模式:

  • 当在业务逻辑层有很多操作需要对相同的实体或者对象进行的时候。CQRS 使得我们可以对读和写定义不同的实体和方法,从而可以减少或者避免对某一方面的更改造成冲突
  • 对于一些基于任务的用户交互系统,通常这类系统会引导用户通过一系列复杂的步骤和操作,通常会需要一些复杂的领域模型,并且整个团队已经熟悉领域驱动设计技术。写模型有很多和业务逻辑相关的命令操作的堆,输入验证,业务逻辑验证来保证数据的一致性。读模型没有业务逻辑以及验证堆,仅仅是返回 DTO 对象为视图模型提供数据。读模型最终和写模型相一致。
  • 适用于一些需要对查询性能和写入性能分开进行优化的系统,尤其是读/写比非常高的系统,横向扩展是必须的。比如,在很多系统中读操作的请求时远大于写操作。为适应这种场景,可以考虑将写模型抽离出来单独扩展,而将写模型运行在一个或者少数几个实例上。少量的写模型实例能够减少合并冲突发生的情况
  • 适用于一些团队中,一些有经验的开发者可以关注复杂的领域模型,这些用到写操作,而另一些经验较少的开发者可以关注用户界面上的读模型。
  • 对于系统在将来会随着时间不段演化,有可能会包含不同版本的模型,或者业务规则经常变化的系统
  • 需要和其他系统整合,特别是需要和事件溯源 Event Sourcing 进行整合的系统,这样子系统的临时异常不会影响整个系统的其他部分。

但是在以下场景中,可能不适宜使用 CQRS:

  • 领域模型或者业务逻辑比较简单,这种情况下使用 CQRS 会把系统搞复杂。
  • 对于简单的,CRUD 模式的用户界面以及与之相关的数据访问操作已经足够的话,没必要使用 CQRS,这些都是一个简单的对数据进行增删改查。
  • 不适合在整个系统中到处使用该模式。在整个数据管理场景中的特定模块中 CQRS 可能比较有用。但是在有些地方使用 CQRS 会增加系统不必要的复杂性。

Reference