分层模式

分层模式是最常见的一种架构模式。甚至说分层模式是很多架构模式的基础,本章下面讲到的一些内容实际上都和分层模式相关联。
分层描述的是这样一种架构设计过程:从最低级别的抽象开始,称为第1层。这是系统的基础。通过将第J层放置在第J-1层的上面逐步向上完成抽象阶梯,直到到达功能的最高级别,称为第N层。
因而分层模式就可以定义为:将解决方案的组件分隔到不同的层中。每一层中的组件应保持内聚性,并且应大致在同一抽象级别。每一层都应与它下面的各层保持松散耦合。
分层模式的关键点在于确定依赖:即通过分层,可以限制子系统间的依赖关系,使系统以更松散的方式耦合,从而更易于维护。
相对于分层,还有一种概念叫分区。分层是对架构的横向划分,而分区是对架构的纵向划分。
常见的分层架构模式:

  • 单层模型:早期批处理系统会使用到。
  • 二层模型:客户端-服务器模型(C/S)
  • 三层模型:MCV(用户表示层、业务逻辑层、数据层)、网络系统常用(核心层、汇聚层和接入层)
  • 四层模型:RUP典型分层(应用层、专业业务层、中间件层、系统软件层)
  • 六层模型:功能层(用户界面)、模块层、组装层(软件总线)、服务层(数据处理)、数据层、核心层。


    传统三层架构

    传统的三层架构便是三层模式了,三层式的集中式架构,采用面向对象的设计方法,业务逻辑分表现层、领域层、数据访问层,这种架构很容易某一层或者几层变得臃肿,扩展性较差, 另外摩尔定律失效, 单台机器性能有限。

分布式架构

三层是的集中式架构会引发很多问题,于是便有了分布式架构,简单的说,“分工协作,专人做专事”就是分布式架构的概念。
基于某种分层策略,把系统拆分成许多个小模块,这是分布式架构必须干的事情,也是最难的事情,因为分层策略的设定会影响到整个系统的迭代发展。
以微服务来举例子,微服务的粒度应该多大 ?微服务如何设计呢?微服务如何拆分 ?微服务边界在哪里 ?
很长时间人们都没有解决这一问题,就连Martin Fowler在提出微服务架构的时候也没有告诉我们这该如何拆分微服务。
甚至在很长的时间里人们对微服务拆分产生了一些误解, 有人认为:”微服务很简单,就是将之前的单体应用拆分成多个部署包, 或者将原来的单体应用架构替换为一套支持微服务的技术架构,就算是微服务了。” 还有人认为微服务应该拆分得越小越好。
鉴于上述情形, 很多项目因为前期拆分过度, 导致复杂度过高, 导致后期难以运维甚至难以上线。
可以得出一个结论:微服务拆分困境产生的根本原因就是不知道业务或者微服务的边界到底在什么地方。换句话说,确定了业务边界和应用边界,这个困境也就迎刃而解了。
而DDD就是解决了这个确定业务边界的问题,可见DDD并不是一种技术架构,而是一种划分业务领域范围的方法论。DDD的兴起是由于很多熟悉领域驱动建模(DDD)的工程师在进行微服务设计时, 发现用DDD的思路进行业务梳理可以很好规划服务边界, 可以很好实现微服务内部和外部的”高内聚、低耦合”。于是越来越多的人将DDD作为业务划分的指导思想。
那么,什么是DDD呢?

DDD

DDD(domain driver designer,也就是领域驱动设计),它有三个关键词:领域,驱动,设计。领域,是要探索业务的边界;驱动,表示前者是后者的决定性因素;设计,包括产品设计,UIUE设计,软件设计。它不仅仅是开发架构的方案,而是完整的解决方案实施思路。正是因为它是完整的方案,才能让领域专家,产品和研发真正在同一个角度去思考和沟通,避免推诿扯皮,含糊不清。
从上文就可以知道DDD是一种拆解业务、划分业务、确定业务边界的方法, 是一种高度复杂的领域设计思想,将我们的问题拆分成一个个的域, 试图分离技术实现的复杂性,主要解决的是软件难以理解难以演进的问题,DDD不是一种架构, 而是一种架构方法论, 目的就是将复杂问题领域简单化, 帮助我们设计出清晰的领域和边界, 可以很好的实现技术架构的演进。下面为常用的的设计架构图,先不用刻意去理解名词,后面会逐步说明。
image.png

