一、现状
1、 业务深度耦合,集成无处不在
2、市场环境和运营政策多变,应用随需应变
二、复杂和规模增长的解决之道
三、软件架构模式的演进
四、领域建模
微服务设计过程中往往会面临边界如何划定的问题,项目团队为微服务到底应该拆多小而争得面红耳赤。不同的人会根据自己对微服务的理解而拆分出不同的微服务,于是大家各执一词,谁也说服不了谁,都觉得自己很有道理。
那在实际落地过程中,不少项目在面临这种微服务设计困惑时,是靠拍脑袋硬完成的,上线后运维的压力就可想而知了。那是否有合适的理论或设计方法来指导微服务设计呢?
没错,就是DDD。那么今天我们就详细讲解下:“微服务设计为什么要选择领域驱动设计?”。
综合来看,我认为微服务拆分困境产生的根本原因就是不知道业务或者微服务的边界到底在什么地方。换句话说,确定了业务边界和应用边界,这个困境也就迎刃而解了。那如何确定,是否有相关理论或知识体系支持呢?在回答这些问题之前,我们先来了解一下领域驱动设计与微服务的前世今生。
2004年埃里克·埃文斯(Eric Evans)发表了《领域驱动设计》(Domain-Driven Design –Tackling Complexity in the Heart of Software)这本书,从此领域驱动设计(Domain Driven Design,简称DDD)诞生。DDD核心思想是通过领域驱动设计方法定义领域模型,从而确定业务和应用边界,保证业务模型与代码模型的一致性。
五、DDD分层架构
不同分层之间的调用方向
五、从事件风暴到代码落地
六、领域驱动设计介绍
限界上下文:
我们知道语言都有它的语义环境,同样,通用语言也有它的上下文环境。为了避免同样的概念或语义在不同的上下文环境中产生歧义,DDD在战略设计上提出了“限界上下文”这个概念,用来确定语义所在的领域边界
聚合
它是聚合软件包的根目录,可以根据实际项目的聚合名称命名,比如主播聚合。把实体和值对象聚集到聚合当中,并定义聚合的边界聚合内实现高内聚的业务逻辑,它的代码可以独立拆分为微服务
聚合根
负责对本模块内的所有的实体类方法进行聚合,作为一个整体对外暴露。
实体
在代码模型中,实体的表现形式是实体类,这个类包含了实体的属性和方法,通过这些方法实现实体自身的业务逻辑。在DDD里,这些实体类通常采用充血模型,与这个实体相关的所有业务逻辑都在实体类的方法中实现,跨多个实体的领域逻辑则在领域服务中实现。
值对象
通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体。在DDD中用来描述领域的特定方面,并且是一个没有标识符的对象,叫作值对象。
七、微服务代码模型
其实,DDD并没有给出标准的代码模型,不同的人可能会有不同理解。下面要说的这个微服务代码模型是经过查询网上大量资料后建立起来的,主要考虑的是微服务的边界、分层以及架构演进
微服务一级目录结构
微服务一级目录是按照DDD分层架构的分层职责来定义的。从下面这张图中,我们可以看到,在代码模型里分别为用户接口层、应用层、领域层和基础层,建立了 interfaces、application、domain 和 infrastructure 四个一级代码目录
这些目录的职能和代码形态是这样的。
Interfaces(用户接口层):它主要存放用户接口层与前端交互、展现数据相关的代码。前端应用通过这一层的接口,向应用服务获取展现所需的数据。这一层主要用来处理用户发送的Restful请求,解析用户输入的配置文件,并将数据传递给Application层。数据的组装、数据传输格式以及Facade接口等代码都会放在这一层目录里。
Application(应用层):它主要存放应用层服务组合和编排相关的代码。应用服务向下基于微服务内的领域服务或外部微服务的应用服务完成服务的编排和组合,向上为用户接口层提供各种应用数据展现支持服务。应用服务和事件等代码会放在这一层目录里。
Domain(领域层):它主要存放领域层核心业务逻辑相关的代码。领域层可以包含多个聚合代码包,它们共同实现领域模型的核心业务逻辑。聚合以及聚合内的实体、方法、领域服务和事件等代码会放在这一层目录里。
Infrastructure(基础层):它主要存放基础资源服务相关的代码,为其它各层提供的通用技术能力、三方软件包、数据库服务、配置和基础资源服务的代码都会放在这一层目录里。
各层目录结构
1. 用户接口层
Interfaces 的代码目录结构, controller提供较粗粒度的调用接口,将用户请求委派给一个或多个应用服务进行处理 。
2. 应用层
Application 的代码目录结构有:event 和 service。
Event(事件):这层目录主要存放事件相关的代码。它包括两个子目录:publish 和 subscribe。前者主要存放事件发布相关代码,后者主要存放事件订阅相关代码(事件处理相关的核心业务逻辑在领域层实现)。
这里提示一下:虽然应用层和领域层都可以进行事件的发布和处理,但为了实现事件的统一管理,我建议你将微服务内所有事件的发布和订阅的处理都统一放到应用层,事件相关的核心业务逻辑实现放在领域层。通过应用层调用领域层服务,来实现完整的事件发布和订阅处理流程。
Service(应用服务):这层的服务是应用服务。应用服务会对多个领域服务或外部应用服务进行封装、编排和组合,对外提供粗粒度的服务。应用服务主要实现服务组合和编排,是一段独立的业务逻辑。你可以将所有应用服务放在一个应用服务类里,也可以把一个应用服务设计为一个应用服务类,以防应用服务类代码量过大。
3. 领域层
Domain 是由一个或多个聚合包构成,共同实现领域模型的核心业务逻辑。聚合内的代码模型是标准和统一的,包括:entity、event、repository 和 service 四个子目录。
而领域层聚合内部的代码目录结构是这样的。
Aggregate(聚合):它是聚合软件包的根目录,可以根据实际项目的聚合名称命名,比如权限聚合。在聚合内定义聚合根、实体和值对象以及领域服务之间的关系和边界。聚合内实现高内聚的业务逻辑,它的代码可以独立拆分为微服务。
以聚合为单位的代码放在一个包里的主要目的是为了业务内聚,而更大的目的是为了以后微服务之间聚合的重组。聚合之间清晰的代码边界,可以让你轻松地实现以聚合为单位的微服务重组,在微服务架构演进中有着很重要的作用。
Entity(实体):它存放聚合根、实体、值对象以及工厂模式(Factory)相关代码。实体类采用充血模型,同一实体相关的业务逻辑都在 实体类代码中实现。跨实体的业务逻辑代码在领域服务中实现。
聚合根:职责:聚合之间通过聚合根关联引用,如果需要访问其他聚合的实体,先访问聚合根,再导航到聚合内部的实体;即外部对象不能直接访问聚合内的实体
解决的问题: 复杂数据模型缺少统一的业务规则控制而导致的聚合,实体之间数据不一致的问题;
实体:在分层架构里,实体采用充血模型,在实体类内实现实体的全部业务逻辑。这些不同的实体都有自己的方法和业务行为;
值对象:根据需要将某些实体的某些属性或属性集设计为值对象。
Event(事件):它存放事件实体以及与事件活动相关的业务逻辑代码。
Service(领域服务):它存放领域服务代码。一个领域服务是多个实体组合出来的一段业务逻辑。你可以将聚合内所有领域服务都放在一个领域服务类中,你也可以把每一个领域服务设计为一个类。如果领域服务内的业务逻辑相对复杂,我建议你将一个领域服务设计为一个领域服务类,避免由于所有领域服务代码都放在一个领域服务类中,而出现代码臃肿的问题。领域服务封装多个实体或方法后向上层提供应用服务调用。
Repository(仓储):它存放所在聚合的查询或持久化领域对象的代码,通常包括仓储接口和仓储实现方法。为了方便聚合的拆分和组合,我们设定了一个原则:一个聚合对应一个仓储。
Repository 是一个独立的层,介于领域层与数据映射层(数据访问层)之间。它的存在让领域层感觉不到数据访问层的存在,它提供一个类似集合的接口提供给领域层进行领域对象的访问。Repository 是仓库管理员,领域层需要什么东西只需告诉仓库管理员,由仓库管理员把东西拿给它,并不需要知道东西实际放在哪
特别说明:按照DDD分层架构,仓储实现本应该属于基础层代码,但为了在微服务架构演进时,保证代码拆分和重组的便利性,我是把聚合仓储实现的代码放到了聚合包内。这样,如果需求或者设计发生变化导致聚合需要拆分或重组时,我们就可以将包括核心业务逻辑和仓储代码的聚合包整体迁移,轻松实现微服务架构演进。
4. 基础层
Infrastructure 的代码目录结构有:util 一个子目录(目前规划一个)。
Util:主要存放mq消息、第三方接口、文件、通用算法等基础代码,你可以为不同的资源类别建立不同的子目录。
代码模型总目录结构
在完成一级和二级代码模型设计后,你就可以看到下图这样的微服务代码模型的总目录结构了。
八、将领域对象映射到微服务代码模型中
1、公会业务流程
2、DDD领域建模
DDD强调先构建领域模型然后设计微服务,以保证领域模型和微服务的一体性,因此我们不能脱离领域模型来谈微服务的设计和落地。但在构建领域模型时,我们往往是站在业务视角的,并且有些领域对象还带着业务语言。我们还需要将领域模型作为微服务设计的输入,对领域对象进行设计和转换,让领域对象与代码对象建立映射关系。
领域对象的整理
我们第一个重要的工作就是,整理事件风暴过程中产生的各个领域对象,比如:聚合、实体、命令和领域事件等内容,将这些领域对象和业务行为记录到下面的表格中。你可以看到,这张表格里包含了:领域模型、聚合、领域对象和领域类型四个维度。一个领域模型会包含多个聚合,一个聚合包含多个领域对象,每个领域对象都有自己的领域类型。领域类型主要标识领域对象的属性,比如:聚合根、实体、命令和领域事件等类型。
微服务 | 层 | 聚合 | 领域对象 | 领域类型 | 依赖的领域对象 | 包名 | 类名 | 方法名 |
---|---|---|---|---|---|---|---|---|
公会 | 应用层 | / | 公会主播数据 | 应用服务 | 领域服务:公会主播每日数据 | /application/service/anchor_data/GuildAnchorDataService | GuildAnchorDataService | getGuildAnchorDailyIncome |
/ | 签约主播数据 | 应用服务 | 领域服务:签约主播每日数据 | /application/service/anchor_data/SigendAnchorDataService | SigendAnchorDataService | |||
/ | 应用服务 | |||||||
领域层 | 公会 | 公会 | 聚合根 | /domain/guildAggregate/entity/GuildRoot | GuildRoot | |||
公会新增 | 方法 | |||||||
公会修改 | 方法 | |||||||
公会解约 | 方法 | |||||||
公会删除 | 方法 | |||||||
公会主播列表查询和导出 | 方法 | |||||||
公会已创建 | 方法 | |||||||
合作公会信息 | 实体 | 被聚合根公会引用 | /domain/guildAggregate/entity/CooperationGuildEntity | CooperationGuildEntity | ||||
官方公会信息 | 实体 | 被聚合根公会引用 | /domain/guildAggregate/entity/OfficialGuildEntity | OfficialGuildEntity | ||||
公会状态 | 值对象 | 聚合根公会的值对象 | /domain/guildAggregate/entity/GuildRoot | GuildRoot | ||||
主播(签约主播、官方主播、合作公会主播) | 签约主播 | 聚合根 | ||||||
主播新增 | 方法 | |||||||
主播修改 | 方法 | |||||||
主播定级 | 方法 | |||||||
主播解约 | 方法 | |||||||
主播删除 | 方法 | |||||||
主播入会、主播退会、主播定级通知 | 领域事件 | |||||||
主播基本信息 | 实体 | |||||||
主播合同信息 | 实体 | |||||||
主播新增合同、修改合同 | 命令 | |||||||
公会结算 | 公会结算 | 聚合根 | ||||||
公会结算激励比例 | 方法 | |||||||
有效主播规则 | 方法 | |||||||
有效新增主播规则 | 方法 | |||||||
总流水规则 | 方法 | |||||||
流水增幅规则 | 方法 | |||||||
公会每月结算数据 | 实体 | |||||||
主播结算 | 主播每月结算 | 聚合根 | AnchorDataRoot | |||||
签约主播每月结算数据 | 实体 | |||||||
合作公会主播每月结算数据 | 实体 | |||||||
开播时长规则 | 方法 | |||||||
观看时长规则 | 方法 | |||||||
DAU规则 | 方法 | |||||||
底薪规则 | 方法 | |||||||
流水规则 | 方法 | |||||||
签约主播结算结果查询与编辑 | 方法 | |||||||
公会主播结算结果查询与编辑 | 方法 | |||||||
主播结算状态 | 值对象 | |||||||
公会审批 | ||||||||
签约主播审批 | 聚合根 | |||||||
公会主播审批 | 聚合根 | |||||||
公会虚拟金 | 聚合根 | |||||||
主播数据 | 主播所有数据 | 聚合根 | 被结算聚合依赖 | /domain/anchorDataAggregate/entity/AnchorDataRoot | AnchorDataRoot | |||
签约和官方主播每日流水收益数据统计 | 方法 | /domain/anchorDataAggregate/entity/AnchorDataRoot | AnchorDataRoot | writeDailyIncomeData | ||||
签约和官方主播每日直播时长数据统计 | 方法 | |||||||
签约和官方主播每日观看时长数据统计 | 方法 | |||||||
主播每月数据总计 | 方法 | |||||||
主播每日收益等综合数据 | 实体 | |||||||
主播每日观看时长数据(依赖数据平台8点数据) | 实体 | |||||||
主播身份每月镜像 | 实体 | |||||||
主播身份每日镜像 | 实体 | |||||||
主播数据类型 | 值对象 | 被主播数据聚合依赖 | /domain/anchorDataAggregate/entity/AnchorDataVo | AnchorDataVo |
九、总结
业务技术到底是在做业务,还是做技术?业务技术的技术性体现在哪里? 通过上面的案例,我们可以看到业务所面临的复杂性并不亚于底层技术,要想写好业务代码也不是一件容易的事情。业务技术和底层技术人员唯一的区别是他们所面临的问题域不一样。
业务技术面对的问题域变化更多、面对的人更加庞杂。而底层技术面对的问题域更加稳定、但对技术的要求更加深。。
但是,不管是业务技术还是底层技术人员,有一些思维和能力都是共通的。比如,分解问题的能力,抽象思维,结构化思维等等。。
做不好业务开发的,也做不好技术底层开发,反之亦然。业务开发一点都不简单,只是我们很多人把它做“简单”了。