架构演化--将MVC代码重构成DDD

什么是好的代码架构

在当前的工作中我们所面临的主要矛盾是“越来越多的多场景化复杂业务需求与有限的研发人力之间的矛盾”。而要解决这一矛盾,就要求我们的系统能做到:设计易拓展、代码易复用、逻辑易传承、运行更稳定

  • 设计易拓展
    一个好的架构应该能够实现业务与技术组件的分离,使设计者能够专注于业务流程,以填空的方式直接套用开箱即用的组件、框架和解决方案,不必进行大量的重复设计;另外好的架构也能够引导设计者完成最小子问题的正交分解(将一个复杂的问题或系统分解成多个相互独立(即正交)的部分过程),将设计者从错综复杂的上层业务逻辑中拯救出来,逐个击破,降低需求的复杂度和理解成本。
  • 代码易复用
    一个好的架构应该有良好的分层,强调正交子模块的拆分与封装,同层原子模块之间避免互相依赖和耦合,让上层系统能够轻松实现底层业务逻辑的组合复用,以O(n1+n2+…+nm)的实现复杂度支撑O(n1∗n2∗…∗nm)业务复杂度;另外我们的业务逻辑是建立在数据之上的,一个封装良好的代码架构在实现业务逻辑复用的同时应该有健全的数据模型维护和共享机制,避免同一个数据对象的重复查询,并能够轻松通过批量操作降低系统的I/O负载。
  • 逻辑易传承
    用文档来建立知识库是一个麻烦并且耗费时间的事情,尤其是小公司可能连知识库都没有。所以当我们接手上一个人的代码的时候,将会无比的痛苦。我们意识到业务功能都是由我们的代码承接的,它天然具备业务知识库的功能。因此一个好的代码架构不仅能够实现业务功能,而且要承担起传递业务知识的职责:当有新同事加入时,代码能够以最直接的方式帮助他快速建立起对整个业务的宏观认知,而进行具体的需求开发时,可以快速定位改动点并深入了解其业务细节。
  • 运行更稳定
    一个好的代码架构在面对多场景化的需求时,可以做到场景隔离,避免不同场景的特有逻辑之间互相耦合干扰,出现一个场景需求上线后影响其他业务场景的问题;除此之外,一个好的架构应该通过设计良好的框架和标准模板在有益的程度上对开发者的编码行为进行约束,把规范框架化,而不是过多的依赖人治和code review来实现规范统一。

架构演进之路

第一代:没有架构的架构
最开始的时候我们的架构如下图所示,这也是我们目前最常见一种代码架构。可以看出它的特点就是“简单”,没有过多的封装和设计,平铺直叙,数据查询和业务逻辑处理互相交织,是面向数据库编程的典型案例。这种架构在早期场景单一、需求简单的阶段可以快速实现功能,没有多余的设计成本,但是随着业务的发展,系统服务的场景越来越多,这套架构就变得越来越不简单了。
在这里插入图片描述

这种架构主要存在两个问题:
1.性能低:.由于大家习惯“打补丁”式的开发,来了一个业务需求就在现有的流程中增加一个if…else分支,然后直接在新分支内实现业务逻辑。当业务流程积攒的足够冗长时,就很容易忽视前置流程已经查询好的数据对象,造成数据重复查询。同时为了实现逻辑的复用,我们开始把一些常用逻辑封装为单独的方法,然后在上层业务流程中直接调用,然而我们在封装底层方法往往会把数据的获取逻辑封装下来,这进一步加剧了数据重复查询的问题,在有循环调用的场景中这个问题会更加突出。另外这种逻辑的复用方式还会造成数据库访问碎片化,我们很难利用批量操作的优势优化系统性能。很有可能会导致系统的性能降低
2.不易阅读:除了性能问题之外,由于不同业务场景的逻辑互相交织,代码分支判断逻辑缺少统一的规划,if分支层层嵌套,导致我们的代码逻辑圈复杂度不断飙升,本来应该通用的逻辑对不同场景的适配性越来越低。渐渐地,我们发现新增需求的开发越来越“不简单”了:在试图复用一段看起来相似的代码逻辑时会有很多纠结和不尽人意的地方,对代码执行流程的认知也不似以往那么清晰了,为了防止对旧的业务流程造成影响,我们开始增加更多的if分支,这反过来进一步加剧了情况的恶化,于是我们的代码中充斥着重复代码、多达5、6层的嵌套…