既然是领域驱动设计,那么说明设计是最重要的。比如产品经理提出了一个需求,我们一般都会以自底向上的数据驱动模式来开发,也就是先建好数据库,再进行编程。而领域驱动则是一种自顶而下的模式,先设定好领域内的功能,设定业务活动,最后才去数据库建表。
DDD有两个设计理念,战略设计和战术设计(一般用四色建模法,用例分析法,事件风暴,领域故事讲述法)

  • 战略设计:主要从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。
  • 战术设计则:主要从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现。

可以看出,上面两个设计理念主要是为了划分出模型与边界,一般情况下我们可以通过三步来确定领域模型和微服务边界:

  1. 在战略讨论中(比如事件风暴)中梳理业务过程中的用户操作、事件以及外部依赖关系等,根据这些要素梳理出领域实体等领域对象。
  2. 根据领域实体之间的业务关联性,将业务紧密相关的实体进行组合形成聚合,同时确定聚合中的聚合根、值对象和实体。
  3. 根据业务及语义边界等因素,将一个或者多个聚合划定在一个限界上下文内,形成领域模型。限界上下文之间的边界是第二层边界,这一层边界可能就是未来微服务的边界,不同限界上下文内的领域逻辑被隔离在不同的微服务实例中运行,物理上相互隔离,所以是物理边界,边界之间用实线来表示。

战略设计

在某个领域里,核心围绕上下文的设计,主要关注上下文的划分、上下文映射的设计、通用语言的定义。
通俗来说就是指在某个系统内,核心围绕子系统的设计,主要关注子系统的划分,交互方式,核心术语的定义。

限界上下文分离领域模型

限界上下文原文说它是语义和语境上的边界,个人理解是,它是在描述组织交付出来,面向客户的交付边界。如果是在SaaS场景,一个限界上下文应该是一个独立交付的软件;在PaaS场景,它应该说的是一个独立售卖的模块。它的含义是找到一个边界,要把这个边界以外的当成是无法改变的客观环境,不要幻想这个边界以外的人会配合你一起完成交付。那这一步设计就很好理解了,就是找到你业务对外承诺的边界,你要发展的业务在这个边界内,而不在此之外。如果你是对内交付的系统,那么你对其他同事交付的业务边界,就是你的业务限界上下文。
一个组织里,最核心的限界上下文被称为核心域。通常除了它,还有通用子域和支撑子域。通用子域是很成熟的业务,通常可以外包或者购买现成的解决方案,比如搜索子域可以通过ES来支持;支撑子域通常没有现成产品,但是它没有核心域重要,因此也可以一定程度的外包,避免在核心域之外浪费资源,比如大多数公司的数据库中间件是在开源产品上做了一些定制开发和维护。
限界上下文这个概念的目的是为了在业务扩展的时候,防止向领域内注入概念,导致业务变得没有边界,纠缠在一起。

限界上下文发展通用语言

当我们有了业务的限界上下文以后,就需要在这个限界上下文中发展一种语言用于表达软件模型,这个语言就叫做这个限界上下文里的通用语言。它可以是任何计算机语言、人类语言或者图形,只要能让团队内的每个人都能看懂。
通用语言不止是名词,它应该使用一系列具体的模型场景来描述领域模型。它描述了各种业务组件(不是技术组件)做什么,而不是用例或者用户故事。
比如微信朋友圈点赞这个场景,通用语言可能是:用户可以通过点赞,使得某个朋友圈的Feed发出人收到被点赞的通知,达到互动的目的。


使用上下文映射来集成多个限界上下文

上下文映射是两个限界上下文之间的连线,表示了这两个概念之间的关系,也表示这这两个概念的通用语言的翻译。通常来说,不同的限界上下文是不同的团队在维护,那么此时它也代表着两个团队之间合作的关系。
我们常见的映射关系是RPC接口。然而在领域设计里,限界上下文之间使用RPC是有风险的方案,因为会承受网络风险,还意味着两个限界上下文之间存在紧耦合。如果系统A阻塞请求系统B,B又请求C,就很容易导致集成火车事故:火车里某一节车厢有问题就会变成整列火车的问题。
最好的限界上下文映射关系采用事件的订阅,但是这要求领域专家在设计的时候就考虑不同领域之间通知的延迟对于业务的影响,以及如何消除影响。如果不采用DDD的方式,领域专家通常无法意识到领域之间的同步成本,技术人员也很容易一头撞进集成火车里。

使用子域处理遗留系统

我们代码不是在真空里运行,它们免不了会跟一些遗留系统打交道,这些遗留系统的边界并不清晰。因此我们会将遗留系统放到一个子域里,把它们的问题放到我们的设计之外。这一步做完后我们的图案与之前没有本质上的区别,无非是多了一点子域。

战术设计

核心关注上下文的实体建模,定义值对象,实体等,也就是对战略技术的基础上对类的具体的设计的讨论。

重点名词

实体

描述了领域中唯一且可持续裱花的抽象模型。
白话解释就是具有唯一id跟值对象组成的类,比如订单对象,并且拥有实现业务逻辑的方法。

值对象

不关心唯一值,具有检验逻辑,等值判断逻辑,只关心值的类。
比如订单对象里的收货地址对象,这个地址包括了收货人的一些信息。

聚合

实体跟值对象会形成聚合,每个聚合一般是在一个事务中操作,一般都有持久化操作。比如买东西这个操作,有下单,支付等操作,买东西就相当于聚合。

领域服务

领域服务是用来协调领域对象完成某个操作,用来处理业务逻辑的,它本身是一个行为,所以是无状态的。状态由领域对象(具有状态和行为)保存。
上面也说了,领域对象是具有状态和行为的。那就是说我们也可以在实体或值对象来处理业务逻辑。那我们该如何取舍呢?
一般来说,在下面的几种情况下,我们可以使用领域服务:

  • 执行一个显著的业务操作过程
  • 对领域对象进行转换
  • 以多个领域对象为输入,返回一个值对象。

在使用领域服务时,我们又要避免过度使用,因为这样会导致又回到了传统的三层架构的老问题。

领域事件

领域事件可以理解为是发生在一个特定领域中的事件,是你希望在同一个领域中其他部分知道并产生后续动作的事件。但是并不是所有发生过的事情都可以成为领域事件。一个领域事件必须对业务有价值,有助于形成完整的业务闭环,也即一个领域事件将导致进一步的业务操作。
比如上面买东西后“订单支付成功”就是一个领域事件。这时候其他聚合(比如发短信给用户)就可以通过订阅这个领域事件来达到对应关联操作。

聚合设计

一个限界上下文里通常有多个聚合,聚合逻辑上是相对独立的。怎么理解聚合的概念呢?
在DDD实践中,聚合是事务的边界,并且聚合之间并不保证事务,只能用最终一致性。任何需要事务保护的逻辑都应该在一个聚合内。
在限界上下文里,将其他聚合能力整合在一起对外提供能力的聚合,被称为聚合根;其他聚合也被称为实体。

由于聚合是事务的边界,那么每个聚合在设计阶段,最重要的是找到业务的不变性,也就是说,在事务提交前后,数据的约束条件。比如说,你在知乎对一条回答点赞,那么这条回答的点赞数量必须立刻多1,那么点赞的动作和点赞的计数,就应当在一个聚合内。

在聚合被设计出来以后,我们的模型图看起来会是这样的:

image.png

领域事件设计

我们说聚合之间要采用最终一致性,而通常的做法是采用领域事件实现最终一致性。领域事件的名称应该采用通用语言命名,才能符合领域专家的心智。完整的时间名词应该是名词和动词构成的,动词应该是过去时。领域事件的名字和属性应该能够完整描述这个事件的含义。
事件里通常至少包含业务动作和其业务参数,也可以增加更多的下游关注的事件信息,避免下游为了完成处理还需查询。
对于领域事件,我们可以这样理解:
通过将领域中所发生的活动建模成一系列的离散事件,并将每个事件都用领域对象来表示,来跟踪领域中发生的事情。也可以简要理解为:领域事件 = 事件发布 + 事件存储 + 事件分发 + 事件处理。

事件风暴