第二代:略有改善的上下文机制
上下文主要是为了解决数据重复查询问题引入的,思路特别朴素,就是把一个完整业务流程中要用到的全部数据提前在方法一开始就查询好,并做好校验。查询出来的数据对象保存到一个上下文对象中,这个上下文对象会贯穿整个业务流程,业务逻辑中需要用得到底层数据实体的时候统一从上下文对象中获取。
通过上下文的引入,我们基本上解决了数据重复查询的问题,另外我们数据的提前集中查询也有助于启发我们主动通过数据库批量查询进一步提升系统性能。而且上下文构造的过程其实也是数据校验的过程,通过上下文的提前构建,我们在一定程度上实现了预校验的逻辑,从而可以提前发现异常数据,避免写入脏数据和不必要的数据回滚操作。
在这里插入图片描述

这个架构其实也没有什么太大的改变,只是避免数据重复查询的问题;
同时也引入了一些新的痛点,首先就是我们的数据模型中数据对象往往比较多且关系复杂,这导致我们的上下文构造逻辑十分冗长。而且同一个业务域内不同的接口使用的上下文对象中属性有较大重叠,但是也有各自的差异,因此这些上下文对象的构造逻辑又开始出现大量的重复编码或者混乱的封装。比如询量单的新建接口与修改接口对应的上下文中80%的属性是相同的,这些属性的查询和关联逻辑造成了大量的重复编码。除了重复编码问题之外,上下文机制也并没有从根本上解决多场景下业务流程差异复杂度高的问题。

第三代:数据模型与业务模型的分离
上面架构的最大的痛点是我们的业务一般是多场景的,动态的。但是我们的数据模型却是静态的,数据模型的多场景化程度远小于业务规则的多场景化程度,即:同一个功能模块在不同场景下的业务规则存在差异,但却始终在操作同一套数据模型。
说明:其实我们所说的数据模型指的是实体及实体之间的关系, 不论某个产品线或计划类型是否会去设置某个子属性的值,只要我们的数据模型完成了定义,那么在任何场景下数据模型的中实体的定义及实体之间的关系都是不变的,实体只要定义出来,它会一直在那,只是某些场景下其属性值为null而已。而实体属性值的设置逻辑则是典型的业务模型的范畴。

在明确了“多场景化的动态业务模型是建立在一个相对静态的数据模型之上”这一本质之后,为了解决上下文对象构造复杂度高及重复编码的问题,我们需要做的就是数据模型的分离和下沉,为此引入了领域驱动设计思想中的“聚合”概念。它的含义可以通俗的理解为:一组关联密切且关系明确的实体或值对象的集合,一个聚合通常会支撑着一个功能极其内聚的上层业务模块。一个聚合中会定义唯一个聚合根对象,聚合根是整个聚合中实体操作的中心,聚合中的全部实体都可以通过聚合根直接或间接的访问到。聚合根通常并不难确定,比如计划聚合的聚合根自然就是Campaign实体,我们可以直接通过Campaign聚合根对象直接引用到计划下的预算、投放时段等子实体信息。

将聚合的概念落地到代码架构中我们需要做以下升级:

  1. 根据业务流程设计合理的数据模型,需要注意的是数据模型中的实体并不一定要与底层的库表一一对应,而是应该从业务本质出发完成实体划分和定义,另外在模型中也需要体现实体之间的关联关系。
  2. 在业务流程和底层数据库之间增加一个聚合层,在这一层中将第一步设计的数据模型定义为Java对象,其中实体之间的关系则转化为类与属性的关系。比如AdGroup领域对象内属性除了体现ad_group表中定义的字段之外,也定义了单元下的人群、流量包、创意列表等子实体对应的属性。
  3. 上层的业务流程对聚合中实体的访问和修改都是通过聚合根实现的,而要想获取聚合根则必须通过聚合层暴露出来的Repository接口。

Repository层接口是完全面向数据模型定义的,几乎与业务无关,通常不会为某个特殊的业务场景定义专用的数据查询或写入方法,它定义的都是通用的数据访问接口,让上层业务以声明式的方法获取所需的聚合根对象(或集合)。Repository的将数据对象的查询和实体关系的组装逻辑屏蔽在其接口实现中,上层业务不需要再次执行聚合根下子实体对象的查询和关联逻辑。