事件风暴(Event Storming)于2013年首次被提出,2015年被ThoughtWorks技术雷达添加到“实验”阶段,2018年被ThoughtWorks技术雷达添加到“采纳”阶段。
事件风暴是一种快速探索复杂业务领域和对领域建模的实践。
事件风暴从领域中关注的业务事件出发,在此过程中团队经过充分讨论,统一语言,最后找到领域模型。
那到底什么是领域中关注的业务事件呢?
以宠物为例,如果做为宠物主人,你的问题域是如何养好一只猫,那么是不是已经打了疫苗,给宠物饲喂食物等将成为你关注的事情,领域事件会有:疫苗已注射,猫粮已饲喂等。
如果你是宠物医生,问题域是如何治好宠物的病,关注的事情是宠物的身体构成,准确的诊断宠物病情,对症下药,领域事件会有:病情已确诊,药方已开治。虽说二者关注的都是宠物,在不同的问题域下领域事件是不同的。
如果在通用语言中存在“当a发生时,我们就需要做到b。”这样的描述,则表明a可以定义成一个领域事件。领域事件的命名一般也就是“产生事件的对象名称+完成的动作的过去式”的形式,比如:订单已发货(OrderDispatchedEvent)、订单已收货和订单已确认(OrderConfirmedEvent)等事件。

领域事件可以是业务流程的一个步骤,例如订单提交,客户付费100元,订单完工等。领域事件也可以是定时发生的事情,例如每晚对账完成。或者是一个事件发生后引发的后续动作,比如确认收货7天后自动将钱打到卖家账户,比如客户输错密码三次后发生锁定账户。

事件风暴流程

物料准备

在事件风暴开始之前,需要准备以下物料:

  • 便利贴:一大堆,最少要四五种不同的颜色;
  • 记号笔:人手一支,用于在便利贴上写写写;
  • 白板:最好足够长,用来贴便利贴;
  • 开放空间:用于小组成员之间的充分讨论。

    参与人员
    组织者:组织者应当熟悉事件风暴的整个流程,能够组织大家顺利完成事件风暴;

领域专家:领域专家应该是精通业务的人,在事件风暴过程中,要负责澄清一些业务上的概念,思考业务上有没有遗漏的事件;

项目成员:负责开发这个项目的成员,所有角色都可参加,比如BA、QA、UX、DEV。因为事件风暴可以快速让整个团队了解整个项目的业务流程。

寻找领域事件

工作坊由寻找领域事件开始。领域事件一般用橘色的便利贴表示,书写领域实践的规则是使用被动语态,并按照时间顺序贴在白纸上。
最开始可能很多成员都不知道该怎么写,或者不知道该怎么寻找领域事件。可以由组织者写下领域中发生的第一个事件。其它参与者会迅速的开始模仿,这时我们可以让大家快速的进入状态。
在遇到有疑惑的事件时,不必长时间阻塞在那里讨论,把它作为标记记下来即可,后续再进行重点优化。可以贴一个比较醒目的便签纸(比如紫色)在事件旁边。
随着我们对业务认识的不断加深,可以随时回顾和总结之前添加的内容,对于有问题的描述进行更正,对于表述不清楚的内容可以进行重写。
事件是有相对顺序的。可以把一系列有相对顺序关系的事件放在一行上,从左到右排好。这样有助于梳理领域事件,查看是否有遗漏。

寻找命令和角色

在收集完领域事件后,我们可以在此基础上进一步探索系统核心事件的运行机制。这里我们在之前的领域事件的基础上加入指令和角色的概念。

指令代表系统中用户的意图、动作和决定,一般用蓝色的便利贴表示;角色表一类特定用户,一般用黄色便利贴表示。它们之间的关系是“角色”发送“指令”产生了“领域事件”(指令也可由外部系统触发,外部系统通常用粉色的便利贴表示)。
通常来说,一个命令将对应到我们后续应用程序开发的一个API。
在寻找命令和角色的过程中,你可能会遇到某些命令会在“特定的条件下”触发。比如:“当用户通过新的设备登入时,系统会发送提醒通知”。通常,我们将这种系统的行为逻辑称为策略,通常记录在紫丁香色的便利贴上,放在命令旁边。
寻找领域模型和聚合
当我们做完了上一个环节,就可以开始寻找系统中的领域模型和聚合了。我们把跟一个概念相同的指令和事件集合到一起,并用黄色的较大的便利贴表示领域模型。