在这里插入图片描述
从上图可以看出,由于上下文中的很多数据对象都被转移到了聚合中,之前繁琐的数据查询和关联逻辑被分离下沉到了Repository的实现中,业务模型中不同的服务接口可以直接复用Repository中沉淀的数据查询和组装逻辑,上下文构造得以极大的精简,重复编码问题也得到了根本性的解决,体现了我们架构目标中“代码易复用”的要求。

除了更加灵活和优雅的复用数据查询和组装逻辑之外,聚合的引入让我们实现了数据模型和业务模型的分离,聚合层几乎与业务流程无关,直接体现数据模型的完整全貌。当有新同学加入的时候,可以通过阅读聚合层代码获取最全、最准确的数据模型定义,不再需要从代码中四处搜集对象关联关系的蛛丝马迹,这体现了我们架构目标中“逻辑易传承”的要求。

构建聚合对象来接收业务数据:

  @Overridepublic void saveUserAwardRecord(UserAwardRecordEntity userAwardRecordEntity) {// 构建消息对象SendAwardMessageEvent.SendAwardMessage sendAwardMessage = new SendAwardMessageEvent.SendAwardMessage();sendAwardMessage.setUserId(userAwardRecordEntity.getUserId());sendAwardMessage.setOrderId(userAwardRecordEntity.getOrderId());sendAwardMessage.setAwardId(userAwardRecordEntity.getAwardId());sendAwardMessage.setAwardTitle(userAwardRecordEntity.getAwardTitle());sendAwardMessage.setAwardConfig(userAwardRecordEntity.getAwardConfig());BaseEvent.EventMessage<SendAwardMessageEvent.SendAwardMessage> sendAwardMessageEventMessage = sendAwardMessageEvent.buildEventMessage(sendAwardMessage);// 构建任务对象TaskEntity taskEntity = new TaskEntity();taskEntity.setUserId(userAwardRecordEntity.getUserId());taskEntity.setTopic(sendAwardMessageEvent.topic());taskEntity.setMessageId(sendAwardMessageEventMessage.getId());taskEntity.setMessage(sendAwardMessageEventMessage);taskEntity.setState(TaskStateVO.create);// 构建聚合对象UserAwardRecordAggregate userAwardRecordAggregate = UserAwardRecordAggregate.builder().taskEntity(taskEntity).userAwardRecordEntity(userAwardRecordEntity).build();// 存储聚合对象 - 一个事务下,用户的中奖记录awardRepository.saveUserAwardRecord(userAwardRecordAggregate);log.info("中奖记录保存完成 userId:{} orderId:{}", userAwardRecordEntity.getUserId(), userAwardRecordEntity.getOrderId());}

实现Repository接口,将聚合对象中的数据持久化

    @Overridepublic void saveUserAwardRecord(UserAwardRecordAggregate userAwardRecordAggregate) {UserAwardRecordEntity userAwardRecordEntity = userAwardRecordAggregate.getUserAwardRecordEntity();TaskEntity taskEntity = userAwardRecordAggregate.getTaskEntity();String userId = userAwardRecordEntity.getUserId();Long activityId = userAwardRecordEntity.getActivityId();Integer awardId = userAwardRecordEntity.getAwardId();UserAwardRecord userAwardRecord = new UserAwardRecord();userAwardRecord.setUserId(userAwardRecordEntity.getUserId());userAwardRecord.setActivityId(userAwardRecordEntity.getActivityId());userAwardRecord.setStrategyId(userAwardRecordEntity.getStrategyId());userAwardRecord.setOrderId(userAwardRecordEntity.getOrderId());userAwardRecord.setAwardId(userAwardRecordEntity.getAwardId());userAwardRecord.setAwardTitle(userAwardRecordEntity.getAwardTitle());userAwardRecord.setAwardTime(userAwardRecordEntity.getAwardTime());userAwardRecord.setAwardState(userAwardRecordEntity.getAwardState().getCode());Task task = new Task();task.setUserId(taskEntity.getUserId());task.setTopic(taskEntity.getTopic());task.setMessageId(taskEntity.getMessageId());task.setMessage(JSON.toJSONString(taskEntity.getMessage()));task.setState(taskEntity.getState().getCode());UserRaffleOrder userRaffleOrderReq = new UserRaffleOrder();userRaffleOrderReq.setUserId(userAwardRecordEntity.getUserId());userRaffleOrderReq.setOrderId(userAwardRecordEntity.getOrderId());try {dbRouter.doRouter(userId);transactionTemplate.execute(status -> {try {// 写入记录userAwardRecordDao.insert(userAwardRecord);// 写入任务taskDao.insert(task);// 更新抽奖单int count = userRaffleOrderDao.updateUserRaffleOrderStateUsed(userRaffleOrderReq);if (1 != count) {status.setRollbackOnly();log.error("写入中奖记录,用户抽奖单已使用过,不可重复抽奖 userId: {} activityId: {} awardId: {}", userId, activityId, awardId);throw new AppException(ResponseCode.ACTIVITY_ORDER_ERROR.getCode(), ResponseCode.ACTIVITY_ORDER_ERROR.getInfo());}return 1;} catch (DuplicateKeyException e) {status.setRollbackOnly();log.error("写入中奖记录,唯一索引冲突 userId: {} activityId: {} awardId: {}", userId, activityId, awardId, e);throw new AppException(ResponseCode.INDEX_DUP.getCode(), e);}});} finally {dbRouter.clear();}try {// 发送消息【在事务外执行,如果失败还有任务补偿】eventPublisher.publish(task.getTopic(), task.getMessage());// 更新数据库记录,task 任务表taskDao.updateTaskSendMessageCompleted(task);log.info("写入中奖记录,发送MQ消息完成 userId: {} orderId:{} topic: {}", userId, userAwardRecordEntity.getOrderId(), task.getTopic());} catch (Exception e) {log.error("写入中奖记录,发送MQ消息失败 userId: {} topic: {}", userId, task.getTopic());taskDao.updateTaskSendMessageFail(task);}}

这种方式将数据和业务分离开来,数据的持久化不再关心具体的业务,能更好的复用,也能避免长事务的情况,并且整体代码也更加的清晰了。

第四代:领域能力拆分与编排

通过引入聚合我们基本上解决了数据查询逻辑复用的问题,但是由于多平台、多维度和多场景化带来的业务复杂度的问题却依然存在。可以用组合复用原则来解决这个问题。典型的2B的平台,业务特点就是流程冗长复杂,一个业务流程通常由多个流程节点组成,比如单元新建流程,可以分为:基础信息设置、单元名称设置、投放周期设置、投放位置设置、定向设置、出价设置、关键词设置等多个节点组成。这些节点再叠加上不同产品线(展位、快车、触点)、站外不同媒体(头、腾、百、快、京X)、不同的投放平台(京准通、流量货币化、京易投)以及不同的站点(国内、泰国、印尼、出海)等多维度的业务场景,就使系统具备了
O(n1∗n2∗…∗nm)业务复杂度,其中nx为不同业务细分维度下的场景复杂度,而组合复用原则就是专门为解决这一问题而生的。
组合复用原则强调复杂问题的拆分,拆分出来的最小子问题可以互不干扰地进行独立的迭代。在此基础上,上层模块可以通过对最小子问题的组合编排实现一项完整的业务功能。由于最小子问题之间彼此正交,独立维护各个最小子问题的编码复杂度就可以降级为O(n1+n2+…+nm)。基于该思想,我们在新架构中引入了领域能力拆分与编排机制。
在这里插入图片描述

领域能力的识别与拆分
在新架构中我们会将一个完整的业务流程正交分解为多个“能力节点”。这里所说的“正交分解”是指拆分出来的各个子模块之间互不干扰,可以独立进行迭代。举个例子来说,在早期大家进行能力梳理的时候,有同学从单元新建流程中拆分出了“出价信息校验”和“出价设置”两个能力节点,这其实是不合理的。因为出价信息的校验和出价属性的设置并不正交,他们互相依赖,我们应该这两段逻辑合并到一起,抽象为一个“出价设置”节点。
能力节点主要定义了系统中各个原子模块的功能范围。一般来说,一个能力节点通常包含一个能力门面和0到多个能力实例。能力门面并不承接具体的业务逻辑,它的作用是对外暴露统一的调用入口及请求转发,具体的业务逻辑则由能力门面下的能力实例承接。比如出价设置节点下会按照出价类型划分为:手动出价、tCPA智能出价、MC智能出价、eCPC智能出价几个具体的领域能力实例,而在人群定向设置节点下则有京选店铺人群设置、乐高人群设置和自定义人群设置几个领域能力实例。

能力编排与请求路由
将整个系统划分为多个独立的能力节点之后,接下来就需要通过能力编排将这些能力节点串联到一起组装成一个完成的服务。所谓的能力编排就是将业务流程中所需要的原子模块对应的能力节点串联起来,定义好他们之间数据传递的方式和编排规则。需要注意的是,能力编排操作的是能力节点而不是能力实例,在处理服务请求时,每一个能力节点负责将请求路由到正确的领域能力实例中进行处理。之所以这样设计是因为我们的业务流程相对稳定,系统对外提供的服务流程中业务节点及节点间的执行顺序很少会发生变化,需求迭代往往是对某个能力节点进行横向的拓展,也就是对具体的领域能力实例进行增删或者修改。通过能力节点的抽象及路由机制的引入,我们将动态变化着的部分从相对稳定的业务流程中分离出去,从而保障核心流程的稳定性不被频繁变化着的需求所影响,这一点与我们当时做数据模型与业务模型分离的动机是一致的,本质上都是在隔离变化。

将MVC重构成DDD架构

众所周知,MVC 分层结构是一种贫血模型设计,它将”状态“和”行为“分离到不同的包结构中进行开发使用。domain 里写 po、vo、enum 对象,service 里写功能逻辑实现。也正因为 MVC 结构没有太多的约束,让前期的交付速度非常快。但随着系统工程的长期迭代,贫血对象开始被众多 serivice 交叉使用,而 service 服务也是相互调用。这样缺少一个上下文关系的开发方式,让长期迭代的 MVC 工程逐步腐化到严重腐化。

MVC 工程的腐化根本,就在于对象、服务、组件的交叉混乱使用,就是上面提到的“没有架构的架构”。时间越长,腐化的越严重。

在这里插入图片描述
参考小傅哥的这个图,DDD架构和MVC的区别是DDD将贫血模型改成充血模型(数据下沉,改造成聚合对象),业务的实现由服务编排实现(将业务的正交分解),可以明显的看出来DDD更加“干净”

如下是 DDD 架构所呈现出的一种四层架构分层

在这里插入图片描述

应用封装 - app:这是应用启动和配置的一层,如一些 aop 切面或者 config 配置,以及打包镜像都是在这一层处理。你可以把它理解为专门为了启动服务而存在的。
接口定义 - api:因为微服务中引用的 RPC 需要对外提供接口的描述信息,也就是调用方在使用的时候,需要引入 Jar 包,让调用方好能依赖接口的定义做代理。
领域封装 - trigger:触发器层,一般也被叫做 adapter 适配器层。用于提供接口实现、消息接收、任务执行等。所以对于这样的操作,这里把它叫做触发器层。
领域编排【可选】 - case:领域编排层,一般对于较大且复杂的的项目,为了更好的防腐和提供通用的服务,一般会添加 case/application 层,用于对 domain 领域的逻辑进行封装组合处理。但对于一些小项目来说,完全可以去掉这一层。少量一层对象转换,代码的维护成本会降低很多。
领域封装 - domain:领域模型服务,是一个非常重要的模块。无论怎么做DDD的分层架构,domain 都是肯定存在的。在一层中会有一个个细分的领域服务,在每个服务包中会有【模型、仓库、服务】这样3部分。
仓储服务 - infrastructure:基础层依赖于 domain 领域层,因为在 domain 层定义了仓储接口需要在基础层实现。这是依赖倒置的一种设计方式。所有的仓储、接口、事件消息,都可以通过依赖倒置的方式进行调用。
外部接口 - gateway:对于外部接口的调用,也可以从基础设施层分离一个专门的 gateway 网关层,来封装外部 RPC/HTTP 等类型接口的调用。
类型定义 - types:通用类型定义层,在我们的系统开发中,会有很多类型的定义,包括:基本的 Response、Constants 和枚举。它会被其他的层进行引用使用。(这一层没有画到图中)

工程重构(MVC->DDD)

经过实践验证,不需要太高成本,MVC 就可以天然的向 DDD 工程分层的模型结构转变。重点是不改变原有的工程模块的依赖关系,将贫血的 domain 对象层,设计为充血的结构。对于 domain 原本在 MVC 分层结构中,就是一个被依赖层,恰好可以与其他层做依赖倒置的设计方案处理。具体如图所示:
在这里插入图片描述
从 MVC 到 DDD 的映射,使用了相同颜色进行标注:

1.在 MVC 分层结构中,所有的逻辑都集中在 service 层,也是文中提到的腐化最严重的层,要治理的也是这一层。所以首先我们要将 service 里的功能进行拆解。

1.service 中具备领域特性的服务实现,抽离到原本贫血模型的 domain 中。在 domain 分层中添加 xxx、yyy、zzz 分层领域包,分别实现不同功能。注意每个分层领域包内都具备完整的 DDD 领域服务内所需的模块

2 .service 中的基础功能组件,如:缓存Redis、配置中心等,迁移到 dao 层。这里我们把 dao 层看做为基础设施层。它与 domain 领域层的调用关系,为依赖倒置。也就是 domain 层定义接口,dao 层依赖于 domain 定义的接口,做依赖倒置实现接口。

3 .service 本身最后被当做 application/case 层,来调用 domain 层做服务的编排处理。

因为恰好,MVC 分层结构中,也是 service 和 dao 依赖于 domain,这和 DDD 分层结构是一致的。所以经过这样的映射拆分代码实现调用结构后,并不会让工程结构发生变化。那么只要工程结构不发生变化,我们的改造成本就只剩下代码编写风格和旧代码迁移成本。

MVC 分层结构中的 export 层是 RPC 接口定义层,由 web 层实现。web 是对 service 的调用。也就是 DDD 分层结构中调用 application 编排好的服务。这部分无需改动。但如果你原有工程把 domain 也暴露出去了,则需要把对应的包迁移到 export 因为 domain 包有太多的核心对象和属性,还包括数据库持久化对象。这些都不应该被暴露。

MVC 分层中,因为有需要对外部 RPC 接口的调用,所以会单独有一层 RPC 来封装其他服务的接口。这一层被 domain 领域层使用,可以定义 adapter 适配器接口,通过依赖倒置,在 rpc 层实现 domain 层定义的调用接口。

此外 dao 层,在 MVC 结构中原本是比较单一的。但经过改造后会需要把基础的 Redis 使用、配置中使用,都迁移到 dao 层。因为原本在 service 层的话,domain 层是调用不到的这些基础服务的,而且也不符合服务功能边界的划分。

还有一点需要知道:从MVC到DDD的改造,只是目录分层更多了(领域分层),能让你把每个领域下的代码分开存放,但是怎么放也是有讲究的,只有按照设计模式来放,才能放的“整齐”,不然就只是把“屎山”代码给放到不同的地方而已
在这里插入图片描述

领域建模

在 DDD 架构分层中,domain 模块最重要的,也是最大的那个。所有的其他模块都要围着它转。
那如何来建立模型呢?
四色建模风暴事件)是整个 DDD 这套软件设计方法中用于工程拆分界限上下文的非常重要的实践手段。通过建模过程快速识别业务领域中的关键事件和核心流程,也是在这个过程中设计出领域对象的,为后面详细设计和代码开发做指导。

你可以把整个过程理解为,为工程开发提供面向对象设计,涵盖;领域拆分、界限串联、功能聚合。所以相比Service + 数据模型的贫血开发方式,DDD 前期需要付出更多的设计成本,但对于软件的长周期迭代,这样的好处是非常大的。

怎么建模
DDD 的建模过程,是以一个用户为起点,通过行为命令,发起行为动作,串联整个业务。而这个用户的起点最初来自于用例图的分析。用例图是用户与系统交互的最简表示形式,展现了用户和与他相关的用例之间的关系。通过用例图,我们可以分析出所有的行为动作。

在这里插入图片描述
此图是整个四色建模的指导图,通过寻找领域事件,发起事件命令,完成领域事件的过程,完成 DDD 工程建模。

蓝色 - 决策命令,是用户发起的行为动作,如;开始签到、开始抽奖、查看额度等。
黄色 - 领域事件,过去时态描述。如;签到完成、抽奖完成、奖品发放完成。它所阐述的都是这个领域要完成的终态。
粉色 - 外部系统,如你的系统需要调用外部的接口完成流程。
红色 - 业务流程,用于串联决策命令到领域事件,所实现的业务流程。一些简单的场景则直接有决策命令到领域事件就可以了。
绿色 - 只读模型,做一些读取数据的动作,没有写库的操作。
棕色 - 领域对象,每个决策命令的发起,都是含有一个对应的领域对象。

👩🏻‍🏫敲黑板 综上,左下角的示意图。是一个用户,通过一个策略命令,使用领域对象,通过业务流程,完成2个领域事件,调用1次外部接口个过程。我们在整个 DDD 建模过程中,就是在寻找这些节点。

一般步骤是:先根据需求建立用例图,然后花大量时间去挖掘领域事件,需要一起进行讨论,避免漏掉节点。确定了领域事件后,通过决策命令串联领域事件并且给领域事件补充所需要的领域对象。最后进行领域边界的划分

参考图:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/55212.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Ceph RocksDB 深度调优

介绍 调优 Ceph 可能是一项艰巨的挑战。在 Ceph、RocksDB 和 Linux 内核之间&#xff0c;实际上有数以千计的选项可以进行调整以提高存储性能和效率。由于涉及的复杂性&#xff0c;比较优的配置通常分散在博客文章或邮件列表中&#xff0c;但是往往都没有说明这些设置的实际作…

如果您忘记了 Apple ID 和密码,按照指南可重新进入您的设备

即使您的 iPhone 或 iPad 由于各种原因被锁定或禁用&#xff0c;也可以使用 iTunes、“查找我的”、Apple 支持和 iCloud 解锁您的设备。但是&#xff0c;此过程需要您的 Apple ID 和密码来验证所有权并移除激活锁。如果您忘记了 Apple ID 和密码&#xff0c;请按照我们的指南重…

G502 鼠标自定义(配合 karabiner)

朋友送了我一个 G502 多功能鼠标&#xff0c;除了鼠标正常的左键、右键和滑轮外&#xff0c;额外提供了 6 个按键&#xff0c;并且滑轮可以向左、向右、向下按下&#xff0c;共计 9 个自定义的按键。 虽然是 karabiner 的老用户&#xff0c;但一直在使用 TrackPad&#xff0c;所…

SpringGateway(网关)微服务

一.启动nacos 1.查看linux的nacos是否启动 docker ps2.查看是否安装了nacos 前面是你的版本&#xff0c;后面的names是你自己的&#xff0c;我们下面要启动的就是这里的名字。 docker ps -a3.启动nacos并查看是否启动成功 二.创建网关项目 1.创建idea的maven项目 2.向pom.x…

VMware 虚拟机 下载安装 Centos7 和Windows10 镜像源

准备工作 下载 VMware链接&#xff1a;稍后发布链接 Centos7完整版链接&#xff1a;https://www.123865.com/ps/EF7OTd-mdAnH Centos7mini版链接&#xff1a;https://www.123865.com/ps/EF7OTd-1dAnH Windows10链接&#xff1a;https://www.123865.com/ps/EF7OTd-4dAnH 演示环境…

【Git】一文看懂Git

Git 一、简介1. Git 与 SVN 区别1.1 Git 是分布式的&#xff0c;SVN 不是1.1.1 分布式版本控制系统Git1.1.2 集中式版本控制系统SVN 1.2 Git 把内容按元数据方式存储&#xff0c;而 SVN 是按文件1.3 Git 分支和 SVN 的分支不同1.4 Git 没有一个全局的版本号&#xff0c;而 SVN …

CS 工作笔记:SmartEdit 里创建的是 CMS Component

下图是在 SmartEdit 里创建的 cms Component&#xff0c;在 Back-Office 里的截图&#xff1a; SAP Commerce Cloud 的 CMS Component 是其内容管理系统 (CMS) 的核心组成部分&#xff0c;它提供了对在线商店或平台内容的灵活管理。通过这些组件&#xff0c;用户能够在不涉及复…

C# 字符串(String)的应用说明一

一.字符串&#xff08;String&#xff09;的应用说明&#xff1a; 在 C# 中&#xff0c;更常见的做法是使用 string 关键字来声明一个字符串变量&#xff0c;也可以使用字符数组来表示字符串。string 关键字是 System.String 类的别名。 二.创建 String 对象的方法说明&#x…

Spark SQL分析层优化

导读&#xff1a;本期是《深入浅出Apache Spark》系列分享的第四期分享&#xff0c;第一期分享了Spark core的概念、原理和架构&#xff0c;第二期分享了Spark SQL的概念和原理&#xff0c;第三期则为Spark SQL解析层的原理和优化案例。本次分享内容主要是Spark SQL分析层的原理…

亚马逊 Bedrock 平台也能使用Llama 3.2 模型了

亚马逊 Bedrock 平台推出 Llama 3.2 模型&#xff1a;多模态视觉和轻量级模型 概述 由 Meta 提供的最新 Llama 3.2 模型现已在 Amazon Bedrock 平台上推出。这一新模型系列标志着 Meta 在大型语言模型&#xff08;LLM&#xff09;领域的最新进展&#xff0c;它在多种应用场景…

本地访问autodl的jupyter notebook

建立环境并安装jupyter conda create --name medkg python3.10 source activate medkg pip install jupyter 安装完成后&#xff0c;输入jupyter notebook --generate-config 输入ipython,进入python In [2]: from jupyter_server.auth import passwd In [3]: passwd(algori…

Spring Data(学习笔记)

JPQL语句&#xff1f;&#xff1f;&#xff1f;&#xff08;Query括号中的就是JPQL语句&#xff09; 怎么又会涉及到连表查询呢&#xff1f; 用注解来实现表间关系。 分页是什么&#xff1f;为什么什么都有分页呢 &#xff1f; 继承&#xff0c;与重写方法的问题 Deque是什么 ?…

【JavaSE】反射、枚举、lambda表达式

目录 反射反射相关类获取类中属性相关方法常用获得类相关的方法示例常用获得类中属性相关的方法示例获得类中注解相关的方法 反射优缺点 枚举常用方法优缺点 枚举与反射lambda表达式语法函数式接口简化规则使用示例变量捕获集合中的应用优缺点 反射 Java的反射&#xff08;refl…

通信协议感悟

本文结合个人所学&#xff0c;简要讲述SPI&#xff0c;I2C&#xff0c;UART通信的特点&#xff0c;限制。 1.同步通信 UART&#xff0c;SPI&#xff0c;I2C三种串行通讯方式&#xff0c;SPI功能引脚为CS&#xff0c;CLK&#xff0c;MOSI&#xff0c;MISO&#xff1b;I2C功能引…

若依从redis中获取用户列表

因为若依放入用户的时候&#xff0c;会在减值中添加随机串&#xff0c;所以用户的key会在redis中变成&#xff1a; login_tokens:6af07052-b76d-44dd-a296-1335af03b2a6 这样的样子。 如果用 Set<Object> items redisService.redisTemplate.keys("login_tokens&…

dcatadmin 自定义登录页面

一、问题&#xff1a; 在后台管理系统中&#xff0c;不同的项目想要不同的登录页面&#xff0c;但是框架自带的登录页面就只有一个。 解决&#xff1a; 由芒果系统改造的dcatadmin登录插件&#xff0c;实现一键安装改变登录页面。 项目介绍 基于Laravel和Vue的快速开发的后台管…

YOLO11改进 | 检测头 | 小目标遮挡物性能提升的检测头Detect_MultiSEAM【完整代码】

秋招面试专栏推荐 &#xff1a;深度学习算法工程师面试问题总结【百面算法工程师】——点击即可跳转 &#x1f4a1;&#x1f4a1;&#x1f4a1;本专栏所有程序均经过测试&#xff0c;可成功执行&#x1f4a1;&#x1f4a1;&#x1f4a1; 基于深度学习的人脸检测算法取得了巨大进…

PID控制原理:看下这三个故事,你就明白了

一、PID的故事 小明接到这样一个任务&#xff1a;有一个水缸点漏水(而且漏水的速度还不一定固定不变)&#xff0c;要求水面高度维持在某个位置&#xff0c;一旦发现水面高度低于要求位置&#xff0c;就要往水缸里加水。 小明接到任务后就一直守在水缸旁边&#xff0c;时间长就觉…

遇到慢SQL、SQL报错,应如何快速定位问题 | OceanBase优化实践

在数据库的使用中&#xff0c;大家时常会遇到慢SQL&#xff0c;或执行出错的SQL。对于某些SQL问题&#xff0c;其错误原因显而易见&#xff0c;但也有不少情况难以直观判断。面对这类问题&#xff0c;我们应当如何应对&#xff1f;如何准确识别SQL错误的根源&#xff1f;是否需…

嵌入向量生成与查询

嵌入向量生成与查询 文本嵌入模型 M3E 是 Moka Massive Mixed Embedding 的缩写 Moka&#xff0c;此模型由 MokaAI 训练&#xff0c;开源和评测&#xff0c;训练脚本使用 uniem&#xff0c;评测 BenchMark 使用 MTEB-zh Massive&#xff0c;此模型通过千万级 (2200w) 的中文句…