把跟这个领域模型相关的命令放到左边,事件放到右边。需要注意的是,这个时候会去掉“事件的相对顺序”这个概念,因为我们已经不需要了。
可能有些领域模型不能作为一个独立存在的对象。它应该被另一个领域模型持有和使用。那这时候,可以考虑把两个模型合起来,形成一个聚合。在最上面的模型就是这个聚合的聚合根,其之下的模型都是它的实体或值对象。

划分子域和限界上下文

找到领域模型以后,我们应当就可以比较轻松地划分子域和限界上下文了。

在划分限界上下文的时候也可以反过来检验领域模型和通用语言的正确性。如果发现一个模型有歧义,那它就应该是限界上下文边界的地方,我们应该重新思考这个模型,必要时进行拆分。

常见的问题

在实施事件风暴的过程中会遇到一些问题,这里列举一些常见的问题及解决方案。

事件的粒度

我们在讨论这个问题之前,首先要思考事件是什么。事件是领域专家关心的业务事件。所以它不能比领域专家关心的业务更细,因为那将毫无意义。

举个例子,如果我们关心的是一个人一天的作息,那我们可能关心的是用户已起床,用户已吃早餐,用户已上班。但我们不会关心到更细节,比如:用户已睁眼,用户已洗漱,用户已出门,用户已上地铁……

同时,事件粒度也不能太粗,因为太粗粒度的事件不利于寻找领域模型。比如我们在平台上发一篇文章的业务。如果你只写一个“文章已发布”,那就可能会丢失掉一些比较重要的业务流程。

尝试改成:文章已保存,文章已申请审核,文章已通过审核,文章已审核失败,文章已对外发表,文章已加入分类,文章已推荐……你会发现,中间多了一个审核的过程,如果不找到这些命令,就很有可能遗漏掉“文章审核单”之类的模型。

对某个事件有歧义

这是好事情,说明你们团队需要讨论了,有时还可以发掘出原本可能没有注意到的业务细节。但在实施事件风暴的时候,不必刚开始就花太多的时间在上面,阻塞了后面的事件发掘。而是应该先前面说的那样,用一个醒目的标记记下来,后面再回过头来充分讨论。

或许最开始有歧义的地方,在事件逐渐完善,领域模型定义出来后,就没有歧义了。

一个命令产生多个连锁事件

这个是正常的,一个命令可能会触发一个事件或者多个事件。也有可能一个事件触发了另一个事件,只需要把它们贴在一起即可。

领域模型周围的事件过多

这个时候你们应该警惕了。一个领域模型不应该包含过多的领域事件,因为这会让这个模型变得很大,很复杂。你们需要考虑把这个领域模型拆分开了。

仔细思考一下,这个领域模型是不是可以拆成两个?一些下面的实体是不是可以拿出来单独作为一个聚合根?它们中的一些事件表述是不是有歧义?可不可以拆开来划分到两个限界上下文中?

比如“用户”在权限上下文中我们关注的是它的角色和权限,它是否登录成功,它的密码等等。

而在商品上下文中,我们关注的是它的姓名,电话,地址等等。

这种情况,是应该把它们拆开的。

感觉命令就是事件的动词?

很多时候其实就是这样的。比如角色是用户,命令是发布,产生了事件文章已发布。但也不完全是这样,因为在这个过程中可以统一语言。比如:用户,喝水,产生的事件可以是用户已补充水分,而不是用户已喝水。

也有可能会有一些定时任务或者策略,这都有利于我们熟悉业务。更何况,找到命令可以指导我们后续的API开发,所以寻找命令是有必要的。

成员完全不熟悉业务怎么办?

可以由领域专家先进行业务大概流程的讲解。如果有UX已经设计好的图就更好了。大家可以在这个环节发出自己的疑问,澄清一些关键信息。

领域专家也可以把主要的业务流程写下来,打印到纸上或者反映到大屏幕上。比如:

产品运营人员可以添加新的商品,编辑产品库存,并发布到京西商城,用户可以进行购买;当商品销售价格和库存数量发生变化后,产品运营人员会进行修改,并重新发布到商城。

没有领域专家怎么办?

团队总得是有人了解业务的。比如BA(有些团队可能是PM、TL等)。如果实在没有,可以让领域专家写一份上面那种主要的业务流程,大家按照这个业务流程来做。

但还是最好有一个领域专家,因为出现分歧的时候是很需要沟通达成一致的,如果没有领域专家在,团队有可能得到一些不准确的模型和语言。