关于ddd:快速理解DDD领域驱动设计架构思想基础篇-京东物流技术团队

1 前言本文与大家一起学习并介绍畛域驱动设计(Domain Drive Design) 简称DDD,以及为什么咱们须要畛域驱动设计,它有哪些优缺点,尽量用一些通俗易懂文字来形容解说畛域驱动设计,本篇并不会从深层大阐述解说落地实现,这些大家能够在理解入门后再去深层次学习探讨或在后续进阶和高级篇理解,心愿通过本文介绍,能够让大家疾速理解DDD并有一个根底的认知,DDD自身就是实践的汇合,很难在不积攒实践状况下来无效的施行DDD,仅仅看一些代码案例后就开搞,最终进去货色也是东施效颦,莫要好高骛远。 最初冀望大家在工作中能多思考,如你所负责我的项目如果用DDD如何设计、以及会面临哪些挑战。 学习理解DDD之前,冀望大家可在温顾下以往咱们所理解把握一些常识,致力让本人所学所把握的内容积淀下来,举荐浏览系列。 Head First 设计模式:根底面向对象概念和重要的设计模式;UML面向对象建模根底:从需要到剖析,从剖析到设计,从设计到编码,UML都有用武之地实现畛域驱动设计:很厚,更加求实,举荐浏览畛域驱动设计:张逸-DDD开山之作,挺玄幻的,多读几遍受益匪浅;2 定义与概念畛域驱动设计(DDD)提出是从零碎的剖析到软件建模的一套方法论。将业务概念和业务规定转换成软件系统中的概念和规定,从而升高或暗藏业务复杂性,使零碎具备更好的扩展性,以应答复杂多变的事实业务问题。总结它是一套残缺而零碎的设计办法、是一种设计思维、一种方法论,并不是"零碎架构",一种架构设计准则、思维。 2.1、为什么要应用"畛域驱动设计",或者说其用处,利用场景式什么?长于解决高简单业务的产品研发、可帮忙咱们提炼稳固的产品内核(畛域模型中称为外围域);通过建模可进步建模高内聚、升高模型间的耦合度,进步零碎的可扩展性与稳定性;强调团队与领域专家的单干沟通,有助于建设一个沟通良好的团队组织;对立设计思维与设计规范,有助于进步团队成员的架构设计能力和面向对象设计能力;现有的微服务建构都是遵循畛域驱动设计的架构准则;如果你负责的软件系统并不简单,那么,你的确不须要学习畛域驱动设计!2.2、畛域驱动设计跟时下风行的架构思维最大的区别是什么?畛域驱动设计的思维是:对象+行为+服务,所有的设计围围绕着对象、行为、服务开展; 时下风行架构设计思维是:基于MVC分层架构进行纵向扩大,分业务模块进行产品横向扩大; 2.2.1. 传统的计划三层利用架构:数据-利用(业务逻辑层)-展示,通常是以数据位为终点进行数据库剖析设计。 服务层过重,数据模型失血,没货色;面条式编程或者面向数据库编程,服务层围绕数据库作业实现业务逻辑,常常一条线撸到底;代码一整块一整块的过重,很难扩大复用;数据库模型只是数据库映射,没有相干的行为撑持,行为都被上一层Service给实现等了,因而是失血 的畛域模型;2.2.2. 畛域驱动计划架构四层在DDD分层构造中将三层中业务逻辑拆解为应用层和畛域层,外围业务逻辑体现下沉到畛域层去实现,以业务畛域模型为外围建模(面向对象建模),更能体现对事实世界的形象,其长处如下 轻服务层+充血的畛域模型;畛域模型封装和实现各自应有的行为,能够认为是一个高内聚、低耦合的组件;因为模型集数据与行为于一身,是一种自解释的对象,代码复用性高,业务逻辑清晰明确;用户界面层:主要职责是通过用户界面向用户显示数据信息,同时解释用户的命令,并把用户的申请发送到应用层。应用层:通过调用根底设置和畛域层实现数据资源操作及业务流程编排,相当于BS层;畛域层:将业务逻辑高度内聚到畛域层,所以畛域层是整个零碎的外围,它只与理论业务相干,不关怀任何技术细节,尽可能做到与长久化无关;基础设施层:蕴含了任何类型的框架、数据库拜访代码或者公共的办法等,纯技术的一层; 2.3、如何学习畛域驱动设计没有谁可能做到畛域驱动设计的欲速不达,所谓"实践联系实际",在刚开始接触或学习设计畛域驱动时,总会有一种诉求心愿能给出公式般的设计准则或标准,仿佛软件设计就像拼积木个别,只有遵循图示给出的拼搭过程,不经思考就能拼出期待的模型,这仿佛不切实际的空想,要把握畛域驱动设计,首先要理解把握一些概念以常识实践,在此基础之上思考这些概念背地蕴含的原理,设计准则,思考限界上下文(Bounded Context)边界的划分,理论还是围绕"高内聚、低耦合"准则的体现,只是咱们思考什么内容才是高内聚,如何形象能力做到低耦合,在分层架构中,各层之间该如何合作?呈现了依赖如何解耦,依然须要从重用与变动的角度去思考设计决策。 3 畛域驱动设计畛域驱动设计强调以"畛域"为外围驱动力,通过模型驱动设计来保障畛域模型与程序设计的统一,畛域模型不应该蕴含任何技术实现因素,模型中的对象实在的表白了畛域概念,却不受技术实现的束缚,畛域模型自身和技术无关,畛域驱动从设计上划分为策略设计和战术设计。 一个畛域是由一个或多个模型组成;从定义上讲模型是畛域的形象;从了解上将模型能够认为是一个高内聚、低耦合的组件、模块,也能够称为一个子畛域;模型重在建模过程,建模过程会形象出一系列畛域对象和畛域服务;在DDD中,定义一系列的规范"畛域元素"用于领域建模;如战术设计元模型 3.1. 策略设计强调业务策略上的重点,如何按重要性调配工作,以及如何进行最佳,遵循量大准则: 必须领导设计决策,以便缩小各个局部之间的相互依赖,在应用设计用意更为清晰的同时而又不失去要害的互操作性和系统性;必须把模型的重点放在捕捉零碎概念外围,也就是零碎的"近景"上。3.1.1 子域划分整个业务畛域的一部分,关注与宏观业务,通过对大畛域进行划小在业务间划分出概念上分界线,便于在零碎策划阶段决策如何分配资源(人、钱)。 外围子域:畛域中最有价值和最外围的局部,产品成败的要害,外围竞争力,在DDD开发中,次要关注核 心域,给予最高优先级;撑持子域:我的项目中对外围子域起着撑持作用的相干性能,专一于业务的某个背面;通用子域:与我的项目用意无关的内聚子畛域,解决一些通用问题,任何专有的业务都不应该放在通用子域;3.1.2. 策略建模关注点在于零碎物理划分,依据你对畛域的宰割后果及公司或部门的组织构造决策如何划分子系统,比方分出几个,零碎间如何交互,该层面往往暂不会波及够多技术细节。 限界上下文(Bounded Context):艰深讲指零碎中模块,微服务架构中的子服务、单体中"包(Java)"或名称空间(C#),对系统的一个物理划分,限定了畛域模型的边界,在更深层次介绍深挖,这货色得分成两个词:限界、上下文,能够了解成零碎设计之初,你须要画一个圈设置一个范畴,保障畛域模型限度在这个圈内不可串场,这个‘圈’即为限界上下文。通用语言:用于对立领域专家、产品、研发、测试大家在应用的语言,避免出现需要了解不统一,设计与需要不统一,沟通不顺畅等问题,简略来说大家在一起聊某个货色的时候都能明确彼此所致的是什么,场景不同,同一个词就会有着不同含意。上下文映射图:用图的形式,表白出限界上下文之间关联,后续会独自在具体介绍上下文映射图,如 单干关系、防腐层、大泥球等;3.2. 战术设计依赖于畛域模型和通用预言,通过技术模式将畛域模型和通用预言中的概念映射到代码实现中。随着模型的进化,代码实现也会进行重构,以更好的体现模型概念。 次要包含:代表畛域中的概念,如实体、值对象、畛域服务、模块等;用于治理对象的生命周期。如聚合、工厂、仓库等;用于集成或跟踪,如畛域事件等; 3.3. 名词解释畛域/子域:什么畛域?从狭义上将,畛域即是一个组织所做的事件以及所蕴含的所有,畛域可大可小有界线,不是无限大,如电商畛域,交易畛域,购物畛域等,比方咱们常听客户说“咱们有这样几块业务”一般来说这里所谓"几块儿"就是指子域 。实体(entity):这个词被咱们宽泛应用,甚至过分应用,实体是一个重要的概念,一个典型实体应具备3个因素(身份标识、属性、畛域行为),必须有惟一的身份标识,没有身份标识的畛域对象就不是实体。如:User对象就是一个实体。属性:实体的属性用来阐明主体的动态特色,并持有数据与状态。@Datapublic class Product{ private String sku; private String name; private Price price;}畛域行为:实体领有畛域行为,能够更好地阐明其作为主体的动静特色。一个不具备动静特色的对象不属于畛域行为。@Datapublic class Product{ private String sku; private String name; private Price price; //变更状态的畛域行为 public void changePriceTo(Price newPrice){ //设计产品新加个 ....... }}值对象(value object):比拟形象,通常作为实体的属性,区分值对象与实体的区别在于,值对象是不可变的,并且没有惟一标识,仅由其属性的值定义,参加则对它的判断是根据值还是根据身份标识,前者是值对象,后者是实体;举个小例子:订单项和订单的关系:多对一,一个订单里有多条订单项,一个订单项,只会呈现在一个订单里,组合关系,局部不能脱离主体独自存在 ...

September 6, 2023 · 1 min · jiezi

关于ddd:实践篇DDD脚手架及编码规范-京东云技术团队

一、背景介绍咱们团队始终在继续推动业务零碎的体系化治理工作,在这个过程中咱们积淀了本人的DDD脚手架我的项目。脚手架我的项目是体系化治理过程中比拟重要的一环,它的作用有两点: (1)能够对新建的我的项目进行对立的标准; (2)对于领导老我的项目进行DDD的革新提供领导。 本文次要是梳理和总结了DDD脚手架应用中的编码标准以及遇到的问题。 二、脚手架的实践根底DDD相干的利用架构有很多种,比方四层架构,洋葱架构,六边形架构,整洁架构等。这些利用架构都有各自的特点和不同。然而他们的总体思维都是类似的,次要是通过分层来实现性能和关注点的隔离。达到的指标是畛域层不依赖任何其余内部实现,这样就能保障外围业务逻辑的洁净和稳固。 左图是整洁架构的示意图,左图为分层,右图示意各个分层的变动频率和形象层级。整洁架构次要分为4层: (1)Frameworks&Drivers层:这一层示意零碎依赖的内部零碎,比方数据库、缓存、前端页面等。这一层是变动频率最高的,也是须要和咱们的外围业务逻辑做隔离的。 (2)Interface Adapters层:这一层是一个适配层,次要负责内部零碎和外部业务零碎的适配,这一层的次要作用就是内部零碎和外部零碎的适配和协定转换。 (3)Application Business Rules: 利用业务规定层,能够了解为用例层,这一层示意整个利用能够提供哪些用例级别的性能和服务。这一层也是对第4层中的外围业务规定的编排层。 (4)Enterprise Business Rules: 这一层就是最为外围的业务逻辑层,这一层不蕴含任何和技术相干的内容,只蕴含业务逻辑。 三、脚手架介绍及应用应用命令如下: mvn archetype:generate -DarchetypeGroupId=com.jd.jr.cf -DarchetypeArtifactId=ddd-archetype -DarchetypeCatalog=local -DarchetypeVersion=0.0.1-SNAPSHOT -DinteractiveMode=false -DgroupId=com.jd.demo.test //从这一行开始须要依据项目名称批改 -DartifactId=demo-test -Dversion=1.0.0 -Dpackage=com.jd.demo.test -DappName=demo-test -s D:/git/settings.xml // 本地 git配置文件生成完的我的项目构造如下: |--- adapter -- 适配器层 利用与内部利用交互适配| |--- controller -- 控制器层,API中的接口的实现| | |--- assembler -- 拆卸器,DTO和畛域模型的转换| | |--- impl -- 协定层中接口的实现| |--- repository -- 仓储层| | |--- assembler -- 拆卸器,PO和畛域模型的转换| | |--- impl -- 畛域层中仓储接口的实现| |--- rpc -- RPC层,Domain层中port中依赖的内部的接口实现,调用近程RPC接口| |--- task -- 工作,次要是调度工作的适配器|--- api -- 利用协定层 利用对外裸露的api接口|--- boot -- 启动层 利用框架、驱动等| |--- aop -- 切面| |--- config -- 配置| |--- Application -- 启动类|--- app -- 应用层| |--- cases -- 应用服务|--- domain -- 畛域层| |--- model -- 畛域对象| | |--- aggregate -- 聚合| | |--- entities -- 实休| | |--- vo -- 值对象| |--- service -- 域服务| |--- factory -- 工厂,针对一些简单的Object能够通过工厂来构建| |--- port -- 端口,即接口| |--- event -- 畛域事件| |--- exception -- 异样封装| |--- ability -- 畛域能力| |--- extension -- 扩大点| | |--- impl -- 扩大点实现|--- query -- 查问层,封装读服务| |--- model -- 查问模型| |--- service -- 查问服务整体的分层架构图如下: ...

August 24, 2023 · 1 min · jiezi

关于ddd:DDD-架构分层MQ消息要放到那一层处理

作者:小傅哥 博客:https://bugstack.cn 积淀、分享、成长,让本人和别人都能有所播种!本文的主旨在于通过简略洁净实际的形式教会读者,应用 Docker 配置 RocketMQ 并在基于 DDD 分层构造的 SpringBoot 工程中应用 RocketMQ 技术。因为大部分 MQ 的发送都是基于特定业务场景的,所以本章节也是基于 《MyBatis 应用教程和插件开发》 章节的扩大。 本章也会包含对于 MQ 音讯的发送和接管应该处于 DDD 的哪一层的实际解说和应用。 本文波及的工程: xfg-dev-tech-rocketmq:https://gitcode.net/KnowledgePlanet/road-map/xfg-dev-tech-roc...RocketMQ Docker 装置:rocketmq-docker-compose-mac-amd-arm.yml导入测试库表 road-map.sql一、案例背景首先咱们要晓得,MQ 音讯的作用是用于;解耦过长的业务流程和应答流量冲击的消峰。如;用户下单领取实现后,拿到领取音讯推动后续的发货流程。也能够是咱们基于 《MyBatis 应用教程和插件开发》 中的案例场景,给雇员晋升级别和薪资的时候,也发送一条MQ音讯,用于发送邮件告诉给用户。 从薪资调整到邮件发送,这里是2个业务流程,通过 MQ 音讯的形式进行连贯。其实MQ音讯的应用场景特地多,原来你可能应用多线程的一些操作,当初就扩大为多实例的操作了。发送 MQ 音讯进去,让利用的各个实例接管并进行生产。二、畛域事件因为咱们本章所解说的内容是把 RocketMQ 放入 DDD 架构中进行应用,那么也就引申出畛域事件定义。所以咱们先来理解下,什么是畛域事件。 畛域事件,能够说是解耦微服务设计的要害。畛域事件也是畛域模型中十分重要的一部分内容,用于标示以后畛域模型中产生的事件行为。一个畛域事件会推动业务流程的进一步操作,在实现业务解耦的同时,也推动了整个业务的闭环。 首先,咱们须要在畛域模型层,增加一块 event 区域。它的存在是为了定义出于以后畛域下所需的事件音讯信息。信息的类型能够是model 下的实体对象、聚合对象。之后,音讯的发送是放在根底设置层。自身根底设置层就是依赖倒置于模型层,所以在模型层所定义的 event 对象,能够很不便的在根底设置层应用。而且大部分开发的时候,MQ音讯的发送与数据库操作都是关联的,采纳的形式是,做完数据落库后,推送MQ音讯。所以定义在仓储中实现,会更加得心应手、瓜熟蒂落。最初,就是 MQ 的音讯,MQ 的生产能够是本身服务所收回的音讯,也能够是内部其余微服务的音讯。就在小傅哥所整体讲述的这套扼要教程中 DDD 局部的触发器层。三、环境装置本案例波及了数据库和RocketMQ的应用,都曾经在工程中提供了装置脚本,能够按需执行。 这里次要介绍 RocketMQ 的装置; 1. 执行 compose yml文件:docs/rocketmq/rocketmq-docker-compose-mac-amd-arm.yml - 对于装置小傅哥提供了不同的镜像,包含Mac、Mac M1、Windows 能够按需抉择应用。 version: '3'services: # https://hub.docker.com/r/xuchengen/rocketmq # 留神批改项; # 01:data/rocketmq/conf/broker.conf 增加 brokerIP1=127.0.0.1 # 02:data/console/config/application.properties server.port=9009 - 如果8080端口被占用,能够批改或者增加映射端口 rocketmq: image: livinphp/rocketmq:5.1.0 container_name: rocketmq ports: - 9009:9009 - 9876:9876 - 10909:10909 - 10911:10911 - 10912:10912 volumes: - ./data:/home/app/data environment: TZ: "Asia/Shanghai" NAMESRV_ADDR: "rocketmq:9876"在 IDEA 中关上 rocketmq-docker-compose-mac-amd-arm.yml 你会看到一个绿色的按钮在左侧侧边栏,点击即可装置。或者你也能够应用命令装置:# /usr/local/bin/docker-compose -f /docs/dev-ops/environment/environment-docker-compose.yml up -d - 比拟适宜在云服务器上执行。首次装置可能应用不了,一个起因是 brokerIP1 未配置IP,另外一个是默认的 8080 端口占用。能够依照如下小傅哥说的形式批改。2. 批改默认配合关上 data/rocketmq/conf/broker.conf 增加一条 brokerIP1=127.0.0.1 在结尾# 集群名称brokerClusterName = DefaultCluster# BROKER 名称brokerName = broker-a# 0 示意 Master, > 0 示意 SlavebrokerId = 0# 删除文件工夫点,默认凌晨 4 点deleteWhen = 04# 文件保留工夫,默认 48 小时fileReservedTime = 48# BROKER 角色 ASYNC_MASTER为异步主节点,SYNC_MASTER为同步主节点,SLAVE为从节点brokerRole = ASYNC_MASTER# 刷新数据到磁盘的形式,ASYNC_FLUSH 刷新flushDiskType = ASYNC_FLUSH# 存储门路storePathRootDir = /home/app/data/rocketmq/store# IP地址brokerIP1 = 127.0.0.1关上 `data/console/config/application.properties批改server.port=9009 端口。server.address=0.0.0.0server.port=9009批改配置后,重启服务。3. RockMQ登录与配置3.1 登录RocketMQ 此镜像,会在装置后在控制台打印登录账号信息,你能够查看应用。 ...

August 17, 2023 · 3 min · jiezi

关于ddd:实践篇手把手教你落地DDD-京东云技术团队

1. 前言常见的DDD实现架构有很多种,如经典四层架构、六边形(适配器端口)架构、整洁架构(Clean Architecture)、CQRS架构等。架构无优劣高下之分,只有熟练掌握就都是适合的架构。本文不会一一去解说这些架构,感兴趣的读者能够自行去理解。 本文将率领大家从日常的三层架构登程,精炼推导出咱们本人的利用架构,并且将这个利用架构实现为Maven Archetype,最初应用咱们Archetype创立一个简略的CMS我的项目作为本文的落地案例。 须要明确的是,本文只是给读者介绍了DDD利用架构,还有许多概念没有波及,例如实体、值对象、聚合、畛域事件等,如果读者对残缺落地DDD感兴趣,能够到本文最初理解更多。 2. 利用架构演变咱们很多我的项目是基于三层架构的,其构造如图: 咱们说三层架构,为什么还画了一层 Model 呢?因为 Model 只是简略的 Java Bean,外面只有数据库表对应的属性,有的利用会将其独自拎进去作为一个\Maven Module,但实际上能够合并到 DAO 层。 接下来咱们开始对这个三层架构进行形象精炼。 2.1 第一步、数据模型与DAO层合并为什么数据模型要与DAO层合并呢? 首先,数据模型是贫血模型,数据模型中不蕴含业务逻辑,只作为装载模型属性的容器; 其次,数据模型与数据库表构造的字段是一一对应的,数据模型最次要的利用场景就是DAO层用来进行 ORM,给 Service 层返回封装好的数据模型,供Service 获取模型属性以执行业务; 最初,数据模型的 Class 或者属性字段上,通常带有 ORM 框架的一些注解,跟DAO层分割十分严密,能够认为数据模型就是DAO层拿来查问或者长久化数据的,数据模型脱离了DAO层,意义不大。 2.2 第二步、Service层抽取业务逻辑上面是一个常见的 Service 办法的伪代码,既有缓存、数据库的调用,也有理论的业务逻辑,整体过于臃肿,要进行单元测试更是无从下手。 public class Service { @Transactional public void bizLogic(Param param) { checkParam(param);//校验不通过则抛出自定义的运行时异样 Data data = new Data();//或者是mapper.queryOne(param); data.setId(param.getId()); if (condition1 == true) { biz1 = biz1(param.getProperty1()); data.setProperty1(biz1); } else { biz1 = biz11(param.getProperty1()); data.setProperty1(biz1); } if (condition2 == true) { biz2 = biz2(param.getProperty2()); data.setProperty2(biz2); } else { biz2 = biz22(param.getProperty2()); data.setProperty2(biz2); } //省略一堆set办法 mapper.updateXXXById(data); }}这是典型的事务脚本的代码:先做参数校验,而后通过 biz1、biz2 等子办法做业务,并将其后果通过一堆 Set 办法设置到数据模型中,再将数据模型更新到数据库。 ...

May 29, 2023 · 2 min · jiezi

关于ddd:实践篇领域驱动设计DDD工程参考架构-京东云技术团队

背景为什么要制订参考工程架构不同团队落地DDD所采取的利用架构格调可能不同,并没有对立的、规范的DDD工程架构。有些团队可能遵循经典的DDD四层架构,或改良的DDD四层架构,有些团队可能综合思考分层架构、整洁架构、六边形架构等多种架构格调,有些在实践中可能引入CQRS解决读模型与写模型的差异化等等。即便无奈制订通用的、规范的工程利用架构,但为团队制订一个遵循畛域驱动设计思维的参考架构仍然有价值。基于以下起因: 为团队实际DDD的战术设计提供能够疾速开始的工程参考参考工程大量的命名和构造决策,显式的体现DDD的相干理念,有利于团队对DDD的战术实现达成统一认知同时,参考架构有助于积淀团队对畛域驱动设计的一些思考和最佳实际参考架构的考量因素尽管无奈制订齐全通用的DDD参考架构,但制订某个特定上下文下的参考架构却具备可行性和实际价值。针对于上下文的抉择要尽量贴合理论的工程实际场景并思考多维度的因素。 本文所述参考工程架构遵循以下准则: 遵循畛域驱动设计的实质思维充分考虑业务零碎建设特点依赖最小化,放弃轻量希望工程参考架构能涵盖以下范畴 拆散业务域与技术域参考架构要遵循技术和业务隔离的个性,能够参考多种架构格调。业务与技术关注点的拆散并不是DDD独有的特点,在六边形、整洁架构、洋葱架构中都遵循了这一重要准则。 多限界上下文场景大多数团队基于DDD进行微服务拆分的时候,特地是零碎建设初期,对单个微服务利用内的限界上下文的粒度须要衡量。因为团队组织架构因素及微服务老本问题,单个利用包容的限界上下文个别是多个(现实状况下是1:1)。这些限界上下文随着后续的逐渐迭代有可能会迁徙至独立利用。因而,参考架构将多上下文的利用场景作为重要考量因素。 明确的组件、职责边界及依赖关系反对畛域报表场景:报表场景在业务零碎较为常见,DDD并没有体现该场景的解决形式。作为工程参考架构,还是心愿可能从理论业务登程,体现对写模型和报表模型的显示反对内部依赖最小化:须要排除不必要的依赖,放弃工程架构的轻量化参考架构分析利用的多上下文构造基于以上准则,参考工程思考单个利用内多上下文的场景,以期在模块化和服务粒度及老本间进行衡量折衷。利用架构对多上下文的反对的逻辑示意图如下所示,在解决方案域对限界上下文进行辨认和划分之后,基于其业务内聚性和关联性,把多个上下文实现单个工程利用中。单个利用内的多个限界上下文间可能存在交互,交互的模式能够是基于事件驱动,也能够是基于过程内调用。采纳事件驱动的形式上下文间的耦合性对低一些,但个别须要引入事件总线的反对,额定组件的引入必然会导致复杂性的回升。过程内调用则耦合性会高一些,但从实现角度复杂度会低一些。具体抉择哪种形式开发人员能够基于理论状况进行衡量。 须要再次阐明的是,这种利用架构决策是一种多因素的衡量,并没有与子域与限界上下文1:1的理想化实际保持一致。 从上图的逻辑示意图咱们再深刻一层,从分层的维度去分析一下具体的利用架构的展示模式,如下图所示: 分层关注点客户端 客户端与利用处于不同的过程,是利用能力的生产端,在理论我的项目中可能是APP端、PC端、小程序端、公众号端或三方的业务调用端等等。 接入层 接入层是内部零碎与利用外部业务能力的中间层,接入层是应用层对外的门面,是以后利用对外裸露业务能力的入口。该层的组成可能是对外提供的HTTP接口申明、分布式定时任务调度、音讯监听器、RPC服务等等。其重要职责包含对外部零碎的申请进行根底的参数校验、入参适配和服务路由(转发至系一层的应用服务)以及响应数据的适配。 业务层: 该层是利用的业务逻辑所在层,整个架构格调采纳模块化单体格调,在该层不同的限界上下文体现为不同的模块。在每个限界上下文内采纳分层架构,独立划分为应用层、畛域层和网关层。 应用层: 协调畛域对象、畛域服务或内部依赖服务实现业务用例,该层只做能力协调,不解决任何畛域逻辑。 畛域层: 畛域层是整个分层的外围,与技术实现无关,次要负责畛域模型、畛域事件、畛域服务定义,以及业务相干内部服务的接口形象以及仓库的接口形象等。 畛域层与应用服务的本质区别是:应用层不蕴含畛域逻辑,畛域逻辑全副下沉到畛域层实现。 网关层: 网关层定位是利用的进口网关,是利用与内部基础设施交互的防腐层,解决所有技术相干实现。 该组件的命名有多种形式,比方有些团队将其命名为 “rpc”,也有些团队将其命名为 “infrastructure”,不同的命名体现了团队对其背地所表白的隐喻的决策抉择。在本文的参考架构中抉择了 网关-Gateway这一命名,决策起因是:限界上下文本身是高内聚的,其与内部的交互须要对立进口,Gateway所表白的网关的含意失当的体现了这种对立进口的理念。如果Facade层是利用的北向网关,是内部零碎申请进入外部的入口。则此时的Gateway则表白的是限界上下文的南向网关,是外部利用连贯内部的进口。 组件及依赖从宏观的分层咱们再深刻一层看下每层的组件划分。如下图所示: Start组件: 整个利用的启动入口、加载利用配置信息等等。 Common组件 提供在不同的限界上下文间复用的畛域模型元素的形象,比方对Command、Query、Event、Entity、ValueObjec通用形象等。当然,畛域模型的通用形象不是必须在Common组件内以提供复用,也能够作为一个独立的限界上下文,并以共享内核形式与其它上下文进行共享,或者也能够实现为独立的jar包组件。 API 组件 RPC类型服务的接口申明组件,以公司外部应用的JSF为例,该组件是利用对外部零碎裸露的JSF API的组件。该组件能够是独立的工程,当然,有些团队会将其作为一个Module放入利用工程中。 对立门面组件:Facade 内部客户端触达利用零碎的入口,也是外部应用服务的对立门面,相似于六边形架构格调下的适配器。参考架构中基于不同场景划分为 provider(RPC服务)、task(定时工作)、listener(MQ监听)、rest(http接口)等几个子包。内部申请进入零碎后,由Facade组件实现入参根本校验、入参转换、服务路由以及出参转换等操作。另外,还能够承当解决登录态、鉴权以及日志等相干能力。 应用服务组件:Application Service 应用服务代表着用例以及零碎行为,其通过委托到畛域层和基础设施层(参考架构中的Gateway组件)实现用例的应用逻辑逻辑解决,能够了解为应用服务是畛域层的客户端。该组件典型的职责: 从存储层加载畛域对象、委托畛域对象执行畛域逻辑、保留畛域对象 重要事件告诉到内部出入参转化适配事务处理内部非畛域逻辑的服务调用External API 应用服务的逻辑不仅仅须要协调畛域层,有时还须要依赖于内部的三方服务。External API 组件负责对这些内部服务进行接口申明定义,不做具体实现。 应用服务组件不间接依赖这些内部服务实现,而是依赖其接口形象。同时,此处的模型定义是基于该限界上下文的语义,是一种对外部模型的适配。 该组件不依赖其它组件,且仅被应用服务组件和Gateway组件依赖。网关组件依赖External API 组件的接口申明并提供底层技术实现,应用服务组件依赖其接口,并通过IOC形式将具体实现注入实现服务调用。 留神,该组件所依赖的服务不波及畛域逻辑,只是用于撑持应用服务的编排。如果是波及了畛域逻辑,则对外部服务依赖的接口定义须要下沉到Domain层。 Query Query组件解决畛域相干的报表查问场景,在限界上下文内作为与应用服务对等的组件存在,两个组件别离负责业务的查问和命令逻辑。 尽管引入了Query组件对报表场景提供反对,但没有齐全的引入CQRS这一模式。在很多材料中CQRS与DDD同时提及的概率比拟高,因为,在DDD下咱们解决了简单的面向畛域的写侧模型,但在报表场景下,这种富畛域模型有可能并不是最佳抉择。如果读侧和写侧都基于对立的畛域模型,个别会导致畛域模型的折衷设计。为了满足查问侧诉求,畛域模型不得不引入额定的、畛域无关的属性,由此造成畛域模型的净化。 Domain Domain组件是畛域逻辑外围,承当整个零碎畛域逻辑的实现,其定义了畛域模型、畛域服务、畛域事件以及仓储层的形象。该组件不依赖其它组件(除了通用的畛域模型形象组件Common之外。 上图所体现的参考架构应用了DDD的战术设计的经典建模元素,比方聚合、实体、值对象、仓储、工厂以及畛域事件等。在理论落地过程中,这些设计元素的形象具备肯定的挑战,设计过程中须要通过一直剖析、衡量和重构以实现建模,这正是外围设计所在。 Gateway 网关层承当整个技术相关性的实现,是外部利用的进口网关。 技术相关性是网关组件区别于其它组件的基本个性。在该组件内要解决技术实现的所有细节,比方与内部服务、中间件、DB的交互等。同时,其与Domain组件以及External API组件的接口形象合作,独特承当了零碎与内部依赖间(包含内部服务以及利用依赖的中间件、DB等基础设施)的防腐层职能,负责外部模型到内部模型转化、内部模型到外部模型转化以及具体交互。基于网关组件的个性,也非常适合在该层做对立的内部服务数据缓存及降级熔断解决。 网关层依赖于Domain、Application Service、External API和Query组件,负责上述四个组件定义的接口实现。在Gateway组件通过子包对实现进行隔离: query:查问服务组件的实现external:External API 组件中依赖内部服务的接口实现repository:仓储接口的实现最初利用架构模式的抉择是零碎架构设计的重要维度之一,构造不仅仅是简略的包构造和命名,其传播的是一种顶层形象,背地蕴含了大量的实际和常识。制订合乎团队状况的工程参考架构,并在团队成员间达成共识十分重要。畛域驱动设计并没有对立的、通用的架构,试图定义规范架构是不切实际的。本文形容的工程架构只是一个参考,实际过程中应该基于团队特定状况而有所差别,但原则上都应该遵循业务域与技术域拆散的核心理念。 ...

May 22, 2023 · 1 min · jiezi

关于ddd:领域驱动设计DDD架构解析和绘图模板分享

DDD整洁架构DDD整洁架构为了解决强调用的关系,呈现了洋葱架构(六边形)架构,就是为了实现依赖倒置 它的思维就是把畛域模型放到外围的地位,畛域模型是独立的,不会间接强依赖其余层,而通过适配器来实现畛域模型和外层的数据交换。 DDD分层架构和三层架构的区别与关系DD分层架构和三层架构的区别与关系DDD代码分层架构与传统三层架构比照,能够发现传统三层架构被看成是一个贫血模式的畛域驱动设计 DDD分层接口调用时序逻辑关系DDD分层接口调用时序逻辑关系上面是基于DDD畛域模型设计的零碎中罕用接口调用时序交互流程 CQRS架构CQRS架构CQRS,中文名为命令和查问职责拆散。 CQRS 将零碎中的操作分为两类,即「命令」(Command)  与「查问」(Query) 。命令则是对会引起数据发生变化操作的总称,即咱们常说的新增,更新,删除这些操作,都是命令。而查问则和字面意思一样,即不会对数据产生变动的操作,只是依照某些条件查找数据。 CQRS 的核心思想是将这两类不同的操作进行拆散,而后在两个独立的「服务」中实现。这里的「服务」个别是指两个独立部署的利用。在某些非凡状况下,也能够部署在同一个利用内的不同接口上。 Command 与 Query 对应的数据源也应该是相互独立的,即更新操作在一个数据源,而查问操作在另一个数据源上。当然查问和命令对应的数据源尽管不一样,然而必定是须要同步的,那咱们该怎么实现数据源的同步呢? 从图上能够看到,当 command 零碎实现数据更新的操作后,会通过「畛域事件」的形式告诉 query 零碎。query 零碎在承受到事件之后更新本人的数据源。所有的查问操作都通过 query 零碎裸露的接口实现。 从架构图上来看,CQRS 的实现仿佛并不难,许多开发者感觉无非是「增删改」一套零碎一个数据库,「查问」一个零碎一个数据库而已,有点相似「读写拆散」,并没有什么特地的中央。然而真正要应用 CQRS 是有许多问题与细节要解决的。 备注:以上架构图都应用PDDON在线画图工具收费制作,有须要要的敌人能够到官网上找到相似的架构图模板哦,收费克隆模板,依据自家业务克隆模板进行调整就能够应用了,工具和模板地址:https://pddon.com

May 18, 2023 · 1 min · jiezi

关于ddd:一文说透DDD真香

本文首发自「慕课网」(www.imooc.com),想理解更多IT干货内容,程序员圈内热闻,欢送关注"慕课网"! 如果你晓得微服务,那么就肯定据说过DDD..... DDD到底是什么?大家都晓得,微服务划分的一个重要实践根底就是畛域驱动设计。而DDD全称就是“畛域驱动设计”,它是一种软件开发中用到的建模设计思维,软件的建模和设计相似修建畛域中建筑师的工作。建筑师把修建的架构设计进去,首先是要满足和优化用户的需要,用户要住着舒服,平安;同样,软件的业务架构师也须要设计软件的业务架构,让软件可能十分好地满足业务需要,推动业务倒退,软件的建模和设计就是业务架构中的重要工作内容。 业务越简单的软件,建模和设计就越难,中国在芯片畛域被卡脖子,不仅是因为没有光刻机,另一个重要起因是短少芯片设计的EDA软件,EDA软件就是简单软件的一个典型代表,芯片畛域极其简单,所以EDA软件的建模和设计也同样简单。对简单软件,传统的面向对象思维曾经不够用了,举个例子,某大厂尽管具备极强的技术和治理能力,然而,晚期应用面向对象的建模和设计思维来开发整个电商零碎,最终也是以失败告终。DDD就是为了解决业务简单的软件系统的建模和设计问题诞生的一种新思维(其实不算太新,然而近几年逐步升温)。 DDD被广泛应用于哪里?但凡波及到简单软件系统的设计,都须要用到DDD。 例如DDD对业务零碎进行建模和设计,能够使简单零碎更具备可维护性和可更改性,各个模块的耦合更低,不会呈现牵一发而动全身的状况。这一点,从上面咱们会提到的某大厂应用面向对象建模和设计办法的失败教训,到当初WXG采纳DDD就可能看到。 为什么DDD这么火?一方面,随着国内经济的倒退,晚期的倒退红利曾经逐步隐没了,空白畛域根本不复存在,各畛域竞争越来越强烈,业务的复杂性也越来越高,所以对应的软件系统的复杂性也越来越高,以前简略软件也能赚钱的时代曾经一去不返了。只会CURD、设计简略软件的人当然也没有市场了,市场须要的是具备简单软件设计能力的人才,DDD是目前最无效的针对业务简单零碎的建模和设计思维,所以,市场上对把握DDD的人才需求量一直减少。把握DDD的人,能力担当古代软件系统的架构师,有较好的职业倒退空间。 另一方面,随着微服务架构的风行,人们发现DDD可能为微服务架构中的一些问题提供理论指导,比方:如何划分微服务。所以人们更加意识到DDD思维的重要性。 发展趋势: 1) 和大数据、AI联合; 2) 实践和具体建模办法不断完善; 3) 面向DDD的框架和编程语言。 DDD的特点和外围是什么?DDD的核心思想简略用一句话来说就是:通过合成来管制复杂性。合成又分两种:横向合成和纵向合成。 所谓横向合成:是指通过畛域划分来合成问题,通过限界上下文来合成零碎,把一个简单的畛域分解成几个不太简单的子畛域,不同子畛域解决不同的问题,不同的子畛域用不同的限界上下文来实现,这样,单个限界上下文的复杂度就可控了。纵向合成:是指把技术实现从具体的业务逻辑分离出来,防止技术复杂性传染到业务层,当技术计划变更时,也不会影响业务代码。DDD的分层架构和形成因素有哪些?传统的分层架构分为上面四层,然而畛域层依赖基础设施层会让畛域层受到基础设施层的具体技术实现影响。所以,倒退进去六边形架构和洋葱架构,这类架构归纳起来,统称为“整洁架构”。 整洁架构的特点,就是把最外围的畛域层,放在两头,不依赖其余层。 大厂应用DDD的多吗?能够很负责任的通知你,目前大厂应用DDD的十分多,基本上所有的业务开发都在学习和利用DDD,或者是在利用DDD的路上。 从市场JD咱们能够看出: 1) 市场须要懂DDD的人才,因为懂DDD的人才可能设计简单零碎; 2) 懂DDD的人才能负责古代业务零碎的架构师,才会有更好的职业倒退空间; 3) 大厂的外围部门都在应用DDD,应用DDD可能使软件系统的设计更正当,具备更好的可维护性和可改变性,模块和模块之间的耦合更低,业务不会受技术的影响,不会牵一发而动全身。 目前而言,DDD是针对简单业务软件系统进行建模与设计的惟一无效的方法论工具,不夸大地说,在当今阶段,只有学会利用DDD,技术人员才真正具备了成为架构师的资格。能够必定的是,任何软件开发人员,只有想实现更好的职业倒退,都能够学习DDD,从中受害。 欢送关注「慕课网」帐号,咱们会始终保持内容原创,提供IT圈优质内容,分享干货常识,大家一起独特成长吧! 本文原创公布于慕课网 ,转载请注明出处,谢谢合作

May 11, 2023 · 1 min · jiezi

关于ddd:项目终于用上了-DDD-领域驱动太强了

在公司对领取业务、结算业务、资金业务应用DDD进行领域建模的两年,失去了许多好评,也面对过不少质疑,总体来说还是能播种不少,这对团队成员了解业务起着很大作用。近半年始终在钻研DDD的落地实战,现在已修得阶段性成绩,急不可待与大家分享我的落地教训。 DDD分为策略设计与战术设计。一般来说,领域建模是属于策略层的,而DDD工程落地是属于战术层的,两者是否联合应用,视理论状况而定,比方传统的MVC架构也能应用DDD进行领域建模,DDD架构最好是先做DDD领域建模。 最新上线的一个微服务——外部交易中心,咱们应用了DDD架构来落地,心愿看完对大家有启发。 工程架构分层实践在工程落地之前,咱们有必要先理解下支流的工程架构或架构思维都有哪些,对这些实践有所理解的,也能够间接跳过看下一个局部。 1、经典DDD四层架构 在该架构中,下层模块能够调用上层模块,反之不行。即: Interface ——> application | domain | infrastructureapplication ——> domain | infrastructuredomain ——> infrastructure分层作用: 用户界面层/体现层:负责向用户显示解释用户命令应用层:定义软件要实现的工作,并且指挥协调畛域对象进行不同的操作。该层不蕴含业务畛域常识畛域层/模型层:零碎的外围,负责表白业务概念,业务状态信息以及业务规定。即蕴含了该畛域(问题域)所有简单的业务知识形象和规定定义。该层次要精力要放在畛域对象剖析上,能够从实体,值对象,聚合(聚合根),畛域服务,畛域事件,仓储,工厂等方面动手基础设施层:一是为畛域模型提供长久化机制,当软件须要长久化能力时候才须要进行布局;二是对其余层提供通用的技术支持能力,如音讯通信,通用工具,配置等的实现;2、整洁架构思维 整洁架构(Clean Architecture)是由Bob大叔在2012年提出的一个架构模型,顾名思义,是为了使架构更简洁。 依赖规定:用一组同心圆来示意软件的不同畛域。一般来说,越深刻代表你的软件档次越高。外圆是战术是实现机制,内圆的是外围准则。 这条规定规定软件模块只能向内依赖,而外面的局部对里面的模块无所不知,也就是外部不依赖内部,而内部依赖外部。同样,在里面圈中应用的数据格式不应被内圈中应用,特地是如果这些数据格式是由里面一圈的框架生成的。 这样做的最大益处是当零碎的内部模块不得不扭转时(比方,替换已有的过期的数据库系统),零碎的内层模块不须要做任何扭转。 3、六边形架构 六边形架构(Hexagonal Architecture),又叫做端口适配器模式,是由Alistair Cockburn在2005年提出的。 六边形架构将零碎分为外部(外部六边形)和内部,外部代表了利用的业务逻辑,内部代表利用的驱动逻辑、基础设施或其余利用。外部通过端口和内部零碎通信,端口代表了肯定协定,以API出现。 一个端口可能对应多个内部零碎,不同的内部零碎须要应用不同的适配器,适配器负责对协定进行转换。这样就使得应用程序可能以统一的形式被用户、程序、自动化测试、批处理脚本所驱动,并且,能够在与理论运行的设施和数据库相隔离的状况下开发和测试。 4、菱形架构 作用于限界上下文的菱形对称架构从畛域驱动设计分层架构与六边形架构中吸取了养分,通过对它们的交融造成了以畛域为轴心的内外分层对称构造。 外部以畛域层的畛域模型为主,内部的网关层则依据方向划分为北向网关与南向网关。通过该架构,可清晰阐明整个限界上下文的组成: 北向网关的近程网关北向网关的本地网关畛域层的畛域模型南向网关的端口形象南向网关的适配器实现限界上下文以畛域模型为外围向南北方向对称发散,从而在边界内造成清晰的逻辑档次,前端UI并未蕴含在限界上下文的边界之内。每个组成元素之间的协作关系体现了清晰直观的自北向南的调用关系。 5、CQRS CQRS(Command Query Responsibility Segregation)意为命令查问职责拆散,它是一种与畛域驱动设计 (DDD) 和事件溯源相干的架构模式。Greg Young在2010年发明了这个术语,CQRS的内容基于Bertrand Meyer的CQS设计模式。 CQRS架构将写入和读取离开,它提出了独自的 API,一个专用于更改应用程序状态的命令路由,另一个专用于返回无关应用程序状态信息的查问路由。 工程架构分层设计基于各个架构有其本人的优缺点,咱们联合公司的现状,取其长避其短,交融一套适宜本人的架构。 以经典DDD四层架构为骨架,其余优良架构思维作领导CQRS命令/查问职责拆散,利用到DDD应用层,解决简单操作/简单查问整洁架构利用到DDD畛域层与基础设施层,接口与实现拆到不同层,把技术代码与业务代码拆散菱形架构领导咱们,外部以畛域层的畛域模型为主,向南北两个办法发散——北向网关(畛域层以上)提供本地网关(如Controller、MQListener)与近程网关(如API包);南向网关(畛域层以下)负责端口形象(如仓库接口)与适配器实现(如内部API封装实现)公司的Base框架在dal包封装了根底CRUD接口,利用到数据拜访层内,作为畛域层与基础设施层的粘合剂,简化链接当然,任何事物有其两面性,交融各个框架后,也有其优缺点—— 长处:通过拆散业务与技术代码,有利于业务迭代降级保护;业务驱动而非技术/数据驱动,通过写代码就能积攒肯定的业务知识;将畛域常识和技术常识分类,从而进步代码的可重用性。 毛病:对从业人员业务剖析能力较高,难以从经典MVC架构转变过去;层级较多,写代码前需思考分明逻辑应该写在哪一层;规定较多,没有MVC架构灵便,不适用于简略业务零碎;学习老本与转移老本比拟高,须要对DDD有更好的了解和更长的设计工夫(资金组践行DDD领域建模2年)。 工程代码构建案例看代码之前咱们先看下领域建模: 通过畛域模型剖析,外部交易中心分为外部调货、规定核心、外部出入库、外部销售、外部洽购这五大模块,每一个模块对应DDD就是一个聚合,所有聚合造成一个DDD的限界上下文(外部交易上下文),之前的文章提到,限界上下文就是咱们划分微服务的一个重要依据。 接下来,咱们联合DDD架构图与领域建模,看看工程代码应该怎么放。 基于Maven的DDD工程,顶层构造咱们按api、service划分为两个module。 api包的作用: api包的定位是跨服务的顶层契约,service包所有层都能够依赖api包api包定义了对外透出的枚举/常量、入参、出参、API接口等,为了方便使用api类,feign层不作业务划分api包只定义契约不写业务逻辑,防止因业务逻辑变更引发的api包降级service包的作用: service包是工程的顶层实现,DDD四层架构在service包体现Application程序入口与DDD的四层处于同一目录此外,针对service包还有另一种支流的module划分形式——间接把service包的api、application、domain、infrastructure作为四个独立的module,长处是能通过pom依赖的形式来限度层与层之间的依赖,开发人员能在编码阶段发现依赖问题及时修改,但毛病也显著——不够灵便,工程也会变得较重。 1、接入层(api) 接入层又叫用户接入层,支流用interface或api命名,基于包默认按字母排序的起因,我倡议应用“api”来命名接入层,但要留神,service包的api层与api包是不同的作用。 接入层是很薄的一层,负责间接对接前端申请或feign实现(facade里的Controller)、数据转换(assembler),入参/出参等契约类(request/response)对立定义在顶层的api包Controller负责对数据做前置校验,具体业务逻辑则交给应用服务或畛域服务实现,可间接调用应用服务办法或畛域办法业务划分在接入层不显著,更多是基于前端模块进行划分Controller,且业务简单时必然存在畛域穿插,故facade下没有再细分业务包assembler数据转换负责解决简单的数据转换,简略的数据转换可显式调用工具类的转换方法2、应用层(application) ...

May 9, 2023 · 1 min · jiezi

关于ddd:关于聚合根领域事件的那点事深入浅出理解DDD-京东云技术团队

作者:京东物流 赵勇萍 前言最近有空会跟共事探讨DDD架构的实际落地的状况,但真实情况是,理论中对于畛域驱动设计中的实体,值对象,聚合根,畛域事件这些战术类的实际落地,每个人了解仍然因人而异,大概率是因为这些概念还是有一些形象,同时有有别于传统的MVC架构开发。 在此,通过小demo的形式跟大家分享一下我对DDD中战术层级的了解,算是抛砖引玉,该了解仅代表我集体在现阶段的一个了解,也可能将来随着业务教训深刻,还会有不同的了解。 既然说是小demo,还是要从业务场景登程,也就是我最熟知的电商业务场景说起。然而该篇文章里, 我会简化一些理论业务场景中的复杂度,通过最小颗粒度的demo,来反映实际过程中的根本问题。 一个简略的demo业务场景话不多说,我先抛出我本人假如的一个业务场景,就是咱们熟知的电商网站下单购物的场景。具体细节如下: 1. 实体:• 商品:领有惟一标识、名称、价格、库存等属性。 • 订单:领有惟一标识、下单工夫、状态等属性。订单蕴含多个订单项。 2. 值对象:• 地址:领有省、市、区、具体地址等属性。 3. 畛域事件:• 订单创立事件:当用户下单时触发该事件,蕴含订单信息、商品信息等数据。 • 订单领取事件:当用户实现领取时触发该事件,蕴含订单信息、领取金额等数据。 • 订单发货事件:当商家发货时触发该事件,蕴含订单信息、快递公司、快递单号等数据。 4. 聚合根:• 商品聚合根:蕴含商品实体和相干的值对象,负责商品的创立、批改、查问等操作。 • 订单聚合根:蕴含订单实体和相干的值对象,负责订单的创立、批改、查问等操作。 5. 对外接口服务:• 创立订单接口:用户提交购买申请后,零碎创立相应的订单,并触发订单创立事件。 • 领取订单接口:用户实现领取后,零碎更新订单状态,并触发订单领取事件。 • 发货接口:商家发货后,零碎更新订单状态,并触发订单发货事件。 • 查问订单接口:用户能够依据订单号等条件查问本人的订单信息。 该demo中,商品和订单是两个外围畛域概念,别离由对应的聚合根负责管理。同时,通过定义畛域事件,实现了不同业务场景下的数据更新和告诉。最初,对外提供了一组简略的接口服务,不便零碎的应用和扩大。 demo的java代码实现好了,有了以上咱们对业务场景的充沛分析,确定了子域,接下来咱们该写咱们的代码。 商品实体类:// 省略getter/setter办法public class Product { private Long id; private String name; private BigDecimal price; private Integer stock;}2. 订单实体类 // 省略getter/setter办法public class Order { private Long id; private LocalDateTime createTime; private Integer status; private List orderItems;}3. 订单项实体类 ...

April 27, 2023 · 2 min · jiezi

关于ddd:04期领域驱动设计与微服务

这里记录的是学习分享内容,文章保护在 Github:studeyang/leanrning-share。 如何了解畛域驱动设计?随着微服务的衰亡,你肯定据说过畛域驱动设计 DDD(domain-driven design),然而如果把它当成一个术语来看,仿佛有点形象。这到底是个什么玩意? 别急,你必定还据说过测试驱动开发(TDD, Test-driven development)吧? 这是个什么概念呢?就是说开发的过程中要测试后行,提倡先写测试程序,而后编码实现。开发是目标,测试是辅助,所以叫做测试-驱动-开发,咱们应该把它拆成 3 个术语来了解。 所以,对于畛域驱动设计,设计是目标,畛域才是辅助。想要设计一个软件,然而因为业务太过简单,设计过程难以进行。这时,应用畛域的思维来辅助设计。 微服务应该拆多小?如果你是业务架构师,你在设计过程中会遇到哪些难题呢?我想你面临的第一个问题就是:微服务到底应该拆多小? 有人说:“微服务嘛,就是要越小越好!” 这时运维可能要跳进去打你了,微服务如果拆分适度,导致我的项目简单度过高,不仅运维保护这些服务消耗人力,太小的微服务也占用了资源。 那是否有适合的实践或设计办法来领导微服务设计呢? 答案就是 DDD。 DDD 是一种解决简单畛域的设计思维,包含两局部,策略设计和战术设计。策略设计就是辅助建设业务畛域模型,划分畛域边界,建设限界上下文(DDD 的专业术语,下文会解释)。 战术设计则从技术视角登程,侧重于畛域模型的技术实现,实现软件开发和落地,包含微服务代码架构模型的设计和实现。 DDD 思维是如何领导微服务拆分的呢?能够分为三步: 第一步,列举业务场景,找出畛域实体对象。 第二步,依据畛域实体间的业务关联,将相干的实体组合造成聚合。它们属于同一个微服务。 第三步,依据语义边界,将多个聚合划定在一个限界上下文内,造成畛域模型。这一层边界就是微服务的边界。 DDD 畛域的思维在钻研简单畛域问题时,DDD 会按肯定的规定将业务畛域进行细分,这跟自然科学的钻研办法相似。 当人们在自然科学钻研中遇到简单问题时,通常的做法就是将问题按肯定的规定进行细分,再针对细分进去的问题子域一一深入研究,当所有问题子域实现钻研时,咱们就建设了全副畛域的残缺常识体系了。 举个例子:如果咱们要钻研一颗桃树。依照器官的不同分为营养器官和生殖器官,对营养器官进一步细分,分为叶,茎、根,对生殖器官进一步分为花、果实、种子。 对器官进一步细分,将器官分为组织。对组织进一步细分,将组织细分为细胞。细胞就是咱们要钻研的最小单元。细胞之间的细胞壁确定了单元的边界,也确定了钻研的最小边界。 子域将桃树细分成了六个子域:根、茎、叶,花、果实、种子。子域再依照重要水平进行划分,分为外围域、通用域、撑持域。 决定产品和公司外围竞争力的子域是外围域;没有太多个性化的诉求,同时被多个子域应用的是通用域;既不蕴含决定产品和公司外围竞争力的性能,也不蕴含通用性能的子域,它就是撑持域。 须要留神的是,外围域要依据公司的倒退策略及业务的理论状况来确定。 举例来说,如果这颗桃树的客人是一名园丁,那他关注的就是桃花盛开,春色满园,所以花就是外围域。如果这颗桃树的客人是一名果农,那他关注的就是桃子品质、产量,所以果实就是外围域。 限界上下文咱们晓得语言都有它的语义环境,为了防止同样的概念或语义在不同的上下文环境中产生歧义,DDD 在策略设计上提出了“限界上下文”这个概念,用来确定语义所在的畛域边界。 举个例子:下图中的两个账户,光凭名字咱们根本无法辨别,只有通过它们所在的限界上下文咱们能力看出它们之间的区别。 再比方,电商畛域的商品在不同的阶段有不同的术语,在销售阶段是商品,而在运输阶段则变成了货物。同样的一个货色,因为业务畛域的不同,赋予了这些术语不同的涵义和职责边界。 一个限界上下文就能够拆分为一个微服务,这个边界使得一个概念在这个边界内没有二义性。 实体总结来说有四种状态。 第一,实体的业务状态:在策略设计时,畛域模型中的实体是多个属性、操作或行为的载体。 第二,实体的代码状态:在代码模型中,实体的表现形式是实体类,这个类蕴含了实体的属性和办法,以及外围业务逻辑。 DDD 强调“设计即代码”。对于“注射流感疫苗”这个业务用例,当团队探讨到业务模型时,他们会说:“护士给病人注射规范剂量的流感疫苗。” 传统代码的表现形式是这样的: public void shot() { patient.setShotType(ShotTypes.TYPE_FLU); patient.setDose(dose); patient.setNurse(nurse);}DDD 思维的代码表现形式是: public void shot() { Vaccine vaccine = vaccines.standardAdultFluDose(); nurse.administerFluVaccine(patient, vaccine);}很显著,第二类代码更容易了解的多。 ...

March 20, 2023 · 1 min · jiezi

关于ddd:一条卡券系统领域建模实践

分享一下组内小伙伴雷羽、张聪在优惠券零碎上落地领域建模的总结 一、引入领域建模的起因模型转换: 将数据库模型转换为实体模型,最终操作实体模型长久化到数据库保留(一个id即可示意有且仅有一张优惠券)行为内聚:应用DDD畛域实体模型,使优惠券相干行为内聚到实体(查问、支付、折扣计算、主动选券、应用)面向扩大:优惠券品种繁多,却又大同小异,针对不同类型优惠券实现根底函数(满减券、津贴、折扣券、抵扣券)向上弥合数据源差别,暗藏实现细节,比方依据不同的用户发放个性化的优惠券(不同的人面额门槛适用范围不一样),通过对立的畛域模型暗藏掉该种券在应用等方面的差别;模型灵便转换实现复用,比方在劝buy会员等预支场景,咱们通过预支付优惠券(理论用户并没有该优惠券)将Coupon模型转化为UserCoupon模型,来实现优惠计算的复用;二、实体设计 优惠券:解决用户支付问题根底优惠券畛域实体Coupon,提供优惠券所有根底能力(蕴含查问优惠券信息出参转换、支付、预支付、对内提供的优惠金额计算能力)聚焦不同类型券重写优惠金额计算(折扣券、抵扣券、津贴、满减券),在诸如商详、劝buy会员等用户未取得该券的场景下,通过Coupon.receiveInAdvance将模型转换为UserCoupon,由UserCoupon对立承当简单的应用规定验证,仅撑持外围的金额计算局部性能;用户优惠券:对外提供优惠计算、生产、偿还等能力根底用户优惠券UserCoupon,提供用户优惠券所有根底能力(蕴含查问用户优惠券信息出参转换、计算优惠金额、主动选券、生产、退还);用户满减券、用户折扣券、用户抵扣券、用户津贴针对不同类型券实现优惠金额计算、选取可用sku;暗藏千人一券、千人千券的不同结构过程,暗藏实在与预支付优惠券的能力差异,实现模型的高度复用;兑换券支付兑换券(援用可兑换的优惠券Coupon实体)缓存与序列化,因为Voucher引入Coupon实体,Coupon实体自身曾经缓存,所以Voucher的缓存交由SerializableVoucher,仅长久化couponId;用户兑换券生产能力:兑换商品、兑换优惠券应用(援用Voucher实体)兑换后应用同UserCoupon  三、外围代码支付条件:通过不同维度如(渠道、用户身份、支付工夫、支付形式、支付数量)等校验应用条件:通过不同维度如(门槛、用户身份、应用对象spu、应用工夫、应用平台/形式、订单限度)等校验预领券:VIP劝Buy、宠粉券计算优惠等,提前给满足客观条件的用户预领抵扣主动选券:通过优惠券优惠金额、过期工夫、门槛、优惠券类型等条件帮用户主动选出最为适合的券四、主线流程 领券用券退还

September 29, 2022 · 1 min · jiezi

关于ddd:DDD概念复杂难懂实际落地如何设计代码实现模型

明天我想与你聊一聊,DDD概念简单、难懂,理论落地该怎么设计代码实现模型。对于这个话题,先说说整体框架、思路,我打算联合两局部分享给你,每一部分,置信认真看完,都会或多或少有所播种。以下内容,预计1分钟左右可疾速看完: 前一部分,办法篇,旨在具体介绍DDD所蕴含的几个外围概念,以及围绕这些概念所构建的DDD代码实现模型的组成构造。 后半局部,实际篇,进一步思考。我持续接着说,承接后面的内容,要想让这些代码实现模型真正落地,咱们须要把它们与具体的利用场景联合起来。我将偏重具体论述DDD代码实现模型的设计办法,并给出一个具体的案例剖析。 随同着业务零碎复杂度的一直晋升,以及微服务架构等分布式技术体系的大行其道,畛域驱动设计(Domain Driven Design,DDD),日渐成为零碎建模畛域的支流设计思维和模式。在DDD中,引入了限界上下文、聚合、实体、值对象、畛域事件、资源库、应用服务等一系列外围概念。 通过这些概念,开发人员能够发展零碎设计和实现工作。然而,DDD中的这些概念绝对都比拟形象,甚至有些艰涩难懂。再往相通或相似问题点上靠,我认为本质上对于简单难懂的概念的了解和把握,咱们一开始不用过于纠结这些概念自身,而是能够把它们与事实中的具体实现模型对应起来。 通过两者之间的正当映射,来促成对概念自身的了解,如下图所示。这里多说一句,即使你是其余技术畛域的敌人,或者也曾遇到过相似问题,并有着共通性。心愿看完明天的分享,能够或多或少帮忙到你,并有所启发、思考。 在上图中,咱们一方面尝试把简单概念映射到实现模型。另一方面,基于对实现模型的把握,能够反推对简单概念的了解水平,从而更好地把握这些概念。这也更足以见得实际能力出真知,也只有设计过实现模型,能力真正把握这些概念,从而把它们利用到各种具体的场景中。 这是一种卓有成效的方法。 那么问题就来了,在日常开发过程中,如何确保DDD真正落地,把这些抽象概念转为具体代码模式,是咱们明天要探讨的内容。 01⎪ 想设计代码实现模型,咱非得理解DDD中这几个外围概念? 总体来说,DDD提供的是一种开展业务建模和软件设计的方法论。DDD认为良好的零碎架构,应该是技术架构和业务架构互相交融的后果,开发人员不能脱离业务畛域来设计技术架构。为了实现这一指标,DDD提出了一组外围概念,如图1所示。 咱们先来看第一个外围概念,就比拟难于了解,即限界上下文(Boundary Context)。在DDD中,当咱们把业务畛域拆分成多个子域之后,限界上下文明确了子域的业务界线,并实现子域与子域之间的隔离,如图2所示。 有了限界上下文,咱们就须要围绕业务场景设计畛域模型对象(Domain Model Object)。畛域模型对象,蕴含了丰盛的业务逻辑和操作行为,这点和只蕴含数据属性的传统数据对象,有本质区别。 因而,畛域模型对象是咱们在利用DDD时,最应该关注的一组对象,也是最难把握的一组对象。 在DDD中,畛域模型对象包含三大类,即聚合(Aggregate)、实体(Entity)和值对象(Value Object),这三类对象各有特点。 相较畛域模型对象,畛域事件诞生较晚,但也是畛域模型的一个重要组成部分,因为事实中很多场景,都能够形象成事件(Event),如图4。 在DDD中,通过畛域事件能够实现业务状态变动的无效流传,并在单个限界上下文外部或在多个上下文之间,对这些状态变动做出响应。 业务畛域中的各种状态变动最终都须要进行存储。为此,DDD提供了一个针对业务数据的对立拜访入口,这就是资源库(Repository)。通过资源库,咱们能够实现对各种畛域对象的长久化操作,如图5所示。 最初,咱们来引入应用服务的概念。应用服务包含命令(Command)服务和查问(Query)服务两大类,实质上起到的是一种解耦和协调作用,确保各种畛域模型对象之间的交互和合作。因而,在波及到多个限界上下文之间的交互时,咱们须要重点关注应用服务。如图6所示。 02⎪ 概念简单又难懂,想理论业务场景下真正落地,需引入DDD代码实现模型 对于DDD中的外围概念,我就简略介绍到这里,下一步就是要探讨一个所有开发人员都必须面对的话题,即如何将这些简单难懂的概念,在事实的开发过程中可能真正落地?这就须要引入DDD代码实现模型。 ▶︎ 要想设计代码实现模型,先得搞清楚它有哪几局部组成? 无论设计办法有多好,可能转换为可运行的代码才是王道,这点对于DDD而言尤为如此。 惋惜的是,目前业界对于如何施行这些概念,并没有一套对立的规范和标准,这就导致咱们在具体的开发过程中,经常感到无从下手。为此,本文专门提炼了一整套DDD代码实现模型。接下来,让咱们从DDD代码实现模型的基本概念和组织构造展开讨论。 ▶︎ 在讲代码实现模型之前,先弄清楚什么是实现模型 说起模型(Model),业界支流的方法论认为存在三种不同的类型,即畛域模型、设计模型和代码模型,如图7所示。 对于畛域模型,咱们在后面的内容中曾经做了介绍。在DDD中,聚合、实体、值对象、畛域事件等,都能够归属到这一模型的领域。 而设计模型(Design Model),能够分成边界模型和外部模型两个组成部分。边界模型明确零碎边界,形象系统集成和交互计划。而外部模型细化边界模型,在明确零碎边界的前提下,实现零碎外部模块和组件的形象和构建。因而,在DDD中,咱们往往从限界上下文的角度登程,来发展设计模型的建设,如下图所示。 最初,代码模型为事实世界的解决方案,提供可执行的零碎环境。咱们能够通过在畛域模型和设计模型中嵌入代码的形式来构建代码模型,该模型是将DDD各个简单概念转换为可执行代码的关键所在,也是咱们明天要探讨的次要内容。 显然,畛域模型、设计模型和代码模型之间,存在一种档次依赖关系,如图9所示。 首先,畛域模型代表畛域的固有业务; 设计模型指向畛域模型,关注对外部接口的承诺以及交互关系; 代码模型提供了残缺实现过程,是对设计模型的细化。 正是通过这三种模型的整合,实现了从事实问题到最终可能落地的实现计划的演进。 ▶︎ DDD代码实现模型,应蕴含哪些局部? 针对DDD代码实现模型的探讨,咱们也将遵循上述三种模型的整合过程。联合DDD中的各种外围概念,咱们梳理DDD代码实现模型组成构造,如图10所示。 在上图中,咱们能够清晰看到DDD代码实现模型的四个组成部分,别离面向畛域对象、应用服务、基础设施以及上下文集成。讲到这里,你可能会问,为什么咱们要这样设计DDD的代码实现模型呢? 咱们晓得一个残缺的DDD应用程序,通常由多个限界上下文形成。因而,对于代码实现模型而言,咱们须要重点思考两个维度,即: 单个限界上下文实现过程中的代码模型多个限界上下文之间集成过程中的代码模型在上图中,对于畛域对象、应用服务、基础设施代码实现模型的探讨,属于单个限界上下文的领域,而上下文实现代码集成模型,显然面向多个限界上下文,如图11所示。 通过后面内容的学习,置信你对DDD代码实现模型的组成构造,已了然在胸。 那么,在日常开发过程中,咱们应该如何设计这些代码实现模型呢?有没有具体的案例能够参考呢?这几个问题点,你能够先停下来推敲下。 03⎪ 如何设计DDD代码实现模型? 在剖析DDD代码实现模型时,对于上一篇提到的四个组成部分,咱们须要梳理它们的代码构造和依赖关系。针对代码构造,咱们须要明确代码包的组成,以及外部所蕴含的技术组件。 在明确了包构造之后,依赖关系指的是咱们须要进一步明确这些代码包和技术组件之间的交互关系。基于这两点,让咱们先来探讨畛域对象的代码实现模型。 ...

June 24, 2022 · 1 min · jiezi

关于ddd:领域驱动设计实践支付系统建模

在Airwallex,畛域驱动设计(DDD)办法被用来领导如何对简单的业务问题和零碎设计进行建模。 在这篇博客中,咱们试图全面介绍用DDD模式对领取零碎进行建模的做法。 简介领取零碎是一个相当简单和多变的零碎,从订单、欺诈、告诉、与各种领取形式的整合到资金清理和结算,涉及面很广。 在解决一个简单的零碎时,大多数开发人员可能会遇到一些问题 边界和责任不明确,只是一个有许多模型和业务逻辑的大应用程序。没有隔离和模块化:简单的业务工作流和流程是混合的,难以扩大。没有关注点的拆散:外围业务逻辑与技术实现细节混在一起。软件行业中的许多设计模式都能解决这些问题,在Airwallex,咱们尝试采纳畛域驱动设计(DDD)的办法来为咱们的领取零碎建模,以管理系统设计中的复杂性。 什么是DDD畛域驱动设计(DDD)是由埃里克-埃文斯(Eric Evans)提出的,它是一套思维、准则和模式,有助于依据业务畛域的根底模型设计软件系统。DDD有两个不同的空间:问题空间和解决方案空间。 在问题空间,你是用策略模式来定义零碎的大规模构造,它专一于剖析一个畛域、子畛域和泛在语言。 而在解决方案空间中,采纳战术模式来提供一套设计模式,你能够用它来创立畛域模型。这些模式包含有界的上下文、上下文映射、实体、聚合体、畛域事件、畛域服务、应用服务和基础设施。这些战术模式将帮忙你设计既涣散耦合又有凝聚力的微服务。 如何在实践中利用DDD设想一下,有这样一个场景: 一位顾客想在商家的网站上购买一件T恤,价格是10美元。顾客能够用各种领取形式来领取这件T恤,如Visa卡或微信钱包。客户付款后,商家能够从领取网关取得告诉,这样他们就能够向客户展现付款胜利的页面。商户能够在Airwallex Webapp中查看付款详情,这样他们就能够晓得这件T恤能够取得多少资金,Airwallex扣除了多少费用,以及资金何时会被结算到他的Airwallex钱包。将遵循以下步骤,利用DDD对基于上述场景的领取零碎进行建模。 剖析事实世界中的业务用例,以取得问题空间中的域和子域。通常,在这个阶段,Event Storming是一个很好的工具。定义解决方案空间中的有界上下文在有界线的上下文中,利用战术性DDD模式来定义实体、聚合、畛域服务、畛域事件等。应用上一步的后果来确定你的团队中的微服务。以下是剖析后果。 问题空间畛域领取零碎 子域- 领取解决:商家能够通过各种领取形式承受客户的付款- **金融:对商家的领取资金进行清理和结算。 通用语言在与领域专家探讨后,以下是所有团队承受的通用语言。 - 领取用意:商家创立的订单,指定价格、产品、客户等。 - 付款希图:商家创立的交易,以承受客户对特定订单的付款。 - 付款形式:客户为产品或服务付款的形式。 - 付款结算:一批结算到商家钱包的付款。 - 付款视图:一个聚合的付款细节视图,蕴含与一个付款无关的所有数据。 解决方案空间有界上下文有界上下文(BC)限定了一个畛域模型的范畴。从问题空间的剖析后果来看,咱们能够定义以下有界上下文。 - 领取网关:API网关,为商户提供牢靠的API,以创立或查看付款。 - 领取外围:领取用意、尝试、办法资源管理。 - 领取适配器:与一个内部PSP(微信/支付宝/Visa/Mastercard等)集成。 - 领取结算:为商户计算和结算每笔领取的准则和费用。 - 领取交融:领取细节的聚合视图。 而上下文地图将是这样的: 畛域模型从下面咱们剖析的场景和无所不在的语言中,咱们能够确定以下聚合、实体、价值对象和畛域事件。 畛域服务在咱们的实际中,域服务是为一个聚合体提供的无状态业务逻辑服务,遵循繁多责任模式。通常状况下,咱们会在畛域服务中封装畛域仓库、聚合变动和畛域事件公布。以PaymentAttemptExecutorService为例。 畛域事件畛域事件能够使零碎更具可扩展性,并防止任何耦合--一个聚合体不应该决定其余聚合体应该做什么,以及工夫耦合--付款的胜利实现并不取决于所有过程在同一时间可用。 例如,当PaymentCaptureCommand将领取状态改为已领取时,畛域事件PaymentAttemptCapturedEvent被发送,以告诉聚合的PaymentAttempt被捕捉。在PaymentAttemptCapturedEvent的畛域事件处理程序中,咱们能够把副作用放在业务逻辑上,比方告诉领取交融的边界上下文来更新领取细节和领取结算的边界上下文来计算结算金额和费用。 基础设施在DDD模式中,基础设施层被用来将外围业务畛域与技术实现细节离开。通常,该层采纳反污层(ACL)模式。以畛域存储库为例。 畛域仓库只定义了接口,比方他们能做什么,但实现细节应该暗藏在基础设施层外面,比方应用PostgreSQL或MongoDB来保留数据。例如,在基础设施层,PaymentAttemptPgRepository是基于PostgreSQL的具体实现,toPO是用于将域对象PaymentAttempt转换为长久化对象的映射器。 因而,在畛域层,咱们只关注畛域模型,它与基础设施技术齐全脱钩。当基础设施层有任何变动时,不须要在畛域层中进行扭转。 从畛域模型到微服务当初,咱们曾经为领取零碎定义了一组有边界的上下文,并在每个有边界的上下文中确定了一组实体、集合体和畛域事件服务。 下一步就是要从畛域模型到利用微服务的设计。 在这里,咱们抉择将一个有界上下文映射到一个微服务。 论断在这篇博客中,当咱们试图对领取零碎进行建模时,咱们涉及了畛域驱动设计(DDD)模式的各种概念和策略。采纳DDD能够提供许多益处,例如,在所有的团队中进行清晰的沟通,以及在设计零碎时提供一个成熟的模式来治理复杂性和提供更好的可扩展性。 有了无处不在的语言,咱们能够实现更多的自我形容的类名和函数名。通过聚合模式,咱们能够实现清晰的边界和繁多的责任。通过畛域事件模式,咱们能够将外围业务流程与聚合体上的副作用离开。通过基础设施层和ACL模式,咱们能够将外围业务畛域模型与技术实现细节离开。 通过有边界的上下文模式,咱们能够推导出潜在的微服务候选人。DDD模式是一个宏大的话题,我认为咱们做得还不够充沛,无奈全面解释它们,但咱们想介绍一些要害的话题和咱们实际该模式的教训。在将来,咱们将持续深入研究DDD模式中的每一个主题,如层治理、畛域事件存储、上下文映射模式等。

May 11, 2022 · 1 min · jiezi

关于ddd:领域驱动设计入门与实践-下

上期对 DDD 进行了简略的介绍,Phone Number 案例也使咱们对 DDD 有了进一步的理解。 通过该案例,咱们理解到 PhoneNumber 蕴含了初始化、校验、属性解决等多种逻辑,而传统的 POJO 类只蕴含其属性的getter setter办法。这是 DDD 和传统面向数据开发的重要差别点。 「PhoneNumber-充血模型」与「POJO 类-贫血模型」不难理解,笔者在「畛域驱动设计入门与实际-上」中已对其做过介绍。难的是在理论我的项目中,若应用充血模型,如何把握好其强弱水平须要很丰盛的教训。 DP(Domain Primtive)咱们将PhoneNumber这种类型称为 DP- Domain Primitive。类比 Integer、String 在 Java 编程语言中一样,DP 是 DDD 里所有模型、办法、架构的根底。 定义 DP:在 DDD 里,DP 能够说是所有模型、办法、架构的根底,它是在特定畛域,领有精准定义、可自我验证、领有行为的对象,可认为是畛域的最小组成部分。 应用 DP 的三条准则:将隐性概念显性化/Make Implicit Concepts Expecit在Phone Number这个案例中,若应用 String 类型来定义电话号码,则「归属地编号」、「运营商编号」这些属于电话号码的隐性属性就难以体现进去,咱们通过自定义类型PhoneNumber,通过赋予它行为来显性化了这两个概念,让代码的业务语义更加明确。这里咱们通过一个例子来阐明: 假如当初要实现一个性能: 使 A 用户能够领取 x 元给用户 B,可能的实现如下: public void pay(BigDecimal money, Long recipientId) { bankService.transfer(money, "CNY", recipientId); }如果这个是境内转账,并且境内的货币永远不变,该办法仿佛没啥问题。一旦货币变更或做跨境转账,该办法留有显著的 bug,因为 Money 对应的不肯定是 CNY。 在这个 case 里,当咱们说“领取 x 元”时,除了 x 自身的数字外,还有一个隐含的概念「元」。 ...

April 7, 2022 · 5 min · jiezi

关于ddd:领域驱动设计入门与实践上

编者按: 软件工程师所做的事件就是把事实中的事件搬到计算机上,通过信息化进步生产力。在这个过程中有一个点是不能被忽视的,那就是[零碎的内建品质] 设计良好的零碎: 概念清晰,结构合理,即便代码库宏大,仍然可了解、可保护; 设计蹩脚的零碎: “屎上雕花”。 其中,畛域概念和畛域模型的缺失是造成这种差别的罪魁祸首。 概念解读畛域驱动设计 - DDD(Domain-Driven Design)是一种基于畛域常识来解决简单业务问题的软件开发方法论,其本质是将业务上要做的一件小事,通过推演和形象,拆分成多个内聚的畛域。 它有以下三个重点: 跟领域专家(Domain Expert)密切合作来定义出 Domain 的范畴及解决方案切分畛域出数个子畛域,并专一在外围子畛域透过一系列设计模式,将畛域常识转换成对应的程序模型(Model)畛域可大可小,对应着大小业务问题的边界,对边界的划分与管制是畛域驱动设计强调的核心思想。 DDD 的扭转向 Anemic Model 说 “No!” 跟大家介绍一个有名的反模式:贫血模型(Anemic Model)。此模式泛指那些只有 getter 与 setter 的 model。这些 model 不足行为表述,导致使用者每次都要本人组合出想要的性能。 “贫血模型应用起来像在教小孩子一样,一个指令一个动作还很容易忘掉;具备行为表述能力的模型则像跟小孩儿沟通一样,一次口头就能实现许多指令。” 举个栗子:以数据为核心的形式是要求客户代码必须晓得如何正确地将一个待定项提交到冲刺中。此时,谬误地批改 sprintId 或有另外一个属性须要设值,都要求开发人员认真剖析客户代码来实现从客户数据到 BacklogItem 属性的映射。这样的模型不是畛域模型。 public class BacklogItem extends Entity { private SprintId sprintId; private BacklogItemStatusType status; ... public void setSprintId(SprintId sprintId) { this.sprintId = sprintId; } public void setStatus(BacklogItemStatusType status) { this.status = status; } ...}// 客户端通过设置sprintId和status将一个BacklogItem提交到Sprint中backlogItem.setSprintId(sprintId);backlogItem.setStatus(BacklogItemStatusType.COMMITTED);通过业务语言封装程序行为 ...

March 29, 2022 · 1 min · jiezi

关于ddd:DDD领域驱动设计思想解读及优秀实践吾爱fen享

前言:download:DDD(畛域驱动设计)思维解读及优良实际 下载课程ZY:https://www.97yrbl.com/t-1317.html开发环境jdk1.8【jdk1.7以下只能局部反对netty】springboot 2.0.6.RELEASEidea + maven 五、代码示例itstack-demo-ddd-01└── src ├── main │ ├── java │ │ └── org.itstack.demo │ │ ├── application │ │ │ ├── event │ │ │ │ └── ApplicationRunner.java │ │ │ └── service │ │ │ └── UserService.java │ │ ├── domain │ │ │ ├── model │ │ │ │ ├── aggregates │ │ │ │ │ └── UserRichInfo.java │ │ │ │ └── vo │ │ │ │ ├── UserInfo.java │ │ │ │ └── UserSchool.java │ │ │ ├── repository │ │ │ │ └── IuserRepository.java │ │ │ └── service │ │ │ └── UserServiceImpl.java │ │ ├── infrastructure │ │ │ ├── dao │ │ │ │ ├── impl │ │ │ │ │ └── UserDaoImpl.java │ │ │ │ └── UserDao.java │ │ │ ├── po │ │ │ │ └── UserEntity.java │ │ │ ├── repository │ │ │ │ ├── mysql │ │ │ │ │ └── UserMysqlRepository.java │ │ │ │ ├── redis │ │ │ │ │ └── UserRedisRepository.java │ │ │ │ └── UserRepository.java │ │ │ └── util │ │ │ └── RdisUtil.java │ │ ├── interfaces │ │ │ ├── dto │ │ │ │ └── UserInfoDto.java │ │ │ └── facade │ │ │ └── DDDController.java │ │ └── DDDApplication.java │ ├── resources │ │ └── application.yml │ └── webapp │ └── WEB-INF │ └── index.jsp └── test └── java └── org.itstack.demo.test └── ApiTest.javaapplication/UserService.java | 应用层用户服务,畛域层服务做具体实现/** * 应用层用户服务 * 虫洞栈:https://bugstack.cn * 公众号:bugstack虫洞栈 | 欢送关注并获取更多专题案例源码 * Create by fuzhengwei on @2019 */public interface UserService { UserRichInfo queryUserInfoById(Long id);}domain/repository/IuserRepository.java | 畛域层资源库,由根底层实现/** ...

March 27, 2022 · 3 min · jiezi

关于ddd:从MVC到DDD的架构演进

DDD这几年越来越火,材料也很多,大部分的材料都偏差于实践介绍,有给出的代码与传统MVC的三层架构差别较大,再加上大量的新概念很容易让初学者望而生畏。本文从MVC架构角度来解说如何演进到DDD架构。 从DDD的角度看MVC架构的问题代码角度: 瘦实体模型:只起到数据类的作用,业务逻辑散落到service,可维护性越来越差;面向数据库表编程,而非模型编程;实体类之间的关系是简单的网状结构,成为大泥球,牵一发而动全身,导致不敢轻易改代码;service类承接的所有的业务逻辑,越来越臃肿,很容易呈现几千行的service类;对外接口间接裸露实体模型,导致不必要凋谢外部逻辑对外裸露,就算有DTO类个别也是实体类的间接copy;内部依赖层间接从service层调用,字段转换、异样解决大量充斥在service办法中;项目管理角度: 交付效率:越来越低;稳定性差:不好测试,代码改变的影响范畴不好预估;了解老本高:新成员染指老本高,长期会导致模块只有一个人最相熟,到职老本很大;第一层:老成持重以上的问题越来越重大,很多人开始把眼光转向DDD,于是埋头啃了几本大部头的书,对以下概念有了根本的理解: 对立语言限界上下文畛域、子域、撑持域聚合、实体、值对象分层:用户接口层、应用层、畛域层、根底层于是把MVC架构进行了革新,演进成DDD的分层架构。 DDD分层架构: MVC架构到DDD分层架构的映射: 至此,算了根本入门了DDD架构,扩展性也失去了肯定的晋升。不过随着业务的倒退,一直冒出新的问题: 一段业务逻辑代码,到底应该放到应用层还是畛域层?畛域服务当成原来的MVC中的service层,随着业务一直倒退,类也在一直收缩,如同还是老样子啊?聚合蕴含多个实体类,这个接口用不到这么多实体,为了性能还是间接写个SQL返回必要的操作吧,不过这样貌似又回到了MVC模式既然实体类能够蕴含业务逻辑、畛域服务也能够放业务逻辑,那到底放哪里?材料上说畛域层不能有内部依赖,要做到100%单测笼罩,可是我的畛域服务中须要用到内部接口、地方缓存等等,那这不就有了内部依赖了吗?第二层:草船借箭(战术设计)带着问题一直学习别人教训,并一直的尝试,逐步get到以下技能: 1、畛域层畛域(domain)是个模块,蕴含以下组成部分,传统的service按性能可能拆分到任何一个中央,各司其职。 1个聚合1到多个实体若干值对象多个DomainService1个Factory:新建聚合1个Repository:聚合仓储服务聚合根(AggregateRoot)聚合自身也是一个实体,聚合能够蕴含其余实体,其余实体不能脱离聚合而独自提供服务,比方一篇文章下的评论,评论必须从属于文章,没有文章也就没有评论。仓库层(repository)也必须是以聚合为外围提供服务的; 实体:能够了解为一张数据库表,必须有主键; 值对象:没有主键,依附于实体而存在,比方用户实体下住址对象,个别在数据库中已json字符串的模式存在;最常见的值对象是枚举; 仓库服务(repository)资源库是聚合的仓储机制,内部世界通过资源库,而且只能通过资源库来实现对聚合的拜访。资源库以聚合的整体治理对象。因而,一个聚合只能有一个资源库对象,那就是以聚合根命名的资源库。除此之外的其余对象,都不应该提供资源库对象。仓储服务的实现个别有Spring Data JPA、Mybatis两种形式。 如果是用Spring Data JPA实现,间接应用JPA注解@OneToOne、@OneToMany,配合fetch配置,即可一个办法查问出所有的关联实体。 如果是用Mybatis实现,那么repository须要退出多个mapper的援用,再手动做拼装。 这里有一个经典的Hibernate笛卡尔积问题,答案是在聚合根中,个别不会加在大量的关联实体对象。如果的确须要查问关联对象而关联对象又比拟多怎么办呢?在DDD中有一个CQRS(Command-Query Responsibility Segregation)模式,是一种读写拆散模式,在此场景中须要将查问操作放到查问命令中分页查问。 当然CQRS也是一个很简单模式,不应照搬别人计划,而是依据本人的业务场景抉择适宜本人的计划,以下列举了CQRS的几种利用模式: 工厂服务(factory)作用是创立聚合,只传入必要的参数,工厂服务外部暗藏简单的创立逻辑。简略的聚合能够间接通过new、静态方法等创立,不是必须由factory创立。 畛域服务单个实体对象能解决的逻辑放到实体里,多个实体或有交互的场景放到畛域服务里。 畛域服务可不可以调用仓储层或内部接口? 能够,但不能间接和畛域服务代码放一起,畛域服务模块寄存API,实现放根底层(infrastructure)。 畛域服务对象不倡议间接以聚合名+DomainService命名,而要以操作命令关联,比方用户保留服务命名为:UserSaveService, 审核服务:UserAuditSerivce。 2、应用层应用层通过应用服务接口来裸露零碎的全副性能。在应用服务的实现中,它负责编排和转发,它将要实现的性能委托给一个或多个畛域对象来实现,它自身只负责解决业务用例的执行程序以及后果的拼装。通过这样一种形式,它暗藏了畛域层的复杂性及其外部实现机制。 比方下订单服务的办法: public void submitOrder(Long orderId) { Order order = OrderFetchService.fetchById(orderId); //获取订单对象 OrderCheckSerivce.check(order); //验证订单是否无效 OrderSubmitSerivce.submit(order); //提交订单 ShoppingCartClearService.clear(order); //移除购物车中已购商品 NotifySerivce.emailNotify(order.getUser()); //发送邮件告诉买家}对于简单的业务来说,应用层也有几种模式: 编排服务:最典型比方Drools;Command、Query命令模式;业务按Rhase、Step逐层拆分模式; 3、Maven模块划分根底层是比较简单一层,不过这里还有个比拟纳闷的问题:依照DDD的四层架构图去划分Maven模块,根底层是最上的一层,然而根底层也要蕴含根底组件供其余层应用,这时根底层应该是放到最上层,间接依照这样构建Maven模块会造成循环依赖。 相比来说,另一个架构图更精确一些,不过仍然没有直观体现Maven模块如何划分。 我的最佳实际是将根底层拆分两局部,一部分是根底的组件+仓储API,一部分是实现,maven模块划分图如下所示: 第三层:指挥若定(策略设计)通过以上的两层的磨炼,祝贺你把DDD战术都学习完了,应酬日常的代码开发也够了,不过作为架构师来说,摸索的路线还不能止步于此,接下来会DDD策略局部。策略局部关注点有3个: 对立语言畛域限界上下文1、对立语言对立语言的重要性能够依据Jeff Patton 在《用户故事地图》中给出的一副漫画来直观的形容: 对立语言是提炼畛域常识的输入后果,也是进行后续需要迭代及重构的根底,对立语言的建设有以下几个要点: 对立语言必须以文档的模式提供进去,并且在整个项目组的各团队达成共识;对立语言必须每个中文名有对应的英文名,并且在整个技术栈保持一致;对立语言必须是残缺的,蕴含以下因素: 畛域模型的概念与逻辑;界线上下文(Bounded Context);零碎隐喻;职责的分层;模式(patterns)与习用法。2、畛域划分以事件风暴的模式(Event Storming),列出所有的用户故事(Use Story),用户故事可通过6W模型来构建,即刻画场景的 Who、What、Why、Where、When 与 hoW 六个因素。而后圈选性能相近的局部,就造成了畛域,畛域又依据职能不同划分为:外围域、撑持域、通用域, ...

February 15, 2022 · 1 min · jiezi

关于ddd:学习-DDD-通用语言的模式

大家好,我是霸戈,这周学习了一些对于畛域驱动设计的常识 ,对比拟粗浅的中央做了不少笔记,分享给大家。 在日常需要探讨的时候,常常会碰到一个需要会议开了一个多小时还没有达成共识。作为业务方(领域专家)明明表白的很分明,然而开发人员却始终无奈了解透彻,很显著的起因就是因为单方的常识体系不统一 ,没有造成一种单方相互都能了解的语言。 语言的鸿沟尽管领域专家对软件开的技术所知无限,但他们相熟应用本人的畛域术语——可能还具备各种不同的格调。另一方面,开发人员可能会用一些描述性的,功能性的术语来了解和探讨零碎,而这些术语并不具备领域专家的语言所要传播的意思。 开发人员可能会创立一些用于反对设计的形象,但领域专家无奈了解这些形象。负责解决不同局部的开发人员可能会开发出各自不同的设计概念以及形容畛域的形式。 因为语言上存在鸿沟,领域专家们只能模糊地形容他们想要的货色。开发人员尽管致力的了解一个本人不相熟的畛域,但也只能造成含糊的意识。 尽管多数团队成员会高法把握这两种语言,但他们会变成信息流的瓶颈,并且他们的翻译也不精确。 互相翻译使模型变得混同在一个没有公共语言的我的项目上,开发人员不得不为领域专家做翻译。而领域专家要充当开发人员与其余领域专家之间的翻译。 这些翻译使模型概念变得混同,而这会导致无害的代码重构。这种间接的沟通覆盖了决裂的造成——不同的团队成员应用不同的术语而尚不自知。 因为软件各个局部不可能浑然一体,因而这就导致无奈开发出牢靠的软件。翻译工作导致各类促成深刻了解的常识和想法无奈联合在一起。 不同语言产生的结果如果语言四分五裂,我的项目必将遭逢重大的问题。领域专家应用他们本人的术语,而技术团队应用的语言则通过调整,以便从设计角度探讨畛域。 日常探讨所应用的术语与代码中应用的术语不统一。甚至同一个人在讲话和写货色时应用的语言也不统一,这导致的结果是,对畛域的粗浅表述经常昙花一现,根本无法记录到代码或者文档 中。 翻译使得沟通不畅,并减弱了常识消化。 然而任何一方的语言都不能成为公共语言,因为它们无奈满足所有的需要。 让畛域模型成为公共语言所有的程序的开销,连带着误会的危险,老本切实太高了。我的项目须要一种公共语言,这种语言要比所有的语言的最小公分母强壮得多。通过团队的统一致力,畛域模型能够成为这种公共语言的外围,同时将团队沟通与软件实现紧密联系在一起。该语言将存在于团队工作中的方方面面。 最小公分母: 就是两个分母的最小公倍数,比如说2和3的最小公倍数是6,那么最小公分母就是6。通用语言的词汇通用语言的词汇包含类和次要的操作名称。语言中的术语,有些是用来探讨模型中曾经明确的规定,还有些则来自施加于模型的的高级组织准则如:限界上下文、上下文映射图。 基于模型的语言开发人员应该应用基于模型的语言来形容零碎中的工件、工作和性能。这个模型应该为开发人员和领域专家提供一种用于互相交换的语言,而且领域专家还应该应用这种语言来探讨需要、开发计划和个性。语言应用得越广泛,了解进行得就越顺畅。 将模型作为语言的支柱。确保团队在外部的所有交换中以及代码中保持应用这种语言,在画图、写货色、特地是讲话时也要应用这种语言。 领域专家应该抵制不适合或无奈充沛表白畛域了解的术语或构造,开发人员应该亲密关注那些将会障碍设计的有歧义和不统一的中央 。 总结在DDD的世界里,不论是作为畛域业余还是开发人员,大家在探讨业务的时候都应该应用单方都能了解的语言。只管在初期这种语言是艰涩难懂的,但随着我的项目的倒退会缓缓渐入佳境。 空有通用语言其实不够,应用口头交换的形式,容易造成常识的失落,也不利于我的项目将来的倒退。该当建设模型,所有的探讨都是基于模型的,任何的的变更都要反映到模型下面。 举荐分享一套家庭理财零碎(附源码)举荐一套开源通用后盾管理系统(附源码)举荐一个酷炫的监控零碎从敌人那里搞了 20 个实战我的项目,速领!举荐一个欠缺的停车管理系统,物联网我的项目springboot,附源码举荐一个互联网企业级别的开源领取零碎一款神仙接私活儿软件,吊到不行!举荐 10 款超实用的企业级开源利用!开放平台 SDK 设计实际!“淘宝” 开放平台接口设计思路Spring中经典的9种设计模式

December 5, 2021 · 1 min · jiezi

关于ddd:CQRS在一条订单系统中的实践三DDD分层协作实践

DDD分层合作 领取订单分层 用户接口层:适配不同终端(gateway api、dubbo api、kafka consumer)生成Command|-com.yit.orders.facade: |-com.yit.orders.facade.OrderPayService:创立OrderPayCommand,交由PayingOrderCommandHandler执行应用服务层:执行来自用户界面层的命令,不实现具体业务。个别过程为,通过畛域仓储拜访聚合,调度聚合的行为函数响应命令,治理聚合对应的事务并通过畛域仓储对聚合进行长久化,通过事件总线公布和订阅聚合产生的畛域事件;|-com.yit.orders.module.paying.application |-command |-com.yit.orders.module.paying.application.command.OrderPayCommand:领取指令 |-com.yit.orders.module.paying.application.command.PayingOrderCommandHandler:领取指令的执行(应用服务,治理多个聚合的事务,领取订单的例子中仅操作一个聚合) |-domainEventHandler |-com.yit.orders.module.paying.application.domainEventHandler.pay.common.OrderPayEventHandler:订阅畛域事件(通过订阅模型进行解耦)畛域层:次要实现畛域模型的外围业务逻辑,体现畛域模型的业务能力。聚合确保业务逻辑的原子性和一致性(聚合内实体的事务边界),产生畛域事件。|-com.yit.orders.module.paying.domain |-aggregate |-com.yit.orders.module.paying.domain.aggregate.IPayingOrderRepository:定义聚合仓储接口(依赖倒置) |-com.yit.orders.module.paying.domain.aggregate.ReadWritePayingNormalOrder:聚合(写模型),内聚业务逻辑,所有的业务扭转都必须通过聚合达成 |-event |-com.yit.orders.module.paying.domain.event.pay.common.OrderPayEvent:畛域事件(值对象,由聚合或实体产生,在应用层进行公布,进行聚合边界之外的通信) |-readonly |-com.yit.orders.module.paying.domain.readonly.ReadonlyPayingOrder:读模型畛域仓储层:|-com.yit.orders.module.paying.infrastructure |-com.yit.orders.module.paying.infrastructure.PayingOrderRepository:实现了畛域层定义畛域仓储接口(通过接口的形式达到外层依赖内层)实现参考:举荐一下MS的这个官网微服务示例我的项目 EShopOnContainers 设计面向 DDD 的微服务 DDD 分层

November 29, 2021 · 1 min · jiezi

关于ddd:学习-DDD-之消化知识

接触到DDD到当初曾经有8个月份了,目前所保护的我的项目也是基于DDD的思维开发的,从一开始的无从下手,到当初熟能生巧,学到不少货色,然而都是一些关键字和零散的常识,同时我也感触到了是因为我对我的项目越来越相熟,游刃有余导致我当初在做需要的时候基本不必过多的去思考,就能很好的实现业务需要,我缓缓的意识到,学习DDD是十分有必要的。 在传统的开发模式中,产品经理在跟业务专家沟通业务需要后,对其进行形象并将后果通过口头或者项目管理工具传达到开发人员,开发人员依据产品经理传递的业务需要机械式地进行性能开发,这样的模式使开发人员没有真正的了解业务原理,开发进去的性能就很难达到业务方的要求,即便达到要求也难以应答将来的业务变动。 如果大家都使用DDD的思维进行开发,就能很好的传递业务知识,因为DDD提倡开发人员、产品经理、畛域业余一起探讨、消化业务知识,彻底了解业务原理。 以下是我在学习DDD时做的一些笔记,并整顿成思维导图的模式,这样就能很好的造成结构化的思维,心愿能让大家对DDD有一个更深的了解。 以下内容局部摘自 《畛域驱动设计》和依据本人的了解整顿而成。1.1 无效建模的因素模型和事实绑定开发人员与产品经理在探讨需要的时候,都会画一些草图和对草图做一些文字说明,这其实就是最后的模型。最后的原型尽管简陋,但它在模型与与实现之间建设了晚期链接,而且在所有的后续迭代中咱们始终在保护该链接。 建设一种基于模型的语言起初领域专家不得不向开发人员解释业务知识开发人员也必须向领域专家解释类图的含意随着我的项目的停顿,单方都可能应用模型中的术语,并将它们组织成合乎模型构造的语句 ,而且能够无需翻译就能互相理解要表白的意思领域专家专一业务畛域,开发人员专一开发,两个不同畛域的人在没有造成对立语言的前提下是很难沟通的,比方电商领域专家抛出订单履约的概念的时候,不得不在解释什么是订单履约时,还得向开发人员翻译外面的各种名词,如果领域专家和开发人员常识对等,就能互相理解各自要表白的意思。 开发一个蕴含丰盛常识的模型对象具备行和为强制性的规定模型并不仅仅是一种数据模型模型应蕴含各种类型的常识提炼模型在模型日趋残缺的过程中,要提炼模型,要将新的概念增加到模型中,同时将不再应用的或者不重要的概念从模型中移除。 头脑风暴和试验语言和草图,再加上头脑风暴,将咱们的探讨变成“模型实验室”在这些探讨中能够演示、尝试和判断上百种变动当团队走查场景时,口头表白自身就能够作为所提议模型的可行性测试,因为人们听到口头表白后,就能立刻分辨出它是表白得分明、简洁还是表白的蠢笨1.2 消化常识高效的领域建模人员是常识的消化者建模人员须要从大量的信息中寻找有有的局部,而后一直的尝试各种信息组织形式,致力寻找对大量信息有意义的常识。 常识消化并非一项孤立的流动个别由开发人员领导下,由开发人员与领域专家组成团队来独特合作。独特收集信息,并通过消化而将它们组织为有用的模式信息的原始资源来自领域专家头脑中的常识、现有用户、以及技术团队在相干遗留零碎或者同畛域其余我的项目中积攒的教训传统瀑布模式的有余业务专家与分析员(产品经理)进行探讨,分析员消化了解这些业务知识后,对其进行形象并将后果传递给程序员,再由程序员编写软件代码。 这种办法齐全没有反馈(程序员没有提供本人的想法)分析员全权负责创立模型,但他们创立的模型只是基于业务专家的倡议分析员没有向程序员学习,得不到晚期版本的教训常识只是朝一个方向流动,不会累积领域专家与开发人同一起消化了解模型的益处在团队所有成员一起消化了解模型的过程中,他们之间的交互也会发生变化。 畛域模型的一直精化迫使开发人员学习重要的业务原理,而不是机械地进行性能开发领域专家被迫提炼本人已晓得的重要常识的过程往往也是欠缺其本身了解的过程,而且他们会慢慢了解软件我的项目所必须的概念严谨性。小结开发人员、分析员、领域专家,都应该将本人的常识输出到模型中,这样模型的组织更紧密,形象也更为整洁。 模型不断改进的同时 ,也成为组织我的项目信息流的工具。模型聚焦于需要剖析,它与编码和设计严密交互。 1.3 继续学习当开始编写软件时,其实咱们所知甚少我的项目常识零散地扩散在很多人和文档中,其中夹杂着其余一些无用的信息,因而咱们甚至不晓得哪些常识是真正须要的常识。 看起来没有什么技术难度的畛域很可能是一种错觉 -- 咱们并没有意识到不晓得的货色到底有多少。这种无知往往会导致咱们做出谬误的判断。 所有的我的项目都会失落常识曾经学到了一些常识的人可能去干别的事了团队因为重组而被拆散,这导致常识又被从新扩散开被外包进来的要害子系统可能只交回了代码,而不会将常识传递回来当应用典型的设计办法时,代码和文档不会以一种有用的模式示意出这些来之不易的常识。因而一但因为某些起因团队成员没有口头传递常识,那么常识就会失落。 高效率的团队须要无意识地积攒常识,并继续学习对于开发人员来说, 这意味着既要欠缺技术常识,也要造就个别的领域建模技巧。但这也包含认真学习他们正在正在从事的特定畛域常识。 那些长于自学的团队成员会成为团队的中坚力量,波及最要害畛域的开发工作要靠他们来攻克。这个外围团队头脑中积攒的常识使他们成为更高效的常识消化者。 1.4 常识丰盛的设计业务流动和规定如同所波及的实体一样,然而畛域的外围,任何畛域都有各种类别的概念。常识消化所产生的模式,可能反映出对常识的深层了解。在模型产生扭转的同时,开发人员对实现进行重构,以便反映出模型的变动,这样,新晓得就被合并到应用程序中了1.5 深层模型有用的模式很少停留在表面上, 随着对畛域和应用程序需要的了解逐渐加深,咱们往往会丢最后看起来很重要的外表元素,或者切换它们的角度。这时,一些开始不可能发现的奇妙形象就会慢慢的浮出水面, 而它们恰好切中问题的要害。 举荐分享一套家庭理财零碎(附源码)举荐一套开源通用后盾管理系统(附源码)举荐一个酷炫的监控零碎从敌人那里搞了 20 个实战我的项目,速领!举荐一个欠缺的停车管理系统,物联网我的项目springboot,附源码举荐一个互联网企业级别的开源领取零碎一款神仙接私活儿软件,吊到不行!举荐 10 款超实用的企业级开源利用!开放平台 SDK 设计实际!“淘宝” 开放平台接口设计思路Spring中经典的9种设计模式

November 28, 2021 · 1 min · jiezi

关于ddd:浅谈-DDD-领域驱动设计

文章简介在B端产品研发及我的项目施行中,DDD带给咱们哪些思考?咱们是如何利用的?本文不是科普贴,旨在分享咱们的经验和思考。 背景Domain Driven Design(简称 DDD),又称为畛域驱动设计,起源于卓越软件建模专家Eric Evans在2003年发表的书籍《DOMAIN-DRINEN DESIGN —TACKLING COMPLEXITY IN THE HEART OF SOFTWARE》(中文译名《畛域驱动设计—软件外围复杂性应答之道》)。随着2014年Martin Flower和James Lewis的《Microservice》出版,微服务概念为业界所承受,DDD也被从新提起。人们发现,DDD里的畛域、限界上下文、畛域模型等理念,和微服务的高内聚、低耦合理念有着人造符合,越来越多的人把DDD作为领导微服务划分的方法论之一,也有越来越多的人认为,把握DDD才是一名优良的架构师。 DDD工作流程 - 图片来自于网络 限界上下文 - 图片来自于网络 为什么咱们要开始DDD?笔者所在团队于2015年左右开始进行公司自主产品的研发,过后,DDD次要利用在咱们的微服务划分、代码分层中,对咱们过后从技术角度登程、搭建对立的微服务技术框架,有着重要的指导意义。而真正开始思考将DDD利用在具体的业务畛域与业务场景中,是在2018年末。那时,咱们面临着自主产品施行的我的项目越来越多、须要帮忙客户从0到1构建业务利用的状况,如何在企业甚至行业的简单业务场景中找到适合的架构计划,是过后咱们冀望借助DDD去更体系化答复的问题。 咱们是如何利用DDD的?在DDD的实际中,大多数人应该都会面临一个问题:名词太多!“畛域”、“子域”、“限界上下文”、“聚合”、“实体”、“值对象”等等名词,令人目迷五色。并且,DDD的实践尽管通知了咱们名词的定义和解决准则,然而具体场景之下,到底如何基于准则去剖析,如同并没有标准答案。 这也是咱们在利用DDD时的最大窘境,咱们也在重复思考:如何在团队内对立语言、如何把DDD准则融入到具体可执行的工作事项中? 接下来,咱们将从人和事两个方面来分享咱们的经验与思考。 人: DDD里强调,领域专家和开发团队独特工作。 领域专家的退出,其本质在于让所有的设计回归业务自身,从业务价值登程,聚焦业务外围域,防止适度设计。 心愿领域专家和开发团队独特工作,往往是实际中的第一个难题,如何找到合格的领域专家?如何保障领域专家的工夫投入?如何最大化施展领域专家的“价值”? 对于合格的领域专家:领域专家不是指某一个特定的人,能够是很多人,可互相补充,但同时也不倡议过多。在寻找领域专家时,企业能够从本身的业务畛域登程,在每个业务畛域寻找到有丰盛教训的人员。这里的“经验丰富”并不仅仅是一线实战经验丰盛,更是对业务要有深度的了解和认知。对于领域专家工夫投入的保障:在实践中会发现,有各种各样的“主观”起因,导致领域专家无奈无效投入。尽管有很多主观的状况,然而不论有如许艰难,保障领域专家的工夫投入都是必须要做的。如果领域专家都不参加,咱们怎么有信念讲咱们交付的是业务价值而不是一堆代码呢?如何最大化施展领域专家的“价值”:把形象的问题具象化,通过凝听和疏导,更多地从企业的领域专家处取得信息,放弃“我是专家”的执念。开发团队:在少数的信息化我的项目中,常见的分工是需要分析师进行需要收集与设计,技术人员负责开发和实现,需要分析师进行测试验收。在DDD实际时,倡议开发团队的业务架构师、技术架构师、测试架构师从一开始便独特工作,这样才可能施展每个角色在各自业余畛域的短处、逐渐对立语言、疾速达成共识。 事: DDD辨别了策略设计与战术设计两个档次,提出了聚焦外围域、创造性合作、对立语言的三个准则。很多人在刚开始利用DDD的时候,会感觉本人都“不会谈话”、“不会做事儿”了。这其实是因为过于关注名词自身,陷于对名词的了解和解释上,因而,在“事”上,咱们认为,须要抓住两个要点。 找到更难受的形式DDD通过策略设计解决业务边界划分的问题,通过战术设计实现畛域模型的形象,解决的是从业务到技术的过渡。 在谈DDD的时候,肯定绕不开事件风暴。“事件风暴(Event Storming)是一种疾速的设计技术,让领域专家和开发人员都能够参加到这个快节奏的学习过程中。它聚焦于业务和业务流程,而非名词概念和数据。”(摘自Vaughn Vernon《畛域驱动设计精粹》)。事件风暴为DDD策略与战术落地,提供了一种十分有逻辑、有体系的办法,那么在这个办法之外,还有没有其余的办法呢?答案是,有的。尤其在B端企业的信息化建设中,瀑布施行方法论、麻利开发等等十分多优良的办法与工具,都能够为咱们所用。 在DDD的落地过程中,咱们外围的是要抓住最终要取得的后果,在过程中所采取的形式,能够依据理论状况进行调整,要害是要找到一个让大多数团队成员都难受的形式。以笔者的经验为例,有一家客户习惯了以流程图的形式进行业务梳理,在这种状况下,应用事件风暴的形式会对客户的习惯造成冲击,甚至让客户不晓得如何输入。此时,咱们就应该及时调整,思考流程图与事件风暴如何交融,既能以客户习惯的形式推动,同时也能拿到咱们想要的后果。 将准则与概念造成模板与具体工作工作一千个人有一千个哈姆雷特,每个人对DDD准则、概念都有本人的解读。为了更疾速地在团队内达成DDD落地办法的“对立语言”,倡议以具体的工作工作、产出物模板来承载,让团队更聚焦在指标与工作的达成上。 通过在自主产品的研发和施行我的项目中利用DDD并继续迭代其利用办法,咱们不仅无效地实现了中台我的项目的施行、为客户构建了正当地利用架构,也逐步完善了我的项目施行方法论体系,为更宽泛地我的项目交付和产品研发提供了助力。DDD帮忙解决了产品架构设计中的一部分问题,在架构设计之后,如何保障设计的落地、高效地交付,是每个团队会面临的下一个问题。在这里,笔者举荐应用猪齿鱼这款数智化效力平台,它能够无效治理架构设计所造成的微服务或模块,拉通设计与开发,通过提供合作、测试、DevOps、容器工具,进步团队效力。 猪齿鱼数智化效力平台 当初咱们是如何了解DDD的?DDD首先是一种思维,“聚焦外围域、创造性合作、对立语言”,是对于价值发现、价值认定、探讨、凝听、共识、迭代。这种思维不仅实用于软件设计,也实用于日常工作、集体生存、家庭教育等等方面。咱们不可能在每个方面都做到完满,须要辨认要害要务、聚焦投入;咱们也不可能单独存在,须要与人连贯、深度凝听、踊跃反馈;咱们欣慰于共识默契,也有勇气承受差别,因为正是这些差别让咱们一直碰撞、更好地学习与成长。 其次,DDD是一种办法,它辨别了策略设计与战术设计的,引入了限界上下文与对立语言,提供了实体、值对象、聚合、工厂、资源库等。同时,业界也有很多DDD的相干著述,能够帮忙咱们更无效地去了解这些概念与具体利用办法。当然,你自身所在的畛域与你的过往教训,也是利用这些办法时十分值得参考的。 软件的架构,究其基本是物理世界在数字世界的映射。在DDD的利用中,咱们不仅吸取DDD的思维与办法,也学习麻利、DevOps、测试、微服务等畛域常识,同时联合咱们既往的教训,逐渐总结和迭代了适宜咱们的一套架构设计体系与办法。正如Eric Evans所说的,“DDD还未完结”,咱们也在继续实际与继续调整。 本文是基于笔者以后的认知与了解所造成的,欢送留言探讨。 本文由猪齿鱼技术团队原创,转载请注明出处:猪齿鱼官网 对于猪齿鱼 猪齿鱼Choerodon数智化效力平台,提供体系化方法论和合作、测试、DevOps及容器工具,帮忙企业拉通需要、设计、开发、部署、测试和经营流程,一站式进步管理效率和品质。从团队协同到DevOps工具链、从平台工具到体系化方法论,猪齿鱼全面满足协同治理与工程效率需要,贯通端到端全流程,助力团队效力更快更强更稳固,帮忙企业推动数智化转型降级。戳此处试用猪齿鱼

November 26, 2021 · 1 min · jiezi

关于ddd:CQRS在一条订单系统中的实践二DDD与CQRS结合

DDD与CQRS联合背景常识探讨这个问题之前,咱们先回顾几个根底概念。 聚合根: 恪守不变性规定设计的聚合边界,聚合内的所有操作都是严格遵守业务规约具备强统一的保障聚合根的概念很形象,但却是领域建模外围中的外围。这里不探讨畛域划分或者聚合根如何建设,仅谈谈我对"恪守不变性规定设计的聚合边界"这句话的了解。首先咱们得意识到领域建模实质上是一种常识建模的计划,在这个前提下,咱们的建模必须不能违反业务法令。聚合基本质上就是"业务法令"在零碎内的一致性体现,比方在"拆单"这个语境下不仅仅是父订单状态变为已拆单,也必须随同着子单的生成。在此基础上与聚合根无关的另一条设计准则"通过聚合根拜访实体"也就不难理解了,通过聚合根来无效的限度实体(可变对象)在零碎内的拜访,使实体的所有变更都在"不变性规定"下进行,也就可能防止"业务法令"被毁坏。 洋葱模型两个同心圆档次:代表传播机制和基础设施的外层;代表业务逻辑的内层在了解了聚合根的根底上,我再来看下"洋葱模型"亦或是"六边形架构"就不难理解了。聚合根设计的基本出发点是"业务准则的高度内聚",从这个角度登程势必要求零碎内的数据扭转都必须通过畛域模型束缚,简略的能够将过程概括为用户层(同步事件处理(即对外API)、异步事件处理(音讯监听))==>畛域层(业务逻辑)==>基础设施层(聚合内的数据变动长久化(DB或其余存储模式),跨零碎的长久化变更(发送畛域事件)) CQRS: 命令查问责任拆散,读写双模型;命令模型用于无效地执行写/更新操作,而查问模型用于无效地反对各种读模式。通过畛域事件或其余各种机制将命令模型中的变更流传到查问模型中,让两个模型之间的数据放弃同步。CQRS两种实现策略同步计划,实时双写,长处是实时性高,双写带来了额定的写时开销,具体案例能够参看《拍卖系统优化历程(一) -- 建设拍品Lot模型,实际CQRS》;异步计划,借助音讯或者其余异步同步机制 ,让两个模型达到最终一致性(这里介绍咱们如何借助数据库的读写同步策略来达到读写模型的最终一致性);领取流程的领域建模与CQRS落地订单畛域划分订单根底畛域划分 平安的构建实体(不变性规定的一部分) 围绕订单咱们拆分成了多个畛域("订单"作为各自畛域的聚合根),这些不同畛域上的订单尽管基于的底层数据是重合的,但"订单"在不同的上下文内的定义是不雷同的。 以"领取订单(PayingOrder)"与"待拆单订单(SplittingOrder)"为例,其本质上的底层数据都建设在同样的数据上,划分这两个聚合的边界一方面是其畛域内聚焦的业务问题不雷同, 另一方面从"订单"这个实体上看其生命状态有着显著的边界,PayingOrder待领取订单,SplittingOrder已领取实现待拆单订单,订单是否领取这两个订单实体有了实质的区别。 留神这里咱们探讨的是实体状态而非数据状态,这一点很重要,在这个前提下如果订单状态不合乎以后聚合下的状态要求则聚合内的实体构建失败,通过屏蔽非平安的实体(或者聚合)构建防止了不平安因素造成的影响。 问题到此并没有完结,后面咱们定义的不平安,有时候可能是某个时间段内的误判,比方因为限流降级、主同步提早等策略失效导致了咱们基于偏差的数据得出了"不平安的拜访"的谬误断定 (这也是咱们做读写拆散比拟头疼的问题),那么怎么解决?这里就要借助聚合之外的最终一致性策略来达到。 聚合外的最终一致性 咱们在聚合外部须要谋求强一致性保障,聚合外咱们往往采纳最终一致性来解除零碎间耦合(上图中彩色箭头),对于聚合内产生的事件能够采纳例如kafka这类的消息中间件来进行通信。 必然能够做到对下面误判的重试(比方音讯的重试)直至最终统一,而不必从业务实现上再去投入过多精力进行干涉,因为这个零碎间的一致性问题没有主同步提早、限流降级的影响它也是存在的。 领取事件的处理过程洋葱模型比拟形象,换个形式形容这里的具体实现能够参看我之前写的两篇文章《DDD实际落地(二)》《领取订单领域建模实际》 从CommandModel到QueryModel后面提到了两种实现CQRS的机制,并不难理解。 同步计划,DomainRepository拜访读写模型各自的数据源,进行同步的数据更新;异步计划,CommandModel(畛域实体)执行完命令后发送响应的畛域事件,通过订阅畛域事件来更新查问模型;借助数据层的读写同步机制,达到读写模型的最终一致性; CQRS读写职责拆散,实质上是通过实体职责的拆散简化了读模型的结构(优化查问),同时因为读模型不再承载写命令的执行所以也躲避掉了读模型在数据不齐备或者不实时的状况下不会进一步扩散过期读问题。 在"订单领取"、"订单发货"这些场景咱们采纳了不同于其上两种形式的读写模型同步计划(或者说是异步计划的变种),在内部零碎收到领取实现事件后通过读模型查问待拆单订单,待拆单订单的无效结构建设在 待领取订单的领取行为的失去了一致性解决(PayingOrder负责了写模型对应的数据的强一致性,SplittingOrder只须要验证其状态即可能确保数据失去了残缺的同步),若构建胜利则意味着读模型数据的同步已实现进行数据返回, 否则领取实现的订阅方持续重试期待读模型的数据同步实现。 具体的实现细节能够参看CQRS在一条订单零碎中的实际(一) 中对于订单读写库的应用细节 读模型进行事件重放,达到逻辑的一致性; 在读写同步的计划中,咱们能够看到同步都是通过畛域事件影响形成读写模型的数据层,并向上反馈来达到最终一致性。那么如果某些场景下读模型对于事件响应不通过数据层向上反馈,而是间接作用于逻辑层是不是仍旧行的通? 具体的实现细节能够参看CQRS在一条订单零碎中的实际(一) 中对于"订单查问流程"的形容 这个计划可能失效必须建设在读模型的结构过程中可能重放写模型的事件,许多场景下这个条件还是很难达到的。 总结回过头来咱们再看整个计划中咱们并没有从业务实现上刻意的去做一些调整但仍旧可能实现了CQRS的落地,而这所有的基本其实还是在于开篇所谈的聚合根的准则:"在聚合边界内建模真正的不变条件"、"在聚合边界之外应用最终一致性"。从书本中来到实际中去,这也是我对近期实际的一次总结,心愿可能大家一些启发。

November 17, 2021 · 1 min · jiezi

关于ddd:CQRS在一条订单系统中的实践一

开篇先谈谈通过CQRS在订单领取过程中带来的收益,图有点长能够联合咱们获得的最终收益从下文的流程图中进行印证。 通过CQRS,咱们将订单下单领取的次要性能在极其状况下主体性能可用(降级对领取零碎的依赖),同时可能在零碎复原后数据失去最终的一致性解决; 通过CQRS,咱们将订单内的局部查问性能或者特定状态下(另一个聚合)的查问流量建设在读库,通过对象的生命周期治理(状态治理)来简化数据管理(读写库数据不统一),同时防止提早导致的数据不准确性在零碎内蔓延; Tips:为了简化流程,以下流程图仅保留了外围交互流程 下单流程概要 领取流程概要 订单查问流程概要 另附两篇CQRS的其余实际案例 一条拍卖系统优化(一) -- 建设拍品Lot模型,实际CQRS一条拍卖系统优化(二) -- 模型深入,建设出价用户BidUser模型 更多文章欢送关注我的公众号

September 23, 2021 · 1 min · jiezi

关于ddd:一条拍卖系统优化二-模型深化建立出价用户BidUser模型

接上一篇拍卖系统优化(一) -- 建设拍品Lot模型,实际CQRS,分享一下组内小伙总结的后续优化 一、背景:现有拍卖零碎设计实现 1.1 外围畛域实体 ReadWriteLot 和 ReadonlyLot (CQRS) ReadWriteLot 通过DB数据结构,次要解决拍品的写场景问题, 例如:开拍、出价、成交、流拍 等; ReadonlyLot 在竞拍中的拍品应用redis中的数据结构进行读减速解决竞拍过程中高频读问题(无缓存,redis数据与DB数据强统一),非竞拍中的拍品数据已不再变动通过ReadWriteLot降级失去并进行缓存;1.2 优化前的出价和流拍实现计划 因为业务场景下存在即为了应答线上或下线这不同业务场景下拍卖业务的差别流程,例如:线上出价受额度限度而线下代拍员不受该束缚、线上出价后须要发送出局揭示而同步拍不须要等等。为了应答这种差别咱们通过形象的出价策略接口定义,想借此隔离了不同出价策略 OnlineBidStrategy(线上出价策略) 和 OfflineBidStrategy(线下出价策略)的实现。 1.2.1 线下出价策略:线下的出价蕴含三个局部。 1、有效掉高于本人本次出价的历史出价,有效出价的后果 2. lot 调用出价畛域行为,长久化出价后果 3.发送出价的相干告诉、IM 音讯、批改价格零碎价格 1.2.2 线上出价策略 :线上出价包含三个局部:1.线下出价的前置校验。2. lot 调用出价畛域行为,长久化出价后果 3. 发送出价的相干告诉、IM 音讯、批改价格零碎价格,线下出价主动订阅 1.2.3 畛域实体Lot 的 出价办法 : 线上出价的校验 或 线下出价的校验;2. 出价变更相干出价信息 到这里咱们能够思考这样几个问题: 这里的咱们寄冀望引入出价策略的形象可能暗藏不同场景下的出价流程的差别,真的做到了吗? 咱们寄冀望与出价策略的形象可能隔离实现细节,但存在在策略中的一些操作往往又与拍品畛域对象的出价行为有肯定的相关性,并且这种相关性的感知最终还是扩散到了拍品中,这还是背离了咱们的设计初衷。 线上出价和线下出价的前置校验逻辑散落在 Lot和 两种不同的出价策略中,显然如果出价逻辑变更,我须要批改的是Lot 和 出价策略两个中央,要是能将这个差异性的逻辑对立写在一个中央就好了,比方当线下出价的校验逻辑扭转了,我只去批改 Lot 外部对于线下出价的校验或者我只去批改 OfflineBidStrategy(线下出价策略),这样看起来仿佛更加正当。 现实状况下咱们冀望这种差异化的行为能够通过畛域事件或者对象的不同行为实现,对外提供一个标准化的出价服务,无需感知不同场景下的实现差别,但显然以后采纳的出价策略的设计方案并没有可能隐藏住实现细节和逻辑内聚(Lot实体还是辨别了不同场景做了一些差异化解决)。 2.是不是只有把线上出价的逻辑和线下出价的逻辑都放到lot 的 bid(出价行为)中而后一个简略的 if-else就能将线上出价和线下出价的逻辑隔离,同时又将逻辑内聚到lot中,如下图? 这个解决方案我感觉其实也不是齐全不可行的,只不过目前来说咱们对不同类型的出价场景(线上用户出价、线下用户出价)的差异性次要体现在:出价前置的校验、出价后后置的操作(比方发送音讯、订阅揭示等)、用户出价后会对本身信息进行变更(比方变更本身的额度信息等)。如果依照下面的设计貌似很难满足需要,因为很显著在出价这个上下文中,有两个不同的实体拍品( Lot)、出价用户 (BidUser),之前的设计中并没有构建出BidUser 这个实体,次要是因为咱们对不同出价用户并没有粗疏差异化的辨别,然而随着业务倒退的须要很多时候一个Lot 无奈撑持这个业务逻辑(这也是引入出价策略的起因之一)。 3.出价畛域服务次要解决的外围问题是出价,然而出价后的一些列行为比方发送告诉、IM音讯、订阅拍品、批改price零碎的价格,这些实质上不属于出价行为,即便这些后置的操作执行失败也不应该影响到本次出价,那么应该在哪里触发这些出价的后置操作? 这里很多出价的后置操作实质上与出价外围行为无关,并且后置操作失败或者报错都不应该影响到本次出价的胜利与否,这里咱们最好应用不与事务绑定的畛域事件来实现。具体实现见下文。二、优化设计构建BidUser实体 ...

September 23, 2021 · 1 min · jiezi

关于ddd:一条拍卖系统优化一-建立拍品Lot模型实践CQRS

分享一下组内小伙伴张聪、庆红在年初对拍卖领域建模的总结 一、背景:原拍卖零碎设计(基于数据模型)原拍卖零碎的构建了SkuBidRedisInfo(数据对象) 用来记录拍品在竞拍中的热点信息,并且将这个实体放入Redis中。 在拍品的生命周期中间接应用SkuBidRedisInfo进行逻辑判断,尽管通过SkuBidRedisInfo(拍品)和 AuctionSkuDTO(拍品)结构了 InnerSkuLotInfo (仅作为一个Value object)对象,然而InnerSkuLotInfo并不能残缺的形容一个拍品残缺的生命周期比方 竞拍前、竞拍中、竞拍完结、生成订单中等状态。 二、重构拍卖设计及改良过程 裁减数据对象 LotRedisInfo 信息,简化结构过程:通过LotRedisInfo 存储拍品在竞拍中的信息,竞拍期间仅依赖LotRedisInfo即可构建拍品实体。一方面通过双写AuctionSkuDTO以及LotRedisInfo,放弃竞拍中的数据一致性;另一方面简化了原有拍品实体构建的过程,失去更好的性能晋升; 构建拍品畛域实体 Lot ,定义了拍品的不同生命周期、以及各个生命周期的行为:这里咱们的初始做法是将Lot 的构建分为两种状况:1、竞拍中的拍品Lot,利用LotRedisInfo 进行结构 ,2、非竞拍中的拍品,从DB中取出数据结构Lot,放入Local&Redis缓存; 拍品生命周期内的行为 3.围绕实体进行数据变更: 当拍品信息产生变更时,比方开拍、出价、截拍、成交等行为产生,咱们通过变更Lot,在基于Lot实体转化失去须要长久化的AuctionSkuDTO或LotRedisInfo,进行长久化; 长久化的过程如下: 变更Lot自身的信息 这里长久化Lot信息的时候,咱们会依据Lot的预期截止工夫和Lot对应 InnerActivityInfo 的预期截止工夫比拟判断是否须要延期流动截止工夫,这里对 InnerActivityInfo 的信息进行批改并且长久化刷新缓存(潜在问题1) 长久化 Lot 信息 , Lot 导出相应的 AuctionSkuDTO(长久到DB) 、 流动未完结LotRedisInfo(长久到Redis) 新模型存在的问题这里Lot畛域实体的构建曾经可能详细描述一个拍品的残缺生命周期及各个行为,然而存在两个问题: 问题1 :InnerActivityInfo的批改不平安: 咱们在构建Lot的时候,依赖了流动信息的实体InnerActivityInfo,然而 InnerActivityInfo 在Lot的整个生命周期外面并不是固定不变的,咱们通过一个InnerActivityInfo A构建了一个Lot A,这个时候如果有其余Lot信息变更,导致InnerActivityInfo产生了变更,那么Lot A中应用了InnerActivityInfo A的局部信息就可能是脏数据了; 问题2 :因为引入了Lot在零碎中存在了存储正本(缓存),对于Lot的读写存在了不统一: 利用实例A 获取拍品Lot 的本地缓存Lot A,而后对Lot A进行批改,比方批改拍品程序,长久化,这个场景在单实例下没有问题,然而多实例下,如果在A操作之前,如果利用实例B上也想对同一个拍品Lot 进行批改,读取本地缓存Lot B批改了起拍价,长久化,那么利用实例A上读出来的数据就是脏数据,那么通过一系列操作,A只是想批改一个拍品程序,然而却把B批改起拍价的数据给笼罩了,问题的要害就是咱们不能基于缓存进行信息变更,再长久化到数据库; 解决方案: 问题1----解决方案:对象隔离 在Lot中咱们不要强依赖InnerActivityInfo实体,而是每次应用时,传入一个最新的InnerActivityInfo(最佳做法是传递一个只读正本),这样就保障了在Lot的生命周期中,每次应用到InnerActivityInfo的相干信息都是最新的(这个是不是InnerActivityInfo的一致性问题由InnerActivityInfo保障,通过其畛域服务进行对应的批改); 问题2----- 解决方案:CQRS,将拍品分为两种实体 ReadonlyLot(只读实体)、ReadWriteLot(可读写实体) ReadonlyLot不蕴含对本身信息变更的能力,这样防止了误用。咱们的内存cache和缓存都是建设在ReadonlyLot上的可能会读取到过期数据,但写场景是不容许过期数据的读取(写建设在读上,读过期,会产生写笼罩问题),通过实体层面的隔离咱们杜绝了这种过期读的产生。 当咱们须要批改Lot的信息时,即便咱们获取了ReadonlyLot,也无奈对其进行信息变更,从根本上杜绝了这种不平安公布的产生。 ReadonlyLot&ReadWriteLotReadonlyLot、ReadWriteLot的构建 ...

September 23, 2021 · 1 min · jiezi

关于ddd:慎用DDD

DDD自身确实是个好货色,其在应答简单需要变更,放弃软件品质上,提供了一套优良的方法论。但所有的好货色,都是有老本的。 DDD要想用好,对团队的要求是十分高的。 一个可能驾驭DDD的团队,至多须要满足以下几点要求1、业务需要自身确实曾经简单到了肯定水平,靠面向数据编程的形式曾经难以维计了2、团队有驾驭微服务的技术能力3、有领导层全力支持DDD在我的项目中的落地4、团队自身代码格调良好,开发标准齐全且执行到位,日常中有应用设计模式重构代码的传统 如果以上条件都不满足,那大概率下也是用不好DDD 的,这项需要剖析到软件设计的方法论,就和团队的气质不相符,生吞活剥DDD的设计理念,只可能在抵触中把我的项目给作死

September 1, 2021 · 1 min · jiezi

关于ddd:解构领域驱动设计-第一篇-读书笔记

DDD要解决什么问题?第一本对于DDD的书, 书名就是<畛域驱动设计- 软件外围复杂度应答之道>, 表明DDD其实就是为了应答软件复杂度的挑战. 是什么造成了软件的复杂度?规模构造变动规模其实包含了两方面, 一方面是因为需要越来越多, 性能越来越多. 另一方面是因为性能点之间是有分割的, 牵一发而动全身, 因而规模随着业务的倒退很容易呈指数级的回升. 构造另一方面文中提到很大水平是因为品质属性造成结构复杂. 我感觉其实还有别的方面. 咱们来剖析一下咱们设计构造的目标: 性能, 资源利用相干的. 反对横向拓展的架构, 反对分库分表的构造, 微服务进步资源利用效率. 总线转分布式通信为了进步代码可维护性. 例如咱们设计分层架构就是为了不同档次职责拆散, 进步可读性和可拓展性晋升沟通合作和管理效率. 例如要合乎康威定律(任何组织在设计一套零碎时, 所交付的设计方案在结构上都与该组织的沟通构造保持一致)升高业务复杂度. 例如咱们拆分出不同的子畛域来防止认知过载.简略的逻辑是不须要构造的, 但随着逻辑的增多, 咱们须要引入构造来升高复杂度, 构造肯定水平上也会让逻辑变得复杂, 但只有收益比老本要高, 减少构造就是正当的. 但因为对构造的误用, 或者定义不够清晰, 职责划分不同明确导致不仅没有升高复杂度, 而且还减少了复杂度. 例如咱们对性能适度思考, 为应答高性能的挑战而减少各种中间件和进行异步化革新. 又例如咱们尽管进行了分层架构的设计, 但到处都有不合乎分层准则的代码呈现. 或者咱们谬误的应用了康威定律, 导致了软件复用性大大降低, 进步了保护和拓展老本. 下面两个起因影响的其实是理解能力, 而变动影响的是咱们预测能力. 变动预测能力的有余会让咱们产生适度设计或者设计有余. 设计的实质在于如何和做取舍, 如何均衡不同条件的影响使其达到最优. 如何形象才是最正当的, 这点很大水平上依赖每个人对业务的了解和教训的积攒. 设计元模型蕴含什么畛域驱动设计是以畛域作为驱动力, 应用畛域模型作为外围来进行设计. 为了升高规模, 畛域又能够拆分成子畛域. 为了让资源失去最无效的利用, 子畛域能够分为外围子畛域, 撑持子畛域, 通用子畛域, 依附不同类型的子畛域来正当分配资源. 应用限界上下文来确定业务能力的自治边界. 通过上下文映射来表白限界上下文的协作关系. 通过分层架构将畛域独立进去, 隔离技术复杂度和业务复杂度. 通过了领域专家进行沟通, 在对立语言的领导下获取到畛域模型. 畛域模型蕴含了实体, 值对象, 畛域服务和畛域事件. 畛域逻辑都会封装到这些对象中. 聚合是最小的业务单元, 能够封装多个实体或者值对象, 并维持边界范畴内业务完整性. ...

August 30, 2021 · 2 min · jiezi

关于ddd:谈谈代码DDD从入门到完全入门

本文首发于泊浮目标简书:https://www.jianshu.com/u/204...版本日期备注1.02021.8.22文章首发之前的DDD文章——谈谈代码:升高复杂度,从放弃三层架构到DDD入门,通篇下来像 是简略的讲了一些概念,而后疾速的实战一下——很多同学反馈感觉就是入门了,但没有齐全入门,因而咱们再加一篇。 1.什么是DDD先看下万能的维基百科:Domain-driven design (DDD) is the concept that the structure and language of software code (class names, class methods, class variables) should match the business domain. For example, if a software processes loan applications, it might have classes such as LoanApplication and Customer, and methods such as AcceptOffer and Withdraw. 这边将其称为了一个概念。在我看来DDD是设计模式的超集,一种指导思想——用来领导如何解耦业务零碎,划分业务模块,定义业务畛域模型及其交互。 2. DDD诞生的背景畛域驱动设计这个概念并不新鲜,早在 2004 年就被提出了,到当初曾经有十几年的历史了。不过,它被公众熟知,还是基于另一个概念的衰亡,那就是微服务。因而在实现的时候样子不肯定是截然不同的,更多时候还是依据业务场景来因材施教。 3. DDD的核心思想核心思想是让技术复杂度与业务复杂度隔离,并通过对立语言组织业务逻辑,升高认知老本。 具体次要体现在: 基础设施层:它负责隔离技术复杂度,通过形象封装来对内提供服务,而不是让外部服务间接应用它。厚畛域层:同一畛域的常识聚合在一个畛域中,畛域常识不再被割裂。实体:用充血模型代替贫血模型,完全符合面向对象的思维。将业务中的对象齐全投射到实体中。4. DDD能解决什么样的问题个别软件会经验几个不同的周期: 大烟囱:每个利用各自为政,相似的需要反复开发,节约人力服务化:依据不同的业务属性拆分服务。关注服务拆分、服务治理、模型形象平台化:将相干的微服务同一成一个平台裸露进去。关注畛域收敛、畛域自治、能力积淀中台化:将通用能力下沉至中台,疾速响应前端服务。须要关注数据买通、能力串联、业务响应力DDD次要在技术密集型利用里有较大的作用,尤其是当该利用进入服务化、平台化时,能够在:“服务拆分”、“服务治理”、“畛域收敛”、“畛域自治”施展。在中台化中的“数据买通”也有肯定的作用。 而宏观来说,DDD能够无效缩小代码的冗余水平以及需要响应的速度。 5. DDD实际中要留神的5.1 应用IOC来保障档次之间的隔离 常常有小伙伴问我,分层之间该怎么做?因为分层的边界没做好,代码会再度耦合再一起。对此我给出的答案是参考inversion of control。其常见实现有: Object Dependency InjectService Provider InterfaceStrategyAbstract Factory也能够参考我之前写的文章:技巧:遵循Clean Architecture写好白盒测试。 ...

August 22, 2021 · 1 min · jiezi

关于ddd:DDD-领域驱动战略篇-6-菱形对称架构

畛域驱动架构篇—菱形对称架构畛域驱动设计中,对于架构格调有一个指导思想:不同的限界上下文,依据其畛域模型和业务特色,能够选用不同的架构格调。在传统的分层架构与畛域驱动理念相结合的过程中,产生了多种架构格调:六边型架构、整洁架构、微服务架构等。本文是依据对对IT文艺工作者张逸老师的相干Chat浏览和思考整顿而得。 前言菱形对称架构(Rhombic Symmetric Architecture) 次要针对畛域档次的架构,借鉴了六边形架构、分层架构、整洁架构的常识,并联合了畛域驱动设计的元模型,使其可能更好地使用到限界上下文的架构设计中。 架构演进之路六边型架构六边型架构的特点就是:在满足整洁架构思维的同时,更加关注内层与外层本人与内部资源之间的通信实质对于外界每种类型都有一个适配器与之对应:音讯适配器,REST适配器,SOAP适配器,长久化适配器通过内外两个六边形的不同边界清晰地展示了了畛域与技术的边界(蓝色是畛域,灰色是技术) 端口:咱们能够把端口看成是具体的协定适配器:看成是具体的通信垂直技术,如:Servlet、REST、JPA利用:用例级别的业务逻辑,体现了一个业务场景畛域模型:零碎级别业务逻辑,多个服务和聚合形成一个用例六边型架构中,端口是解耦的要害:入口隔离了内部申请和技术实现细节;进口隔离了数据长久化和内部拜访设施六边型架构的业务(预约机票)示例: ReservationResource:订票申请发送给以 RESTful 契约定义的资源服务,它作为入口适配器,介于利用六边形与畛域六边形的边界之内,在接管到以 JSON 格局传递的前端申请后,将其转换(反序列化)为入口端口ReservationAppService须要的申请对象ReservationAppService:入口端口为应用服务,位于畛域六边形的边界之上。当它在接管到入口适配器转换后的申请对象后,调用位于畛域六边形边界内的畛域服务TicketReservationTicketReservation:订票的畛域逻辑ReservationRepository:进口端口为资源库,位于畛域六边形的边界之上,定义为接口ReservationRepositoryAdapter:真正拜访数据库的逻辑则由介于利用六边形与畛域六边形边界内的进口适配器,该实现拜访了数据库,将端口发送过去的插入订票记录的申请转换为数据库可能接管的音讯,执行插入操作整洁架构整洁架构的模型是一个相似内核模式的内外层构造,它具备以下特点: 1 越凑近核心,档次越高2 由外到内:框架与驱动程序 --》接口适配器 --》利用级业务逻辑--》零碎级业务逻辑3 源码中依赖关系必须指向同心圆内层,从外到内 咱们能够在整洁架构上失去很多值得回味的设计思维: 不同档次的组件其变动频率也不雷同,引起变动的起因也不雷同(合乎高内聚低耦合设计思维)档次越靠内组件依赖越少,处于外围业务实体没有任何依赖(畛域模型和技术完满解耦)档次越靠内组件与业务关系越亲密,专属于特定畛域的内容,很难造成通用的框架(畛域构建出了壁垒)在这个架构中,外层圆代表的是机制,内层圆代表的是策略;机制和具体的技术实现无关,容易受到外部环境变动;策略与业务无关,封装了最外围畛域模型,最不容易受到外界环境变动的影响 六边形架构仅仅辨别了内外边界,提炼了端口与适配器角色,并没有布局限界上下文外部各个档次与各个对象之间的关系;而整洁架构又过于通用,提炼的是企业零碎架构设计的根本规定与主题。因而,当咱们将六边形架构与整洁架构思维引入到畛域驱动设计的限界上下文时,还须要引入分层架构给出更为粗疏的设计领导,即确定层、模块与角色构造型之间的关系。 经典分层架构分层架构是使用最为宽泛的架构模式,简直每个软件系统都须要通过层(Layer)来隔离不同的关注点(Concern Point),以此应答不同需要的变动,使得这种变动能够独立进行。 用户界面层:负责向用户展示信息和解释用户命令。蕴含web端UI界面、挪动端UI界面、第三方服务等。应用层:很薄的一层,用来协调利用的流动,它不蕴含业务逻辑,它不保留业务对象的状态。在畛域设计中,它其实是一个外观(Facade),个别是供其余界线上下文根底设置层中controller调用。畛域层:本层蕴含畛域的信息,是业务软件的外围所在。在这里保留业务对象的状态,对业务对象状态的长久化被委托给根底设置层。它蕴含畛域设计中的:聚合、值对象、实体、服务、资源接口等。根底设置层:不要简略的了解为物理上的一层,它作为其余层的撑持库而存在。它提供了层间的通信,实现对业务对象的长久化。个别蕴含:网络通讯、资源实现(数据库长久化实现)、异步音讯服务、网关、controller管制等。菱形对称架构菱形对称架构交融了分层架构和六边型架构的思维。 对六边型进行分层映射 入口适配器:响应边界外客户端的申请,须要实现过程间通信以及音讯的序列化和反序列化,这些性能皆与具体的通信技术无关,故而映射到基础设施层入口端口:负责协调内部客户端申请与外部应用程序之间的交互,恰好与应用层的协调能力相配,故而映射到应用层应用程序:承当了整个限界上下文的畛域逻辑,蕴含了以后限界上下文的畛域模型,毫无疑问,应该映射到畛域层进口端口:作为一个形象的接口,封装了对外部设备和数据库的拜访,因为它会被应用程序调用,遵循整洁架构思维,也应该映射到畛域层进口适配器:拜访外部设备和数据库的真正实现,与具体的技术实现无关,映射到基础设施层冲破分层架构分层架构仅仅是对限界上下文的逻辑划分,在编码实现时,逻辑层或者会以模块的模式体现,然而也可能将整个限界上下文作为一个模块,每个层不过是命名空间的差别,定义为模块内的一个包。不论是物理拆散的模块,还是逻辑拆散的包,只有能保障限界上下文在六边形边界的爱护下可能维持内部结构的清晰,就能升高架构侵蚀的危险。 根据整洁架构遵循的“稳固依赖准则”,畛域层不能依赖于外层。因而,进口端口只能放在畛域层。事实上,畛域驱动设计也是如此要求的,它在畛域模型中定义了资源库(Repository),用于治理聚合的生命周期,同时,它也将作为形象的拜访内部数据库的进口端口。 将资源库放在畛域层确有论据佐证,毕竟,在抹掉数据库技术的实现细节后,资源库的接口办法就是对聚合畛域模型对象的治理,包含查问、批改、减少与删除行为,这些行为也可视为畛域逻辑的一部分。 然而,限界上下文可能不仅限于拜访数据库,还可能拜访同样属于外部设备的文件、网络与音讯队列。为了隔离畛域模型与外部设备,同样须要为它们定义形象的进口端口,这些进口端口该放在哪里呢?如果仍然放在畛域层,就很难自圆其说。例如,进口端口EventPublisher反对将事件音讯公布到音讯队列,要将这样的接口放在畛域层,就显得不三不四了。假使不放在位于外部外围的畛域层,就只能放在畛域层内部,这又违反了整洁架构思维。 既然进口端口的地位如此难堪,而且很显著出和入不太对称,所以咱们罗唆就把出和入对称下,将端口和适配器对立掉,组合成“网关”。 下面的对称架构虽脱胎于六边形架构与畛域驱动设计分层架构,却又有别于二者。对称架构北向网关定义的近程网关与本地网关同时承当了端口与适配器的职责,这实际上扭转了六边形架构端口-适配器的格调;畛域层与南北网关层的内外分层构造,以及南向网关规定的端口与适配器的拆散,又与畛域驱动设计的分层架构渐行渐远。 既然曾经扭转,就依据思维,从新形象下架构图 就失去了菱形对称架构,次要体现了南北网关的对称关系 菱形对称架构的组成菱形架构其上下文的组成: 北向网关的近程网关:过程间通信北向网关的本地网关:过程内通信畛域层的畛域模型:畛域逻辑南向网关的端口形象:各种资源库操作的形象借接口,能够被畛域层依赖,南向网关的适配器实现:网关端口的实现,运行时通过依赖注入将适配器实现注入到畛域层以六边型中的业务示例为例,改成菱形架构的话,其架构如下: 引入上下文映射北向网关演变北向网关和开放式主机服务很相似,然而职能更多,相当于开放式主机层,蕴含了近程服务和本地服务 近程服务:跨过程通信;蕴含资源(Resource)服务、供应者(Provider)服务、控制器(Controller)服务与事件订阅者(Event Subscriber)服务本地服务:过程内通信;对应于应用层的应用服务当内部申请从近程服务进入时,如果须要调用畛域层的畛域逻辑,则必须经由本地服务发动对畛域层的申请。此时的本地服务又表演了端口的作用,可认为近程服务是本地服务的客户端。 南向网关演变南向网关引入了形象的端口来隔离外部畛域模型对外部环境的拜访,这一价值很等同于上下文映射中的防腐层,同样它也扩充了防腐层的性能南向网关的端口分为: 资源库(repository)端口:隔离对外部数据库的拜访,对应的适配器提供聚合的长久化能力客户端(client)端口:隔离对上游限界上下文或第三方服务的拜访,对应的适配器提供对服务的调用能力事件发布者(event publisher)端口:隔离对外部音讯队列的拜访,对应的适配器提供公布事件音讯的能力改良后的菱形对称架构 菱形对称架构去掉了应用层和基础设施层的概念,以对立的网关层进行概括,并以北向与南向别离体现了来自不同方向的申请。如此造成的对称构造突出了畛域模型的核心作用,更加清晰地体现了业务逻辑、技术性能与外部环境之间的边界。 资源库视为防腐层的端口与适配器,作为领域建模时的角色构造型,与场景驱动设计更好地联合,加强了畛域模型的稳定性。应用层被去掉之后,被弱化为凋谢主机服务层的本地服务,相当于从设计层面回归到服务外观的实质,也有助于解决应用服务与畛域服务之间的概念之争。 代码模型示例: ohs 为凋谢主机服务模式的缩写,acl 是防腐层模式的缩写,pl 代表了公布语言;也能够应用北向(northbound)与南向(sourthbound)取代 ohs 与 acl 作为包名,应用音讯(messages)契约取代 pl 的包名。 转载地址:DDD畛域驱动策略篇(6) 菱形对称架构

August 18, 2021 · 1 min · jiezi

关于ddd:领域驱动设计的价值

畛域驱动设计外围是围绕着畛域模型, 其实是一种面向畛域模型的设计形式. 咱们通过大量与领域专家的沟通合作, 失去畛域模型. 通过代码落地, 让畛域模型能够落地并失去验证. 畛域模型始终在咱们的产品开发的生命周期外面承当了很重要的作用. 因而咱们先看看畛域模型是什么, 咱们应用畛域模型的收益是什么. 既然畛域模型这么重要, 咱们先有个直观的意识, 它是长什么样子的. 什么是模型?畛域驱动设计外围是围绕着畛域模型的, 那什么是模型? 上面这幅图是一个城市的鸟瞰图, 外面蕴含了十分多的信息, 包含河流,修建, 公路等等, 但如果我想坐地铁从A点到B点, 那能够怎么做呢? 我能够找一个地铁线路图, 这个地铁线路图其实就是一个模型. 那模型有什么特点呢? 咱们能够总结一下 形象的, 通过精简的, 只为解决一部分问题. 例如他只能解决我坐地铁的问题, 解决不了我开车的问题.具备业务概念或规定的元素. 例如地铁线路图外面有站点, 有线路等.蕴含元素之间的关系. 例如一条线路下面有多少个站点, 站点的绝对间隔是如何的.畛域分析模型长什么样?畛域驱动设计蕴含了畛域分析模型和畛域设计模型, 咱们先看看什么是畛域分析模型.下面就是一个简略的订单相干的畛域分析模型, 他具备一些业务概念, 例如订单, 订单项等, 也蕴含了他们之间的关系, 这个关系能够是依赖, 关联, 聚合, 组合, 继承, 实现. 最重要的是他能满足我增加缩小订单项, 并批改订单项下面的商品和商品数量的需要. 畛域分析模型是个简略的类图吗? 例如Mybatis外面的Mapper对象能不能在畛域模型下面进行表白?答案是否定的, 因为畛域模型关注的是业务畛域的概念, 而非软件技术相干的概念. 关注的是业务而非具体的代码实现.大家是不是发现畛域模型和咱们常说的架构有点相似. 架构其实也是一种模型, 但畛域模型关注的是如何解决业务问题, 也就是性能需要. 而架构则是为了解决非功能性需要, 例如分层架构是为了解决可读性问题, 可维护性的问题. 微服务架构是为了解决划分弹性边界, 让资源失去最无效的利用. 微内核架构是为了解决可拓展的问题.畛域模型和代码实现区别是怎么子的?那咱们试着用个例子来阐明用畛域模型来设计的益处. 首先需要如下 用户登录胜利会依据不同角色加积分角色分为一般和VIP一般加1分, VIP加2分用事务脚本实现一下这个需要并不简单, 咱们能够用一个办法就解决了 public void login(String username, String password) throws Exception { User user = userMapper.selectByUsername(username); if (Objects.equals(user.getPassword(), password)) { switch (user.getRole()) { case GENERAL: user.setPoint(user.getPoint() + 1); break; case VIP: user.setPoint(user.getPoint() + 2); break; default: } } else { throw new Exception("明码不正确"); } }但如果需要有变怎么办? ...

August 14, 2021 · 2 min · jiezi

关于ddd:结合电商支付业务一文搞懂DDD-IDCF

2004 年,软件巨匠 Eric Evans 的不朽著述《畛域驱动设计:软件外围复杂性应答之道》面世,从书名能够看出,这是一本应答软件系统越来越简单的方法论的图书。然而,在过后,中国的软件业才刚刚起步,软件系统还没有那么简单,即便保护了几年,软件进化了,不好保护了,推倒从新开发就好了。因而,在过来的那么多年里,真正使用畛域驱动设计开发(DDD)的团队并不多。一套优良的方法论,因为事实阶段的起因而始终不温不火。 不过,这些年随着中国软件业的疾速倒退,软件规模越来越大,生命周期也越来越长,推倒从新开发的老本和危险越来越大。这时,软件团队急切需要在较低成本的状态下继续保护一个零碎很多年。然而,大失所望。随着工夫的推移,程序越来越乱,保护老本越来越高,软件进化成了有数软件团队的噩梦。 这时,微服务架形成了规模化软件的解决之道。不过,微服务对设计提出了很高的要求,强调“小而专、高内聚”,否则就不能施展出微服务的劣势,甚至可能令问题更蹩脚。 因而,微服务的设计,微服务的拆分都须要畛域驱动设计的领导。那么,畛域驱动为什么能解决软件规模化的问题呢?咱们先从问题的本源谈起,即软件进化。 软件进化的本源最近 10 年的互联网倒退,从电子商务到挪动互联,再到“互联网+”与传统行业的互联网转型,是一个十分苦楚的转型过程。而近几年的人工智能与 5G 技术的倒退,又会带动整个产业向着大数据与物联网倒退,另一轮的技术转型曾经拉开帷幕。 那么,在这个过程中,一方面会给咱们带来诸多的挑战,另一方面又会给咱们带来无尽的机会,它会带来更多的新兴市场、新兴产业与全新业务,给咱们带来全新的倒退时机。 然而,在面对全新业务、全新增长点的时候,咱们能不能把握住这样的时机呢?咱们冀望能把握住,但每次回到事实,回到正在保护的零碎时,却令人丧气。咱们的软件总是经验着这样的轮回,软件设计品质最高的时候是第一次设计的那个版本,当第一个版本设计上线当前就开始各种需要变更,这经常又会打乱原有的设计。 因而,需要变更一次,版本迭代一次,软件就批改一次,软件批改一次,品质就降落一次。不管第一次的设计品质有多高,软件经验不了几次变更,就进入一种低质量、难以保护的状态。进而,团队就不得不在这样的状态下,以高老本的形式一直地保护上来,保护很多年。 这时候,保护好原有的业务都十分不易,又如何再去冀望将来更多的全新业务呢?比方,这是一段电商网站领取性能的设计,最后的版本设计品质还是不错的: 当第一个版本上线当前,很快就迎来了第一次变更,变更的需要是减少商品折扣性能,并且这个折扣性能还要分为限时折扣、限量折扣、某类商品的折扣、某个商品的折扣。当咱们拿到这个需要时怎么做呢?很简略,减少一个 if 语句,if 限时折扣就怎么怎么样,if 限量折扣就怎么怎么样……代码开始收缩了。 接着,第二次变更须要减少 VIP 会员,除了减少各种金卡、银卡的折扣,还要为会员发放各种福利,让会员享受各种特权。为了实现这些需要,咱们又要在 payoff() 办法中退出更多的代码。 第三次变更减少的是领取形式,除了支付宝领取,还要减少微信领取、各种银行卡领取、各种领取平台领取,此时又要塞入一大堆代码。通过这三次变更,你能够设想当初的 payoff() 办法是什么样子了吧,变更是不是就能够完结了呢?其实不能,接着还要减少更多的秒杀、预订、闪购、众筹,以及各种返券。程序变得越来越乱而难以浏览和保护,每次变更也变得越来越艰难。 问题来了:为什么软件会进化,会随着变更而设计品质降落呢?在这个问题上,咱们必须寻找到问题的本源,能力隔靴搔痒、解决问题。 要探寻软件进化的本源,先要从探寻软件的实质及其法则开始,软件的实质就是对真实世界的模仿,每个软件都能在真实世界中找到它的影子。因而,软件中业务逻辑正确与否的唯一标准就是是否与真实世界统一。如果统一,则软件是 OK 的;不统一,则用户会提 Bug、提新需要。 在这里发现了一个十分重要的线索,那就是,软件要做成什么样,既不禁咱们来决定,也不禁用户来决定,而是由主观世界决定。用户为什么总在改需要,是因为他们也不确定主观世界的规定,只有遇到问题了他们能力想得起来。因而,对于咱们来说,与其气宇轩昂地依照用户的要求去做软件,不如在充沛了解业务的根底下来剖析软件,这样会更有利于咱们缩小软件维护的老本。 那么,真实世界是怎么的,咱们就怎么开发软件,不就简略了吗?其实并非如此,因为真实世界是非常复杂的,要深刻理解真实世界中的这些业务逻辑是须要一个过程的。因而,咱们最后只能意识真实世界中那些简略、清晰、易于了解的业务逻辑,把它们做到咱们的软件里,即每个软件的第一个版本的需要总是那么清晰明了、易于设计。 然而,当咱们把第一个版本的软件交付用户应用的时候,用户却会发现,还有很多不简略、不明了、不易于了解的业务逻辑没做到软件里。这在应用软件的过程中很不不便,和实在业务不统一,因而用户就会提 Bug、提新需要。 在咱们一直地修复 Bug,实现新需要的过程中,软件的业务逻辑也会越来越靠近真实世界,使得咱们的软件越来越业余,让用户感觉越来越好用。然而,在软件越来越靠近真实世界的过程中,业务逻辑就会变得越来越简单,软件规模也越来越宏大。 你肯定有这样一个意识:简略软件有简略软件的设计,简单软件有简单软件的设计。 比方,当初的需要就是将用户订单依照“单价 × 数量”公式来计算应酬金额,那么在一个 PaymentBus 类中减少一个 payoff() 办法即可,这样的设计没有问题。不过,如果当初的须要在付款的过程中计算各种折扣、各种优惠、各种返券,那么咱们必然会做成一个简单的程序结构。 然而,真实情况却不是这样的。真实情况是,起初咱们拿到的需要是那个简略需要,而后在简略需要的根底上进行了设计开发。但随着软件的一直变更,软件业务逻辑变得越来越简单,软件规模不断扩大,逐步由一个简略软件转变成一个简单软件。 这时,如果要放弃软件设计品质不进化,就该当逐渐调整软件的程序结构,逐步由简略的程序结构转变为简单的程序结构。如果咱们总是这样做,就能始终保持软件的设计品质,不过十分遗憾的是,咱们以往在保护软件的过程中却不是这样做的,而是一直地在原有简略软件的程序结构下,往 payoff() 办法中塞代码,这样做必然会造成软件的进化。 也就是说,软件进化的本源不是版本迭代和需要变更,版本迭代和需要变更只是一个诱因。如果每次软件变更时,适时地进行解耦,进行性能扩大,再实现新的性能,就能放弃高质量的软件设计。但如果在每次软件变更时没有调整程序结构,而是在原有的程序结构上一直地塞代码,软件就会进化。这就是软件倒退的法则,软件进化的本源。 杜绝软件进化:两顶帽子后面谈到,要放弃软件设计品质不进化,必须在每次需要变更的时候,对原有的程序结构适当地进行调整。那么该当怎么进行调整呢?还是回到后面电商网站付款性能的那个案例,看看每次需要变更该当怎么设计。 在交付第一个版本的根底上,很快第一次需要变更就到来了。第一次需要变更的内容如下。 减少商品折扣性能,该性能分为以下几种类型: 限时折扣限量折扣对某类商品进行折扣对某个商品进行折扣不折扣以往咱们拿到这个需要,就很不沉着地开始改代码,批改成了如下一段代码: 这里减少了的 if else 语句,并不是一种好的变更形式。如果每次都这样变更,那么软件必然就会进化,进入难以保护的状态。这种变更为什么不好呢?因为它违反了“凋谢-关闭准则”。 开闭准则(OCP) 分为凋谢准则与关闭准则两局部。 凋谢准则:咱们开发的软件系统,对于性能扩大是凋谢的(Open for Extension),即当零碎需要产生变更时,能够对软件性能进行扩大,使其满足用户新的需要。关闭准则:对软件代码的批改该当是关闭的(Close for Modification),即在批改软件的同时,不要影响到零碎原有的性能,所以该当在不批改原有代码的根底上实现新的性能。也就是说,在减少新性能的时候,新代码与老代码该当隔离,不能在同一个类、同一个办法中。后面的设计,在实现新性能的同时,新代码与老代码在同一个类、同一个办法中了,违反了“开闭准则”。怎样才能既满足“开闭准则”,又可能实现新性能呢?在原有的代码上你发现什么都做不了!难道“开闭准则”错了吗? ...

July 26, 2021 · 1 min · jiezi

关于ddd:PHP-事件溯源

本文转载自【何以解耦】:https://codedecoupled.com/php... 事件溯源(Event Sourcing)是畛域驱动设计(Domain Driven Design)设计思维中的架构模式之一。畛域驱动设计是面向业务的一种建模形式。它帮忙开发者建设更贴近业务的模型。 在传统的应用程序中,咱们将状态贮存在数据库中,当状态产生扭转时,咱们即时更新数据库中绝对应的状态值。事件溯源则采纳一种截然不同的模式,它的外围是事件,所有的状态都来源于事件,咱们通过播放事件来获取利用中的状态,所以它叫事件溯源。 在本文中,咱们将使用事件溯源模式编写一个简化的购物车,以此合成事件溯源的几个重要组成概念。咱们也将应用 Spatie 的事件溯源库来防止反复造轮。 在咱们的案例中,用户能够增加,删除以及查看购物车内容,同时它具备两个业务逻辑: 购物车不可增加超过 3 种产品。当用户增加第 4 种产品时,零碎将主动收回一个预警邮件。要求以及申明本文应用 Laravel 框架。本文应用特定版本 spatie/laravel-event-sourcing:4.9.0 以防止不同版本之间的语法问题。本文并非手把手的分步教程,你必须有肯定 Laravel 根底才能够了解本文,请防止咬文嚼字,关注架构模式的组成构造。本文的重点是论述事件溯源的核心思想,此库中对事件溯源的实现形式并非惟一计划。畛域事件(Domain Event)事件溯源中的事件被称为畛域事件,与传统的事务事件不同,它有以下几个特点: 它与业务非亲非故,所以它的命名往往夹带业务名词,而不应该与数据库挂钩。比方购物车削减商品,对应的畛域事件应该是 ProductAddedToCart, 而不是 CartUpdated。它是指产生过的事件,所以它肯定是过来式,比方 ProductAddedToCart 而不是 ProductAddToCart。畛域事件只可追加,不能够删除或者更改,如果须要删除,咱们须要应用具备删除成果的畛域事件,比方 ProductRemovedFromCart。依据以上信息,咱们构建三种畛域事件: ProductAddedToCart:<?phpuse Spatie\EventSourcing\StoredEvents\ShouldBeStored;class ProductAddedToCart extends ShouldBeStored{ public int $productId; public int $amount; public function __construct(int $productId, int $amount) { $this->productId = $productId; $this->amount = $amount; }}ProductRemovedFromCart:<?phpuse Spatie\EventSourcing\StoredEvents\ShouldBeStored;class ProductRemovedFromCart extends ShouldBeStored{ public int $productId; public function __construct(int $productId) { $this->productId = $productId; }}CartCapacityExceeded:<?phpuse Spatie\EventSourcing\StoredEvents\ShouldBeStored;class CartCapacityExceeded extends ShouldBeStored{ public array $currentProducts; public function __construct(array $currentProducts) { $this->currentProducts = $currentProducts; }}事件 ProductAddedToCart 和 ProductRemovedFromCart 别离代表商品退出购物车以及被从购物车中移除,事件 CartCapacityExceeded 代表购物车中商品超标,这是咱们后面提到的业务逻辑之一。 ...

July 11, 2021 · 2 min · jiezi

关于ddd:DDD微服务框架xtooncloud

xtoon-cloud基于畛域驱动设计(DDD)并反对SaaS平台的微服务开发框架. 码云源码:https://gitee.com/xtoon/xtoon-cloud官网 |在线体验 |前端开源框架 | 为何抉择xtoon-cloud解决编写过程式和事务代码,造成前期保护逻辑凌乱、保护老本高的痛点;边界标准易维持,外围业务逻辑内聚在畛域内,低耦合,高内聚,易于长期保护;网上根本讲的都是DDD的实践很少有讲怎么落地,xtoon-cloud提供了残缺落地计划和企业级微服务架构;能够疾速开发,框架提供了系统管理和组织架构等外围模块;反对多租户的SaaS平台;技术交换如果有什么问题或倡议能够 提ISSUE 或 加群(QQ:130950009),交换技术,分享教训。 如果你解决了某些bug,或者新增了一些性能,欢送 奉献代码,感激不尽~ 大家多点 ⭐Star 反对下。 技术选型根底框架:Spring Cloud Alibaba网关:Spring Cloud Gateway鉴权认证:Spring Cloud Security+JWT服务监控:spring-boot-admin流量管制:Sentinel注册配置核心:NacosRPC:dubbomybatis-plus次要模块登录注册:账号、手机号验证登录,租户注册;用户治理:用户新增,调配角色,禁用等;角色治理:角色新增,查看,保护菜单等;菜单治理:树形菜单治理,可配置菜单和按钮权限等;租户治理:租户列表,禁用等;日志治理:记录操作日志记录和查问;我的项目构造xtoon-could├──doc 文档│ ├─db sql│ └─yaml 配置文件│ ├─xtoon-common 公共模块│ ├─xtoon-common-core 外围公共模块│ ├─xtoon-common-log 日志公共模块│ ├─xtoon-common-mybatis mybatis公共模块│ ├─xtoon-common-redis redis公共模块│ ├─xtoon-common-swagger swagger公共模块│ ├─xtoon-common-tenant 多租户公共模块 │ └─xtoon-common-web web公共模块 │ ├─xtoon-ops 运维服务│ ├─xtoon-auth-server 认证服务│ ├─xtoon-gateway-server 网关 │ ├─xtoon-monitor-server 衰弱监控服务 │ ├─xtoon-register-server 注册配置核心│ └─xtoon-sentinel-server 流量管制│ ├─xtoon-service 业务服务│ └─xtoon-sys 系统管理畛域│ ├─xtoon-sys-interface 系统管理接口│ └─xtoon-sys-server 系统管理服务│ 部署部署形式拉取前后端我的项目代码;装置必要的环境:jdk1.8+,mysql5.7+,redis,nodejs等;mysql新建两个库,导入doc上面的sql文件;批改老本地环境配置:mysql,redis;启动前后端我的项目,拜访地址;启动程序xtoon-register-server(注册配置核心)xtoon-sys-server(用户服务)xtoon-auth-server(认证服务)xtoon-gateway-server(网关服务)xtoon-monitor-server(监控服务)xtoon-sentinel-server(流量管制服务)xtoon-could-element(前端我的项目)核心理念六边形实践Alistair Cockburn提出了六边形架构,又被称为端口和适配器架构。察看上图咱们发现,对于外围的应用程序和畛域模型来说,其余的底层依赖或实现都能够形象为输出和输入两类。组织关系变为了一个二维的内外关系,而不是高低构造。每个io与应用程序之前均有适配器实现隔离工作,每个最外围的边都是一个端口。基于六边形架构设计的零碎是DDD谋求的最终状态。 数据驱动和畛域驱动比照畛域驱动设计与之前的零碎设计开发过程有很大的不同: 就在于零碎的参加角色,产品、开发、测试等,须要造成一套通用语言;在于方案设计不再把db设计放在一个外围问题去解决,更加专一于业务模型自身,进行畛域、业务聚合的设计,畛域层的聚合及实体才是整个零碎的核心内容;真正的面向对象编程,由过程式的事务脚本形式,转变为真正的面向对象。分层架构 ...

June 29, 2021 · 1 min · jiezi

关于ddd:供应链商品域DDD实践

简介: DDD是一套方法论,实际是否胜利,不仅仅是个技术问题,更是执行贯彻实施的问题。本文将就DDD的基本概念和DDD的施行进行分享。 作者 | 侧帽起源 | 阿里技术公众号 前言供应链商品域DDD实际工夫不长,在实际过程也碰到了不少问题,有些找到了答案,有些还是在摸索中。最近很荣幸受邀在供应链服务与翻新团队做了一次分享,也想在这里把一些教训和想法分享给大家,借此抛砖引玉。 DDD是一套方法论,实际是否胜利,我感觉不仅仅是个技术问题,更是执行贯彻实施的问题。 本文内容次要有两局部,DDD基本概念和DDD施行。基本概念包含通用语言、分层架构、DDD因素、边界上下文,DDD施行包含畛域常识提取办法、思考形式的转变,在其中会交叉一些商品案例。 一 软件复杂性是什么?在开始DDD前,咱们应该先答复的一个问题,咱们为什么须要DDD?DDD是简单软件应答之道,所以咱们来一起看看,软件的复杂度到底在哪里? 在阿里两年,我感触很深的一个点是,咱们不能继续交付一直演进的简单软件,所以有1.0、2.0、3.0很多的版本,1.0搞不上来了,开始2.0,2.0也搞不上来了,开始3.0,一直循环。 阿里体系复杂度我看来是理解力、不可预测、合作力挑战三个方面。 1 理解力挑战需要规模宏大,业务数量和类型一直增多,业务互相耦合,不同业务相互影响。供应链有20多个行业,经销、代销、一盘货等各种商业模式,有跨境进口、国内业务、国际化业务,这些纵横导致系统复杂度大幅晋升。业务零碎多,边界划分不清,零碎间依赖简单。如供应链商品和共享SELL、AIC和IPM,始终都有边界问题,一个大我的项目过去,边界问题就得探讨上好几天。系统结构简单,因为应答高并发、高稳定性等,功能性代码与非功能性代码混合,如业务代码混杂着各种兜底逻辑、灰度逻辑、重试等等,100行代码,可能业务代码不到30行。2 不可预测性挑战商业环境复杂多变,商业流程、规定多变。商业环境变动快,往年国际化、智能商业路由、考拉交融一下子都来了,在设计上很难后期都布局好。变动不可预测,软件系统变动也不可预测,带来设计挑战。3 合作力挑战大部分需要横跨多个团队,需要传递低效,须要重复沟通,计划产出效率低。团队角色多,业务概念多,没有对立语言,大家了解容易呈现偏差。二 Why DDD?DDD设计适合的畛域模型来映射事实中的业务,来无效地解决畛域中的外围的简单问题,是对OOAD的扩大和延长,其解决之道: 分而治之,控制规模。关注点拆散,应答理解力挑战,畛域模型与存储模型拆散,业务复杂度与技术复杂度拆散。分层架构、拆散外围,放弃构造清晰,应答不可预测性挑战。对立语言,应答合作力挑战。三 DDD外围 1 通用语言通用语言是提炼畛域常识的产出物,取得对立语言就是需要剖析的过程,也是团队中各个角色就零碎指标、范畴与具体性能达成统一的过程。 畛域语言团队专有,负责解释和保护,雷同名称概念,跨出这个团队,了解能够齐全不一样。 领域专家、产品经理、开发人员独特的语言,这种语言是将领域专家和技术人员分割在一起的纽带。 在各种文档和平时沟通中,放弃概念对立,特地提一下,做一个中文对照, 把概念和代码连接起来,在代码做到概念名称对立,缩小混同。 通用语言价值: 定义公共术语,缩小概念混同。沟通达成统一的提前,打消歧义和了解偏差,晋升需要和常识消化的效率。概念和代码的对立语言,连贯概念和实现。 2 分层架构DDD第二个外围是分层架构,拆散模型。优良的架构应该是什么样子?关注点是拆散的,能够分而治之,可测试性好。 一个人同时要做多件事件的时候,不免慌手慌脚。代码也一样,一段代码要解决各种事件的时候,也会乱成一团,所以咱们要合成开来,各个击破。 商品域畛域模型,在分层架构中的地位,如下: CQRS模式:畛域模型在应用层上面,command才走畛域模型;查问和搜寻服务不走。tunnel层,对接db、内部数据资源拜访,畛域和模型解耦,相似DAO。内部通过SPI和模型交互,六边形的adapter模式。3 DDD因素1)实体:有id,有生命周期和状态。有属性,有行为。内部事件会触发他的行为和状态变动。 实体和vo的辨别,vo属性不能批改,应用final润饰。vo为表白模型减负,如商品有100多个属性,铺平开不能体现结构化,不能体现分层分类,将类似描述性属性分组封装成一个个vo。 2)为什么须要service,如批量操作多个实体、跨实体操作,如商品复制,转账。 商品域的工程架构: serivce职责是:实体创立,长久化,跨实体操作等。不同层应用不同数据对象,tunnel应用dataobjects,面向存储,须要和实体互相转换。实体间有关系,能够动静加载关联对象;dataobjects只有数据,没有行为,个别也不会有关系。 4 边界上下文边界 = 域或子域。畛域对象在畛域内才有确切的含意。出了这个边界,不能确保还是这个含意,如苹果。语言是有上下文的。在不同的上下文中,职责和工作不一样。人有多个角色,在家里是爸爸、在公司是小二,职责和工作不一样。 上下文映射: 有了边界,那么畛域如何输入价值呢?一个齐全关闭的零碎没有任何价值。罕用的形式有:共享内核,防腐层等。防腐层:商品上游提供spi,spi不是间接对外开放畛域模型,建设一层凋谢视图。洽购域建设防腐层,收口商品的变更对本域影响。 四 DDD施行1 DDD施行的挑战辨认和提炼畛域常识,并体现在模型代码上,强调一次“并体现在模型代码上”!防腐,放弃模型一直演进,须要继续投入,保障DDD贯彻执行。人的转变,开发思考形式的转变。 2 什么是畛域常识?畛域常识有分层分类,平台通用商业规定,是畛域模型次要输出,商家个性化不能下沉到畛域模型层。 3 畛域常识提炼,需要和链路5W1H分析法两阶段剖析:用户故事、链路和边界剖析。 前3W刻画用户故事,用户要什么,为什么要?举个例子,我作为洽购小二,须要商品库存为0主动下架,因为有超卖危险,客户会投诉。前面的When、Where、How是链路和边界剖析,触发的条件是什么,要实现这个性能须要哪些域参加进来,别离提供什么能力?通过这个剖析,获取用户需要,和全链路分工。 4 畛域常识提炼,结构化分析APP层至上而下过程剖析,模型层自下而上剖析相结合。能力下沉放弃模型一直演进,能力下沉规范:复用、内聚。 5 思考形式的转变畛域驱动,在模型阶段不会关注数据设计、不会关注存储、不会关注音讯怎么发,业务和技术视角关注点做了拆散。 五 商品域实际相干商品域工程架构: ...

June 23, 2021 · 1 min · jiezi

关于ddd:深入理解领域驱动设计中的聚合

简介: 聚合模式是 DDD 的模式构造中较为难于了解的一个,也是 DDD 学习曲线中的一个要害阻碍。正当地设计聚合,能清晰地表述业务一致性,也更容易带来清晰的实现,设计不合理的聚合,甚至在设计中没有聚合的概念,则相同。 作者 | 嵩华起源 | 阿里技术公众号 聚合模式是 DDD 的模式构造中较为难于了解的一个,也是 DDD 学习曲线中的一个要害阻碍。正当地设计聚合,能清晰地表述业务一致性,也更容易带来清晰的实现,设计不合理的聚合,甚至在设计中没有聚合的概念,则相同。 聚合的概念并不简单。本文心愿能回到聚合的实质,对聚合的定义和实操给出一些有价值的倡议。 一 聚合解决的外围问题是什么咱们先来看一下在 DDD Reference 中对于聚合的定义。 将实体和值对象划分为聚合并围绕着聚合定义边界。抉择一个实体作为每个聚合的根,并仅容许内部对象持有对聚合根的援用。作为一个整体来定义聚合的属性和不变量,并把其执行责任赋予聚合根或指定的框架机制。 这是典型的“模式语言”,阐明了聚合是什么,聚合根(aggregation root)是什么,以及如何应用聚合。然而,模式语言的问题在于适度精炼,如果读者曾经相熟了这种模式,很容易看懂,然而最须要看懂的、那些尚不够相熟这些概念的人,却容易感到不知所云。为了能深刻了解一个模式的实质,咱们还是要回到它试图解决的外围问题上来。 在软件架构畛域有一句名言: “架构并不禁零碎的性能决定,而是由零碎的非功能属性决定”。 这句话直白的解释就是:如果不思考性能、健壮性、可移植性、可修改性、开发成本、工夫束缚等因素,用任何的架构、任何的办法,零碎的性能总是能够实现的,我的项目总是能开发实现的,只是开发工夫、当前的保护老本、性能扩大的容易水平不同罢了。 当然事实绝非如此。咱们总是心愿零碎在可了解、可保护、可扩大等方面体现良好,从而多快好省的达成零碎背地的业务指标。然而,在事实中,不合理的设计办法有可能减少零碎的复杂性。咱们先来看一个例子: 假如问题畛域是一个企业外部的办公用品洽购零碎。 企业的员工能够通过该零碎提交一个洽购申请,一个申请蕴含了若干数量、若干类型的办公用品(称为洽购项)。(1)主管负责对洽购申请进行审批。(2)审批通过后,零碎会依据提供商不同,生成若干订单。(3)对同一个问题,存在若干种不同的设计思路,例如以数据库为核心的设计、面向对象的设计和“正确的 OO”的 DDD 的设计。 如果采纳以数据库为核心的建模形式,首先会进行数据库设计——我的确看到还有许多团队依然在采取这种办法,破费大量的工夫进行数据库构造的探讨。为了防止图表过大,咱们仅仅给出了和洽购申请相干的表格。构造如下图所示: 图1 数据库视角下的设计 如果间接在数据库这么低的设计档次上思考问题,除了数据库的设计繁琐易错,更重要的是会面临一些比较复杂的业务规定和数据一致性保障的问题。例如: 如果洽购申请被删除,则相应的和该洽购申请相干的洽购项以及它们之间的关联都须要被删除——在数据库设计中,这种束缚能够通过数据库外键来保障。如果多个用户在对具备相干关系的数据进行并发解决,则可能波及到简单的锁定机制。例如,如果审批者正在对洽购申请进行审批,而洽购提交者正在对洽购项进行批改,则就有可能导致审核的数据是过期数据,或者导致洽购项更新的失败。如果同时更新某些相关联的数据,也可能面临局部更新胜利导致的问题——在数据库设计中,这类约束则须要通过 transaction 来保障。的确,每个问题都是有解决方案的,然而,第一,对于模型的探讨过早地进入了实现畛域,和业务概念脱开了分割,不便于继续地和业务人员合作;第二,技术细节和业务规定的细节纠缠在一起,很容易顾此失彼。有没有一种计划,能够让咱们更多的聚焦于问题畛域,而不是深陷到这种技术细节中? 面向对象技术和 ORM(对象-关系映射)有助于咱们进步问题的形象层级。在面向对象的世界中,咱们看到的构造是这样的: 图2 传统OO视角下的设计 面向对象的形式进步了形象层级,疏忽了不必要的技术细节,例如曾经不须要关怀外键、关联表这些技术细节了。咱们须要关怀的模型元素的数量缩小了,复杂性也相应缩小了。只是,业务规定如何保障,在传统的面向对象办法中并没有严格的实现束缚。例如: 从业务角度来看,如果洽购申请的审批曾经通过,对洽购申请的洽购项进行再次更新应该是非法的。然而,在面向对象的世界中,你却没法阻止程序员写出这样的代码: ...PurchaseRequest purchaseRequest = getPurchaseRequest(requestId);PurchaseItem item = purchaseRequest.getItem(itemId);item.setQuantity(1000);savePurchaseItem(item);语句 1 获得了一个洽购申请的实例;语句 2 获得了该申请中的一个条目。语句 3 和 4 批改了洽购申请条目并保留。如果洽购申请曾经审批通过,这种批改岂不是能够轻易冲破洽购申请的估算? 当然,程序员能够在代码中退出逻辑查看来保障一致性:在批改或保留申请条目前总是查看 purchaseRequest 的状态,如果状态不为草稿就禁止批改。然而,思考到 PurchaseItem 对象能够在代码的任何地位被取出来,且可能在不同的办法间传递,如果 OO 设计不当,就可能导致该业务逻辑扩散到各处。没有设计束缚,这种查看的实现并不是一件容易的事件。 ...

June 17, 2021 · 2 min · jiezi

关于DDD:面向服务体系结构的领域驱动设计

【注】本文译自:https://www.thoughtworks.com/... 这篇文章是对于软件设计的抉择。特地是大型零碎,这些零碎可能会以服务端点的模式分为多个可部署的对象。我不会特地议论服务端点设计,然而我想探讨创立多个服务利用的构思阶段。 当咱们面对简单的问题时,咱们通常试图了解简单的单个局部。通过合成问题,咱们将其变成为更易于了解和治理的局部。 正如在许多产品/项目管理周期中所形容的,对于现实生活中的问题,这通常是由本能驱动的。 咱们不会应用公式来理解去一个须要签证的国家须要做什么。咱们晓得咱们须要签证能力旅行,咱们缓缓把握须要的文件文件,须要填写哪些表格以及如何填写这些表格。当咱们执行其中一个步骤时,咱们不会将流程的所有细节都牢记在心,而只是要做手头的工作。这与要实现的工作的大小无关。潜在的实在规范是对于工夫或进度、咱们的执行力、咱们的认知能力及其与工作相熟水平的关系,甚至可能是执行这些工作的物理地位( 领事馆与 Photoshop 等)。 在软件开发畛域并没有什么不同。多年来,瀑布式的配方已被利用到软件开发过程中,最终,次要是基于启发式和基于教训的评估技术(打算扑克<Planning Poker>、T - 恤尺寸<T-shirt size>)和麻利过程。在现实生活中,咱们试着不去详述整个过程,而是通过观察咱们的最新体现来尝试和了解整个旅程。 同样实用于咱们针对问题建模的软件。咱们开始将它们合成为不同的利用的是方便管理单个利用,以更少的依赖关系更快地开发和部署,最初带来更多的技术抉择自在。咱们意识到,咱们无奈制订出适宜所有人的残缺流程。咱们着眼于各个局部,并意识到咱们在设计模式或技术方面的个体教训,并尝试利用其中最好的抉择。 了解和解决复杂性的一个乏味的软件设计技术是畛域驱动设计(DDD)。畛域驱动设计提倡基于与咱们的用例相干的业务事实进行建模。因为 DDD 办法当初曾经过期,而且宣传程度正在降落,咱们许多人都遗记了 DDD 办法的确有助于了解手头的问题,并朝着对解决方案的广泛了解来设计软件。在构建利用DDD 会以域和子域的模式探讨的问题。它将问题的独立步骤/畛域形容为边界上下文,强调应用一种通用语言来探讨这些,并增加了许多技术概念,如实体、值对象和聚合根规定以反对实现。有时,这些技术规定被视为是施行 DDD 的硬阻碍,但最终,人们往往会遗记重要的局部是依据业务问题组织代码工件,并应用与(1)雷同的通用语言。 设计为服务利用的边界上下文 我想议论的架构格调与微服务十分类似。它是对于将单体利用拆散为多个独立的服务利用,或者从一开始就在边界上下文(DDD概念)的帮忙下独自开发它们。 有许多资源都强调了微服务叙述中更细粒度的服务的长处。越来越多的文章、博客和其余内容是对于陷阱和在向细粒度服务过渡之前或过渡期间应该领有的安全网的。我将尽量不反复微服务或其余反对元素的益处,这些是迁徙到这样的体系结构中所须要。我不想强调后果服务的“小型”性质,而是想强调如何通过应用领域驱动的设计概念来更好地拆散这些服务。 让咱们应用一个实在的示例来实现咱们的想法-借记卡/信用卡获取域。这个畛域能够(可怜的是,很多时候都是这样)作为一组单体利用来实现。咱们领有多个利用的惟一起因是因为不同的利用中的存在严格的技术限度(例如心愿执行批处理)。 我所看到的大多数胜利的架构都意识到,通过数据库进行集成是一种蹩脚的实际,因为它使技术利用和业务职责之间的界线变得含糊,使业务逻辑透露到数据库中,并通过增加更多的应用服务器来阻止程度扩大。因而,以单体利用的的服务集成的模式倒退到更好的架构。 当初,利用之间的界线更加清晰了。然而,您能够看到,依然存在暗藏的数据库交互,这一次是在各个利用外部产生的。我称它们为暗藏的,因为通常一开始它们通常很难被留神到。随着工夫的流逝,代码的纠缠将使原先拆散的业务流程人为地关联起来,并在业务开发中引入更多的摩擦,因为这种共置托管须要联结部署独自的性能,这可能会减慢速度。 如果您幸运地有一个畛域模型来领导的话,则领域建模可帮忙您辨认和拆散简单的实现。如果您还没有现有利用的域模型(在大多数状况下通常是这样),则无需遍历代码以理解不同的职责,而是构建域模型并将性能映射到手边的利用可能是一个更好的办法。这既能节省时间,又能防止被细节吞没的危险。此外,如果业务与开发团队之间存在差距(这可能是域模型最后不存在的次要起因),那么探讨域模型并映射到现有利用的性能将有助于放大这一差距。 咱们设计演进的下一步是将域边界拆散反映到咱们的架构以及边界上下文中。一个域具备多个边界上下文意味着在同一域中能够有多个服务利用运行。有了适当的域模型,潜在的分离点就更可见了,这使咱们能够从潜在的更细粒度的利用中受害(诸如独自发行和版本控制的益处,具备更多功能驱动的纯服务端点的后劲等,其中大多数曾经在微服务文章中进行了探讨)。只管许多微服务探讨都围绕技术不可知论和开发标准(防止/毁坏整体),但对于咱们大多数人所从事的利用而言,十分有价值的一项是畛域和设计方面。一旦过渡到微服务架构(借助域模型),DDD 和更细粒度的服务便能够协同工作、相互支持。 这还将为团队提供肯定水平的独立性,更欠缺的服务性能以及更拆散的交互,如许多微服务文章所述. 同样,从咱们的示例信用卡付款获取域中能够看出,这并不是咱们的服务能够做到的最细粒度的拆散。相同,这是在咱们的畛域常识所领导下最有意义的拆散。重点不是规模,而是业务能力。我置信这是“正确的 SOA”,正如许多圈子所说的那样。

May 20, 2021 · 1 min · jiezi

关于ddd:殷浩详解DDD领域层设计规范

简介: 在一个DDD架构设计中,畛域层的设计合理性会间接影响整个架构的代码构造以及应用层、基础设施层的设计。然而畛域层设计又是有挑战的工作,特地是在一个业务逻辑绝对简单利用中,每一个业务规定是应该放在Entity、ValueObject 还是 DomainService是值得用心思考的,既要防止将来的扩展性差,又要确保不会适度设计导致复杂性。明天我用一个绝对轻松易懂的畛域做一个案例演示,但在理论业务利用中,无论是交易、营销还是互动,都能够用相似的逻辑来实现。 作者 | 殷浩起源 | 阿里技术公众号 在一个DDD架构设计中,畛域层的设计合理性会间接影响整个架构的代码构造以及应用层、基础设施层的设计。然而畛域层设计又是有挑战的工作,特地是在一个业务逻辑绝对简单利用中,每一个业务规定是应该放在Entity、ValueObject 还是 DomainService是值得用心思考的,既要防止将来的扩展性差,又要确保不会适度设计导致复杂性。明天我用一个绝对轻松易懂的畛域做一个案例演示,但在理论业务利用中,无论是交易、营销还是互动,都能够用相似的逻辑来实现。 一 初探龙与魔法的世界架构1 背景和规定素日里看了好多庄重的业务代码,明天找一个轻松的话题,如何用代码实现一个龙与魔法的游戏世界的(极简)规定? 根底配置如下: 玩家(Player)能够是兵士(Fighter)、法师(Mage)、龙骑(Dragoon)怪物(Monster)能够是兽人(Orc)、精灵(Elf)、龙(Dragon),怪物有血量武器(Weapon)能够是剑(Sword)、法杖(Staff),武器有攻击力玩家能够配备一个武器,武器攻打能够是物理类型(0),火(1),冰(2)等,武器类型决定挫伤类型。攻打规定如下: 兽人对物理攻击挫伤减半精灵对魔法攻打挫伤减半龙对物理和魔法攻打免疫,除非玩家是龙骑,则挫伤加倍2 OOP实现对于相熟Object-Oriented Programming的同学,一个比较简单的实现是通过类的继承关系(此处省略局部非核心代码): public abstract class Player { Weapon weapon}public class Fighter extends Player {}public class Mage extends Player {}public class Dragoon extends Player {}public abstract class Monster { Long health;}public Orc extends Monster {}public Elf extends Monster {}public Dragoon extends Monster {}public abstract class Weapon { int damage; int damageType; // 0 - physical, 1 - fire, 2 - ice etc.}public Sword extends Weapon {}public Staff extends Weapon {}而实现规定代码如下: ...

May 20, 2021 · 10 min · jiezi

关于DDD:领域驱动设计DDD

【注】本文译自: https://www.geeksforgeeks.org... 畛域驱动设计(Domain-Driven Design)是程序员 Eric Evans 于 2004 在他的《 畛域驱动设计:解决软件外围中的复杂性》一书中提出的一个概念。 这是一种自顶向下的软件设计办法。首先,让咱们尝试重点介绍一下在这种状况下畛域的含意。 什么是畛域? 在软件开发的上下文中,“域”指的是业务。在利用程序开发过程中,通常应用术语域逻辑或业务逻辑。基本上,业务逻辑是利用程序逻辑所围绕的常识畛域。 应用程序的业务逻辑是一组规定和领导准则,用于解释业务对象应如何互相交互以解决建模数据。留神: 软件工程畛域的畛域是要在其上构建应用程序的业务。 畛域驱动设计: 假如咱们的软件曾经应用了所有最新技术堆栈和基础设施,这样的软件设计架构十分棒,然而当咱们在市场上公布这个软件时,最终还是要由最终用户来决定咱们的零碎是否优良。另外,如果零碎不能解决业务需要,对任何人都没有用途;不论它看起来有多丑陋,或者它的基础设施有多好。依据 Eric Evans 的说法,当咱们在开发软件时,咱们的重点不应该次要放在技术上,而应该次要放在业务上。记住: “客户的工作不是晓得他们想要什么“---史蒂夫·乔布斯 畛域驱动设计波及两种设计工具,一种是策略设计工具,另一种是战术设计工具。程序员或开发人员通常解决战术设计工具,但如果咱们有策略设计工具的常识和良好的了解,它将帮忙咱们构建好的软件。 Spring 数据家族下的大多数框架都是依据畛域驱动的设计办法构建的。 策略设计: 策略设计工具帮忙咱们解决所有与软件建模相干的问题。它是一种相似于面向对象设计的设计办法,在面向对象设计中,咱们被迫从对象的角度思考问题。在策略设计方面,咱们被迫从环境的角度来思考。 上下文(Context): 咱们能够把这个词看作是一个英语单词,它指的是某一事件、事件、陈说或想法的状况,它的意思能够依据这些状况来确定。 除了上下文之外,策略设计还探讨了模型、泛在语言和边界语境。这些是畛域驱动设计的策略设计中罕用的术语。让咱们逐个了解。 模型: 充当外围逻辑并形容畛域的选定方面。它用于解决与该业务无关的问题。通用语言: 所有团队成员应用的一种公共语言,用于连贯团队围绕畛域模型的所有流动。与领域专家和团队成员交谈时,能够将其视为对类、办法、服务和对象应用通用动词和名词。边界上下文: 指的是上下文的边界条件。它是对边界的形容,并充当一个阈值,在这个阈值中定义并实用于特定的域模型。 战术设计: 战术设计探讨实现细节,即建模畛域。它通常会解决有界上下文中的组件。咱们可能据说过或应用过诸如服务、实体、存储库和工厂之类的货色。它们都是通过域驱动设计发明并风行的。战术设计过程产生在产品开发阶段。 让咱们探讨一些重要的战术设计工具。 这些工具是高级概念,可用于创立和批改域模型。 实体: 基于面向对象准则工作的程序员可能晓得类和对象的概念。在这里,实体是具备某些属性的类。这些类的实例具备全局标识,并且在整个生命周期中都放弃雷同的标识。请记住,属性状态可能会发生变化,但身份永远不会扭转。简而言之,实体能够实现一些业务逻辑,并且能够应用 ID 进行惟一标识。在编程的上下文中,它通常在 DB 中作为行长久保留,并且由值对象组成。值对象: 它是不可变的轻量级对象,没有任何标识。值对象通过执行简单的计算,将沉重的计算逻辑与实体隔离开来,从而升高了复杂性。 在上图中,User 是一个实体,Address 是一个值对象,地址能够更改很屡次,但用户的身份证号永远不会更改。每当地址更改时,都会实例化一个新地址并将其调配给用户。服务: 服务是无状态的类,能够适宜实体或值对象以外的其余中央。简而言之,服务是一种性能,存在于实体和值对象之间的某个地位,但它既不与实体相干,也不与值对象相干。聚合: 当咱们有更大的我的项目时,对象图也变得更大,更大的对象图更难保护。聚合是位于单个事务边界下的实体和值的汇合。基本上是聚合,管制变动,有一个根实体叫做聚合根。根实体以聚合的形式治理其余实体的生命周期。 在下面的示例中,如果根实体 User 或 Order 被删除,则与该根实体关联的其余实体将毫无用处,并且该关联的信息也将被删除。这意味着聚合在实质上总是统一的,这是在域事件的帮忙下实现的。生成域事件是为了确保最终的一致性。 在下面的例子中,如果用户的地址曾经扭转,那么它必须同样反映在订单上。为此,咱们能够触发一个从 User 到 Order 的域事件,以便 Order 更新地址,这样咱们就有了最终的一致性,Order 也将最终统一。 聚合和聚合根的其余例子能够是对帖子的评论、问答细节、银行事务细节等。ORM 工具如 hibernate 在创立一对多或多对一关系时应用了大量聚合。工厂和存储库: 工厂和存储库用于解决聚合。工厂帮忙治理聚合生命周期的开始,而存储库帮忙治理聚合生命周期的两头和末端。工厂帮忙创立聚合,而存储库帮忙长久化聚合。咱们应该总是为每个聚合根创立存储库,而不是为所有实体创立存储库。 工厂是 GoF 的设计模式,工厂是有用的,但在聚合规定的上下文中不是强制性的。 ...

May 18, 2021 · 1 min · jiezi

关于ddd:聊聊eventhorizon的Aggregate

序本文次要钻研一下eventhorizon的Aggregate Aggregateeventhorizon/aggregate.go type Aggregate interface { // Entity provides the ID of the aggregate. Entity // AggregateType returns the type name of the aggregate. // AggregateType() string AggregateType() AggregateType // CommandHandler is used to handle commands. CommandHandler}type Entity interface { // EntityID returns the ID of the entity. EntityID() uuid.UUID}type CommandHandler interface { HandleCommand(context.Context, Command) error}// AggregateType is the type of an aggregate.type AggregateType string// String returns the string representation of an aggregate type.func (at AggregateType) String() string { return string(at)}Aggregate接口内嵌了Entity及CommandHandler,定义了AggregateType办法AggregateStoreeventhorizon/aggregate.go ...

March 31, 2021 · 3 min · jiezi

关于ddd:得物技术出价组DDD分层模型总结

在五彩石我的项目启动伊始,就决定采纳畛域驱动模型来设计业务框架,然而因为过后好多人对该模型的不相熟,以及对一些设定的难以了解,导致初版的代码还是存在不少mvc的影子的。随着大家对该模型的逐步理解,统一认为须要对业务代码做一些优化,于是组外在8月份启动了二次DDD革新的外部迭代,通过一直的灰度放量,近期曾经全量放开了新版业务逻辑。 对于对DDD的了解,每个人都能够是不同的,并且根据不同的了解所写的业务框架也是不同的,只有在肯定的范畴内可能逻辑自洽,那是没问题的。在启动DDD革新前,组内也做过一些分享,制订了一些标准,本文就是对组内这些标准的一个总结。 interfaces (用户接口层)流量申请入口。用户接口层负责向用户显示信息和解释用户指令。这里的用户可能是:用户(H5/APP)、程序(API)、音讯队列(MQ)、超时核心回调,自动化测试和批处理脚本等等。 在最后的设计中,只有用户的调用是放在这一层的,其余调用是放在了application,这样导致了两个后果,分层不明确和入口扩散。     所以本次将所有的流量入口全副放在了interfaces做收口。 application(应用服务层)应用层连贯用户接口层和畛域服务层,它是很薄的一层,次要职能是协调畛域层多个聚合实现服务的组合和编排。应用服务层是很薄的一层,实践上不应该有业务规定或逻辑,次要面向用例和流程相干的操作。但应用层又位于畛域层之上,因为畛域层蕴含多个聚合,所以它能够协调多个聚合的服务和畛域对象实现服务编排和组合,合作实现业务操作。 应用层也是微服务之间交互的通道,它能够调用其它微服务的应用服务,实现微服务之间的服务组合和编排。 在设计和开发时,不要将本该放在畛域层的业务逻辑放到应用层中实现。因为宏大的应用层会使畛域模型失焦,工夫一长你的微服务就会演变为传统的三层架构,业务逻辑会变得凌乱。 举个精简的例子,假如出价分四步,1、调用商品接口校验商品是否上架,2、出价规定校验,3、数据落地,4、调用库存接口生成库存; 那么应用层的次要性能就是编排这四步,不做业务解决。其中第1步和第4步放到根底层解决(根底层封装内部RPC),第2步在规定域执行,第3步在出价域执行。 在初版的设计探讨中,有过一个定义是查问业务能够穿畛域层,中转根底层,理由是纯查问的业务没有简单的逻辑,仅仅是拿数据而已。但这也只是一个想法而已,通过理论的业务实际可知查问还是有很多的业务逻辑组装,而根底层是不解决业务逻辑的,拿到数据后只能在应用层做封装,导致了应用层过于厚重。同时也思考到一个正当的规范性,约定同样的流程当前,会更好的推动DDD。 domain(畛域服务层)畛域服务层是由多个业务职责繁多的聚合形成,实现外围的畛域逻辑。畛域层的作用是实现畛域外围业务逻辑,通过各种校验伎俩保障业务的正确性。畛域层次要体现畛域模型的业务能力,它用来表白业务概念、业务状态和业务规定。 畛域层蕴含聚合根、实体、值对象、畛域服务等畛域模型中的畛域对象。畛域模型的业务逻辑次要是由实体和畛域服务来实现的,其中实体会采纳充血模型来实现所有与之相干的业务性能。 什么是充血模型             与之对应的是贫血模型,咱们常常应用的MVC模型中,实体类只定义属性,并没有定义实体行为。这种将数据与业务逻辑拆散,其实是违反了 OOP 的封装个性,实际上是一种面向过程的编程格调。任意代码都能够批改实体的属性,那么实体的属性值就不受限制了。这其实是自下而上的设计思维,是SQL 驱动(SQL-Driven)的开发模式。              而充血模型则是将该实体所有行为也进行了定义(比方save,modify,remove等操作)。任何想要批改实体属性的操作,必须要通过实体自身来实现。这是一种自上而下的设计思维,由具体的业务驱动开发,不须要关怀底层的SQL实现。 实体和畛域服务在实现业务逻辑上不是同级的,当畛域中的某些性能,繁多实体(或者值对象)不能实现时,畛域服务就会出马,它能够组合聚合内的多个实体(或者值对象),实现简单的业务逻辑。 聚合根聚合根是一种更大范畴的封装,把一组在业务上不可分隔的实体和值对象聚合在一起,通过根实体的惟一标识对外提供能力。 实体实体是畛域中须要惟一标识的畛域概念。雷同的两个实体,如果惟一标识不一样, 那么即使实体的其余所有属性都一样,咱们也认为它们两个是不同的实体,比方同一个用户对同一个sku同价格的两个库存实体,除了主键ID外,其余均雷同,但这依然是两个实体。 同时,不应该给实体定义太多的属性或行为,而应该寻找关联,将一些属性或行为转移到其余关联的实体或值对象上。 比方Inventory实体,会存储一些商品信息(sku、spu等),因为商品信息是一个残缺的有业务含意的概念,所以,咱们能够定义一个Commodity对象,而后把Inventory实体中商品相干的信息转移到Commodity对象上。如果没有Commodity对象,而把这些商品信息间接放在Inventory对象上,并且如果对于一些其余的比方费用信息、仓库惟一码等信息也放到进去,会导致Inventory对象很凌乱,构造不清晰,最终导致它难以保护和了解。 值对象值对象就是下面所说的Commodity对象,并不是每一个值对象都必须有一个惟一标识,也就是说咱们不关怀对象是哪个,而只关怀对象是什么。以Commodity对象为例,如果有两个Commodity的spuId是一样的,咱们就会认为这两个Commodity是同一个。也就是说只有spuId一样,咱们就认为是同一个商品。 infrastructure(基础设施层)基础设施层是数据封装层,在这里获取各类数据,比方数据库,缓存,内部畛域,内部接口等。根底层是贯通除畛域层外所有层的,比拟常见的性能还是提供数据库长久化和内部畛域服务调用的。 根底层蕴含根底服务,它采纳依赖倒置设计,封装根底资源服务,实现应用层、畛域层与根底层的解耦,升高内部资源变动对利用的影响。 比如说,在传统架构设计中,因为下层利用对数据库的强耦合,很多的架构演进中最担心的可能就是换数据库了,因为一旦更换数据库,就可能须要重写大部分的代码,这对利用来说是致命的。那采纳依赖倒置的设计当前,应用层就能够通过解耦来放弃独立的外围业务逻辑。当数据库变更时,咱们只须要更换数据库根底服务就能够了,这样就将资源变更对利用的影响降到了最低。 依赖倒置 Domain 层不再间接依赖 Infrastructure 层,而是引入了一个适配器模式(Port/Adapter),应用DIP(Dependency Inversion Principle,依赖倒置)反转了 Domain 层和 Infrastructure 层的依赖关系,其关系如上图所示Domain 层以接口的形式凋谢端口,让Infrastructure层去实现,这样设计的有点是 Domain 层的演变和进化齐全是独立的,向上不受 Application 层影响,向下不受 Infrastructure 层影响。举个例子,畛域层是通过仓储接口(repository)获取根底资源的数据对象,仓储接口会调用仓储实现,具体的根底资源的数据处理过程是在仓储实现中实现的。这样做的益处是,防止将仓储实现的代码混入下层业务逻辑中。如果当前替换数据库,因为做了根底资源的共性的代码隔离,所以实现了应用逻辑与根底资源的解耦。在更换数据库时只须要更换仓储相干的代码就能够了,利用的逻辑不会受太大的影响。 拆散畛域(调用关系) 分层是为了各层独立演进的,下层应用上层定义的服务,而上层对下层无所不知,另外每一层对下层暗藏细节实现,依赖契约交互,独立层在技术计划调整时只有恪守契约则能够做到下层无感知迁徙,这样也不便各层的保护和标准化工作。DDD 有两种架构,严格分层架构和涣散分层架构。优化后的 DDD 分层架构模型就属于严格分层架构,任何层只能对位于其间接下方的层产生依赖。而传统的 DDD 分层架构则属于涣散分层架构,它容许某层与其任意下方的层产生依赖,倡议应用严格分层架构。对象流转interfaces 层: request、response ...

March 26, 2021 · 1 min · jiezi

关于ddd:聊聊cheddar的DomainEvent

序本文次要钻研一下cheddar的DomainEvent DomainEventCheddar/cheddar/cheddar-domain/src/main/java/com/clicktravel/cheddar/domain/event/DomainEvent.java public interface DomainEvent extends Event {}DomainEvent接口继承了Event接口AbstractDomainEventCheddar/cheddar/cheddar-domain/src/main/java/com/clicktravel/cheddar/domain/event/AbstractDomainEvent.java public abstract class AbstractDomainEvent extends AbstractEvent implements DomainEvent { public abstract String context(); @Override public final String type() { return context() + "." + getClass().getSimpleName(); }}AbstractDomainEvent继承了AbstractEvent,申明实现了DomainEvent接口,它申明了一个形象的context办法DomainEventHandlerCheddar/cheddar/cheddar-domain/src/main/java/com/clicktravel/cheddar/domain/event/DomainEventHandler.java public interface DomainEventHandler extends EventHandler<DomainEvent> {}public interface HighPriorityDomainEventHandler extends DomainEventHandler {}public interface LowPriorityDomainEventHandler extends DomainEventHandler {}DomainEventHandler接口继承了EventHandler接口,其泛型为DomainEvent;HighPriorityDomainEventHandler及LowPriorityDomainEventHandler接口继承了DomainEventHandler接口DomainEventPublisherCheddar/cheddar/cheddar-domain/src/main/java/com/clicktravel/cheddar/domain/event/DomainEventPublisher.java public class DomainEventPublisher extends EventPublisher<DomainEvent> { private static DomainEventPublisher instance; public static void init(final MessagePublisher<TypedMessage> messagePublisher) { instance = new DomainEventPublisher(messagePublisher); } private DomainEventPublisher(final MessagePublisher<TypedMessage> messagePublisher) { super(messagePublisher); } public static DomainEventPublisher instance() { if (instance == null) { throw new IllegalStateException("DomainEventPublisher not initialized"); } return instance; }}DomainEventPublisher继承了EventPublisher,其结构器接管MessagePublisher;它提供了init办法用于创立DomainEventPublisher,instance办法用于获取instance小结cheddar定义了DomainEvent接口及AbstractDomainEvent抽象类;DomainEventHandler接口继承了EventHandler接口,其泛型为DomainEvent;DomainEventPublisher继承了EventPublisher,其结构器接管MessagePublisher,其publishEvent办法最初通过MessagePublisher的publish来实现。 ...

March 25, 2021 · 1 min · jiezi

关于ddd:领域驱动实践二

文章内容全副来自张逸《畛域驱动设计实际(策略+战术)》上文 畛域驱动实际 演进后的分层架构代码模型需要:创立订单,发送邮件告诉,异步音讯告诉仓储零碎。 controller是北向网关,承当与用户界面的交互,同理,如果是dubbo,那么dubbo的api也能够视作是北向网关。 南向网关是零碎拜访里面资源用于撑持业务零碎的适配。 畛域层:蕴含 PlaceOrderService、Order、Notification、OrderConfirmed 与形象的 OrderRepository,封装了纯正的业务逻辑,不掺杂任何与业务无关的技术实现。应用层:蕴含 OrderAppService 以及形象的 EventBus 与 NotificationService,提供对外体现业务价值的对立接口,同时还蕴含了基础设施性能的形象接口。基础设施层:蕴含 OrderMapper、RabbitEventBus 与 EmailSender,为业务实现提供对应的技术性能撑持,但真正的基础设施拜访则委派给零碎边界之外的内部框架或驱动器。限界上下文与架构意识限界上下文限界上下文体现的是一个垂直的架构边界,次要针对后端架构档次的垂直切分。对基础设施,框架的技术选型属于架构设计的考量范畴,但不属于限界上下文的代码模型,但这些基础设施,框架的抉择仍要思考他们与限界上下文代码模型的集成、构建与部署。 限界上下文之间应该严格谨守边界,防腐层(ACL)是抵挡内部限界上下文变动的最佳场合。 内部资源拜访的两种架构格调: 1:数据库共享架构 防止不同限界上下文关联表之间的外键束缚。 应该依据畛域逻辑常识去辨认限界上下文,防止从数据库层面建模。 2:零共享架构 将两个限界上下文共享的内部资源彻底拆散。 须要思考通信的健壮性。 须要保证数据的一致性。 运维和监控的复杂度也会回升。 代码构造 看下面这个图,applicationService是能够不通过domainService间接拜访repositories,这就阐明了domainService这一层更多的职责应该是畛域对象的业务编排,如果一个业务只是简略的长久化一个对象,能够不必通过domainService,这样代码更简洁,防止通过无谓的代码档次,升高可读性。 ordercontext - gateways - controllers 北向网关 - OrderController - messages dto - CreateOrderRequest - persistence 南向网关(长久化实现) - OrderMapper - client 南向网关(其余微服务/内部服务) - NotificationClient - mq mq实现 - RabbitEventBus - application 业务service - OrderAppService - interfaces 基础设施,南向网关的形象 - client - NotificationService - SendNotificationRequest - mq - EventBus - domain 畛域服务 - PlaceOrderService - Order - OrderConfirmed - Notification - NotificationComposer - repositories 长久化的形象(长久化也是南向网关的一种,但比拟非凡,所以独立进去) - OrderRepository不同的上下文之间的通信须要进行隔离 ...

December 17, 2020 · 1 min · jiezi

关于ddd:领域驱动设计DDD实践之路四领域驱动在微服务设计中的应用

这是“畛域驱动设计实际之路”系列的第四篇文章,从单体架构的弊病引入微服务,联合畛域驱动的概念介绍了如何做微服务划分、设计畛域模型并展现了整体的微服务化的零碎架构设计。联合分层架构、六边形架构和整洁架构的思维,以理论应用场景为背景,展现了一个微服务的程序结构设计。 一、单体架构的弊病 单体构造示例(援用自互联网) 个别在业务倒退的初期,整个利用波及的性能需要较少,绝对比较简单,单体架构的利用比拟容易部署、测试,横向扩大也比拟易实现。 然而,随着需要的一直减少, 越来越多的人退出开发团队,代码库也在飞速地收缩。缓缓地,单体利用变得越来越臃肿,可维护性、灵活性逐步升高,保护老本越来越高。 上面剖析下单体架构利用存在的一些弊病: 1、复杂性高在我的项目初期应该有人能够做到对利用各个性能和实现一目了然,随着业务需要的增多,各种业务流程盘根错节的揉在一起,整个零碎变得宏大且简单,以至于很少有开发者分明每一个性能和业务流程细节。 这样会使得新业务的需要评估或者异样问题定位会占用较多的工夫,同时也蕴含着未知危险。更蹩脚的是,这种极度的复杂性会造成一种恶性循环,每一次更改都会使得零碎变得更简单,更难懂。 2.技术债权多随着时间推移、需要变更和人员更迭,会逐步造成应用程序的技术债权,并且越积越多。比方,团队必须长期应用一套雷同的技术栈,很难采纳新的框架和编程语言。有时候想引入一些新的工具时,就会使得我的项目中须要同时保护多套技术框架,比方同时保护Hibernate和Mybatis,使得老本变高。 3.谬误难隔离因为业务我的项目的所有功能模块都在一个利用上承当,包含外围和非核心模块,任何一个模块或者一个小细节的中央,因为设计不合理、代码品质差等起因,都有可能造成利用实例的解体,从而使得业务全面受到影响。其根本原因就是外围和非核心性能的代码都运行在同一个环境中。 4. 我的项目团队间协同老本高,业务响应越来越慢多个相似的业务我的项目之间势必会存在相似的功能模块,如果都采纳单体模式,就会带来反复性能建设和保护。而且,有时候还须要相互产生交互,买通单体零碎之间的交互集成和合作的老本也须要额定付出。 再者,当我的项目大到肯定水平,不同的模块可能是不同的团队来保护,迭代联调的抵触,代码合并分支的抵触都会影响整个开发进度,从而使得业务响应速度越来越慢。 5.扩大老本高随着业务的倒退,零碎在呈现业务解决瓶颈的时候,往往是因为某一个或几个功能模块负载较高造成的,但因为所有性能都打包在一起,在呈现此类问题时,只能通过减少利用实例的形式分担负载,没方法对独自的几个功能模块进行服务能力的扩大,从而带来资源额定配置的耗费,老本较高。 针对以上痛点,近年来越来越多的互联网公司采纳“微服务”架构构建本身的业务平台,而“微服务”也取得了越来越多技术人员的必定。 微服务其实是SOA的一种演变后的状态,与SOA的办法和准则没有本质区别。SOA理念的外围价值是,松耦合的服务带来业务的复用,依照业务而不是技术的维度,联合高内聚、低耦合的准则来划分微服务,这正好与畛域驱动设计所提倡的理念相符合。 二、微服务设计1. 微服务划分从狭义上讲,畛域即是一个组织所做的事件以及其中蕴含的所有。每个组织都有它本人的业务范围和做事形式,这个业务范围以及在其中所进行的流动便是畛域。 DDD的子域和限界上下文的概念,能够很好地跟微服务架构中的服务进行匹配。而且,微服务架构中的自治化团队负责服务开发的概念,也与DDD中每个畛域模型都由一个独立团队负责开发的概念吻合。DDD提倡按业务畛域来划分零碎,微服务架构更强调从业务维度去做分治来应答零碎复杂度,跳过业务架构设计进去的架构关注点不在业务响应上,可能就是个大泥球,在面临需要迭代或响应市场变动时就很苦楚。 DDD的外围诉求就是将业务架构映射到零碎架构上,在响应业务变动调整业务架构时,也随之变动零碎架构。而微服务谋求业务层面的复用,设计进去的零碎架构和业务统一;在技术架构上则零碎模块之间充沛解耦,能够自在地抉择适合的技术架构,去中心化地治理技术和数据。 以电商的资源订购零碎为例,典型业务用例场景包含查看资源,购买资源,查问用户已购资源等。 畛域驱动为每一个子域定义独自的畛域模型,子域是畛域的一部分,从业务的角度剖析咱们须要笼罩的业务用例场景,以高内聚低耦合的思维,联合繁多职责准则(SRP)和闭包准则(CCP),从业务畛域的角度,划分出用户治理子域,资源管理子域,订单子域和领取子域共四个子域。 每个子域对应一个限界上下文。限界上下文是一种概念上的边界,畛域模型便工作于其中,每个限界上下文都有本人的通用语言。限界上下文使得你在畛域模型四周加上了一个显式的、清晰的边界。当然,限界上下文不仅仅蕴含畛域模型。当应用微服务架构时,每个限界上下文对应一个微服务。 2.畛域模型 聚合是一个边界内畛域对象的集群,能够将其视为一个单元,它由根实体和可能的一个或多个其余实体和值对象组成。聚合将畛域模型合成为块,每个聚合都能够作为一个单元进行解决。 聚合根是聚合中惟一能够由外部类援用的局部,客户端只能通过调用聚合根上的办法来更新聚合。 聚合代表了统一的边界,对于一个设计良好的聚合来说,无论因为何种业务需要而产生扭转,在单个事务中,聚合中的所有不变条件都是统一的。聚合的一个很重要的教训设计准则是,一个事务中只批改一个聚合实例。更新聚合时须要更新整个聚合而不是聚合中的一部分,否则容易产生一致性问题。 比方A和B同时在网上购买货色,应用同一张订单,同时意识到本人购买的货色超过预算,此时A缩小点心数量,B缩小面包数量,两个消费者并发执行事务,那么订单总额可能会低于最低订单限额要求,但对于一个消费者来说是满足最低限额要求的。所以应该站在聚合根的角度执行更新操作,这会强制执行一致性业务规定。 另外,咱们不应该设计过大的聚合,解决大聚合形成的"巨无霸"对象时,容易呈现不同用例同时须要批改其中的某个局部,因为聚合设计时思考的一致性束缚是对整个聚合产生作用的,所以对聚合的批改会造成对聚合整体的变更,如果采纳乐观并发,这样就容易产生某些用例会被回绝的场景,而且还会影响零碎的性能和可伸缩性。 应用大聚合时,往往为了实现一项基本操作,须要将成千盈百个对象一起加载到内存中,造成资源的节约。所以应尽量采纳小聚合,一方面应用根实体来示意聚合,其中只蕴含最小数量的属性或值类型属性,这里的最小数量示意所需的最小属性汇合,不多也不少。必须与其余属性保持一致的属性是所需的属性。 在聚合中,如果你认为有些被蕴含局部应该建模成一个实体,此时,思考下这个局部是否会随着工夫而扭转,或者该局部是否能被全副替换。如果能够全副替换,那么能够建模成值对象,而非实体。因为值对象自身是不可变的,只能进行全副替换,应用起来更平安,所以,个别状况下优先应用值对象。很多状况下,许多建模成实体的概念都能够重形成值对象。小聚合还有助于事务的胜利执行,即它能够缩小事务提交抵触,这样不仅能够晋升零碎的性能和可伸缩性,另外零碎的可用性也失去了加强。 另外聚合间接的援用通过惟一标识实现,而不是通过对象援用,这样不仅缩小聚合的应用空间,更重要的是能够实现聚合间接的松耦合。如果聚合是另一个服务的一部分,则不会呈现跨服务的对象援用问题,当然在聚合外部对象之间是能够互相援用的。 上述对于聚合的次要应用准则总结起来能够演绎为以下几点: 只援用聚合根。 通过惟一标识援用其余聚合。一个事务中只能创立或批改一个聚合。聚合边界之外应用最终一致性。当然在理论应用的过程中,比方某一个业务用例须要获取到聚合中的某个畛域对象,但该畛域对象的获取门路较繁琐,为了兼容该非凡场景,能够将聚合中的属性(实体或值对象)间接返回给应用层,使得应用层间接操作该畛域对象。 咱们常常会遇到在一个聚合上执行命令办法时,还须要在其余聚合上执行额定的业务规定,尽量应用最终一致性,因为最终一致性能够按聚合维度分步骤解决各个环节,从而晋升零碎的吞吐量。对于一个业务用例,如果应该由执行该用例的用户来保证数据的一致性,那么能够思考应用事务一致性,当然此时仍然须要遵循其余聚合准则。如果须要其余用户或者零碎来保证数据一致性,那么应用最终一致性。实际上,最终一致性能够反对绝大部分的业务场景。 基于上面对电商的资源订购零碎业务子域的划分,设计出资源聚合,订单聚合,领取聚合和用户聚合,资源聚合与订单聚合之间通过资源ID进行关联,订单聚合与领取聚合之间通过订单ID和用户ID进行关联,领取聚合和用户聚合之间通过用户ID进行关联。资源聚合根中蕴含多个资源包值对象,一个资源包值对象又蕴含多个预览图值对象。当然在理论开发的过程中,依据理论状况聚合根中也能够蕴含实体对象。每个聚合对应一个微服务,对于特地简单的零碎,一个子域可能蕴含多个聚合,也就蕴含多个微服务。 3.微服务零碎架构设计 基于上面对电商的资源订购零碎子域的剖析,服务器后盾应用用户服务,资源服务,订单服务和领取服务四个微服务实现。上图中的API Gateway也是一种服务,同时能够看成是DDD中的应用层,相似面向对象设计中的外观(Facade)模式。 作为整个后端架构的对立门面,封装了应用程序外部架构,负责业务用例的工作协调,每个用例对应了一个服务办法,调用多个微服务并将聚合后果返回给客户端。它还可能有其余职责,比方身份验证,拜访受权,缓存,速率限度等。以查问已购资源为例,API Gateway须要查问订单服务获取以后用户已购的资源ID列表,而后依据资源ID列表查问资源服务获取已购资源的详细信息,最终将聚合后果返回给客户端。 当然在理论利用的过程中,咱们也能够依据API申请的复杂度,从业务角度,将API Gateway划分为多个不同的服务,避免又回归到API Gateway的单体瓶颈。 另外,有时候从业务畛域角度划分进去的某些子域比拟小,从资源利用率的角度,独自放到一个微服务中有点薄弱。这个时候咱们能够突破一个限界上下文对应一个微服务的理念,将多个子域合并到同一个微服务中,由微服务本人的应用层实现多子域工作的协调。 所以,在咱们的零碎架构中可能会呈现微服务级别的小应用层和API Gateway级别的大应用层应用场景,实践诚然是实践,还是须要结合实际状况灵便利用。 三、畛域驱动概念在单个微服务设计中的利用1.架构抉择剖析 分层架构图(援用自互联网) 六边形架构图(援用自互联网) 整洁架构图(援用自互联网) 下面整洁架构图中的同心圆别离代表了软件系统中的不同档次,通常越凑近核心,其所在的软件档次就越高。 整洁架构的依赖关系规定通知咱们,源码中的依赖关系必须只指向同心圆的内层,即由低层机制指向高层策略。换句话说,任何属于内层圆中的代码都不应该关涉外层圆中的代码,尤其是内层圆中的代码不应该援用外层圆中代码所申明的名字,包含函数、类、变量以及所有其余有命名的软件实体。同样,外层圆应用的数据格式也不应该被内层圆中的代码所应用,尤其是当数据格式由外层圆的框架所生成时。 总之,不应该让外层圆中产生的任何变更影响到内层圆的代码。业务实体这一层封装的是整个业务畛域中最通用、最高层的业务逻辑,它们应该属于零碎中最不容易受外界影响而变动的局部,也就是说个别状况下咱们的外围畛域模型局部是比较稳定的,不应该因为外层的基础设施比方数据存储技术选型的变动,或者UI展现形式等的变动受影响,从而须要做相应的改变。 在以往的我的项目教训中,大多数同学习惯也比拟相熟分层架构,个别包含展现层、应用层,畛域层和基础设施层。六边形架构的一个重要益处是它将业务逻辑与适配器中蕴含的表示层和数据拜访层的逻辑拆散开来,业务逻辑不依赖于表示层逻辑或数据拜访层逻辑,因为这种拆散,独自测试业务逻辑要容易得多。 另一个益处是,能够通过多个适配器调用业务逻辑,每个适配器实现特定的API或用户界面。业务逻辑还能够调用多个适配器,每个适配器调用不同的内部零碎。所以六边形架构是形容微服务架构中每个服务的架构的好办法。 依据咱们具体的实践经验,比方在咱们平时的我的项目中最常见的就是MySQL和Redis存储,而且也很少扭转为其余存储构造。这里将分层架构和六边形架构进行思维交融,目标是一方面心愿咱们的微服务设计构造更柔美,另一方面心愿在已有编程习惯的根底上,更容易接受新的整洁架构思维。 咱们我的项目中微服务的实现联合分层架构,六边形架构和整洁架构的思维,以理论应用场景为背景,采纳的应用程序结构图如下。 从上图能够看到,咱们一个利用总共蕴含应用层application,畛域层domain和基础设施层infrastructure。畛域服务的facade接口须要裸露给其余三方零碎,所以独自封装为一个模块。因为咱们个别习惯于分层架构模式构建零碎,所以依照分层架构给各层命名。 站在六边形架构的角度,应用层application等同于入站适配器,基础设施层infrastructure等同于出站适配器,所以实际上应用层和基础设施层同属外层,能够认为在同一层。 facade模块其实是从畛域层domain剥离进去的,站在整洁架构的角度,畛域层就是内核业务实体,这里封装的是整个业务畛域中最通用、最高层的业务逻辑,个别状况下外围畛域模型局部是比较稳定的,不受外界影响而变动。facade是微服务裸露给外界的畛域服务能力,个别状况下接口的设定应合乎以后畛域服务的边界界定,所以facade模块属于内核畛域层。 ...

December 15, 2020 · 3 min · jiezi

关于ddd:领域驱动设计实践

文章内容全副来自张逸《畛域驱动设计实际(策略+战术)》畛域驱动是什么畛域驱动设计就是针对软件开发畛域提出的一套零碎与实践分析方法,是“一种思维形式,也是一组优先工作,它旨在减速那些必须解决简单畛域的软件我的项目的开发”。畛域驱动设计贯通了整个软件开发的生命周期,包含对需要的剖析、建模、架构、设计,甚至最终的编码实现,乃至对编码的测试与重构。畛域驱动设计强调畛域模型的重要性,并通过模型驱动设计来保障畛域模型与程序设计的统一。从业务需要中提炼出对立语言(Ubiquitous Language),再基于对立语言建设畛域模型;这个畛域模型会领导着程序设计以及编码实现;最初,又通过重构来发现隐式概念,并使用设计模式改良设计与开发品质。 策略设计阶段问题域方面针对问题域,引入限界上下文(Bounded Context)和上下文映射(Context Map)对问题域进行正当的合成,辨认出外围畛域(Core Domain)与子畛域(SubDomain),并确定畛域的边界以及它们之间的关系,维持模型的完整性。 架构方面通过分层架构来隔离关注点,尤其是将畛域实现独立进去,可能更利于畛域模型的单一性与稳定性;引入六边形架构能够清晰地表白畛域与技术基础设施的边界;CQRS 模式则拆散了查问场景和命令场景,针对不同场景抉择应用同步或异步操作,来进步架构的低提早性与高并发能力。 战术设计阶段整个软件系统被合成为多个限界上下文(或畛域)后,就能够分而治之,对每个限界上下文进行战术设计。 演进的畛域驱动设计过程策略设计会管制和合成战术设计的边界与粒度,战术设计则以实证角度验证畛域模型的有效性、完整性与一致性,进而以演进的形式对之前的策略设计阶段进行迭代,从而造成一种螺旋式回升的迭代设计过程。 意识分层架构经典分层架构畛域驱动设计分层架构 档次职责用户界面/展示层负责向用户展示信息以及解释用户命令应用层很薄的一层,用来协调利用的流动,它不蕴含业务逻辑,它不保留业务对象的状态,但它保有利用工作的进度状态畛域层本层蕴含对于畛域的信息,这是业务软件的外围所在。在这里保留业务对象的状态,对业务对象和它们状态的长久化被委托给了基础设施层基础设施层本层作为其余层的撑持库存在。它提供了层间的通信,实现对业务对象的长久化,蕴含对用户界面层的撑持库等作用分层的根据与准则分层的第一个根据是基于关注点为不同的调用目标划分档次。分层的第二个根据是面对变动。层与层之间的关系应该是正交的。保障同一层的组件处于同一个抽象层次。 分层架构的演变分层架构是一种架构模式,但终归它的目标是为了改良软件的架构品质,咱们在使用分层架构时,必须要恪守架构设计的最高准则,即建设一个高内聚、松耦合的软件系统架构。 整洁架构在架构设计时,咱们应设计出洁净的应用层和畛域层,放弃它们对业务逻辑的专一,而不掺杂任何具体的技术实现,从而实现畛域与技术之间的齐全隔离,这一思维被 Robert Martin 称之为整洁架构(Clean Architecture)。畛域模型就是业务逻辑的模型,它应该是齐全纯正的。对下,例如,针对数据库、音讯队列或硬件设施,能够认为是一个南向网关,对于以后限界上下文是一种输入的依赖;对上,例如,针对 Web 和 UI,能够认为是一个北向网关,对于以后限界上下文是一种输出的依赖。 六边形架构六边形架构通过内外两个六边形为零碎建设了不同档次的边界。微服务架构

November 30, 2020 · 1 min · jiezi

关于ddd:领域驱动事件风暴

佛,在信徒眼里是佛,是心愿;在工艺品厂里,对于工人来说,就是一个活,是工作对象,是支出的起源;对于物流公司来说,是货,是责任担当,是运输的标的。不同的事件主题关注的业务事件是不同,畛域模型也是不同的。在不同的畛域模型中,对立语言。事件风暴事件风暴是一种疾速摸索简单业务畛域和对领域建模的实际。事件风暴从畛域关注的业务事件登程,通过团队的充沛探讨,对立语言,最终找到畛域模型。 如何确定畛域关注的业务事件在通用语言中存在“如果A产生,咱们就须要做到B。”,这样的表述,那么A就能够定义成为一个畛域事件。畛域事件的命名个别采纳 “产生事件的对象名称+实现动作的过来模式” 的模式,这有点相似用户故事的形容。其实用户故事就能够看作是一个畛域事件,只是用户故事转换成业务事件时,须要依据业务畛域对立语言。 如何发展事件风暴大部分的材料都是站在全局的高度去做事件风暴,将整个零碎一起进行事件拆分,划分畛域模型。这样做当然没错,然而理论开发过程中,咱们往往1:短少领域专家;2:短少足够是工夫来做畛域剖析,事件风暴。这往往导致事件风暴成为实践,而短少实际。从我个人观点来看,架构是一直演进的,业务始终在变动,代码也始终在批改,那么,咱们就能够从业务点登程,从一个需要登程,做事件风暴。 筹备工作必不可少的便当贴,凋谢的空间,大白板。全员参加,包含业务,产品,开发,测试,UI。 外围概念事件风暴将零碎拆分为不同的元素,用不同色彩的便当贴示意。 对立语言对立语言十分重要,是沟通的终点,如果一个业务内,包含业务方,产品,开发之间对于概念的表述不对立,会造成沟通不顺畅,甚至呈现背道而驰的景象。因为后期咱们是从单个需要登程,可能对立语言定义进去的概念并不精确,或者在命名上有争议,不用介意,对立语言除了精确形容业务对象以外,更次要的性能是上下文的沟通和传递,只有上下文是对立的,业务就能够顺利开展,代码也能够精确编写。如果后续其余需要减少变更,发现之前定义的名称不精确,概念上批改过去就能够了。 事件风暴过程 辨认畛域事件事件风暴以辨认畛域事件开始。书写畛域事件的规定是应用被动语态,依照事件倒退程序贴在白板上。遇到有争议的事件,不用过多纠结,先标记成热点事件,后续能够重点探讨。事件个别由名次和动词组合而成,例如:订单已创立;地址已填写。 留神:用户的前端操作不是事件,例如:用户提交订单,用户提交表单;这些只是为事件提供数据。辨认参与者事件一共有四种参与者: - 角色:触发事件的人- 策略:触发事件的规定- 内部零碎- 事件:即以后事件的前置事件留神:策略是规定,但规定不是策略。策略是规定+定时器的组合。策略会触发事件,但规定不会。辨认限界上下文从两个方向辨认限界上下文: 纵向:辨认事件流中的事件,假使相邻两个事件的关系较弱,或者体现了两个非常明显的阶段,就能够对其进行宰割。横向:梳理是有的事件,依据组成事件的名词和动词去发现事件之间的相关性(雷同、类似的名词),而后去提炼一个整体的概念。限界上下文蕴含场景,角色,流动,常识和能力,不蕴含UI局部。限界上下文能够由不间断的事件组成。限界上下文在命名的时候应用名词来定义。 辨认限界上下文遵循的准则繁多抽象层次准则每个限界上下文从概念上应该尽量处于对立抽象层次,不能嵌套。 正交准则限界上下文之间不能相互影响,相互蕴含。{% img /images/ddd/event4.jpg %} 辨认上下文映射通过事件风暴: 首先辨认跨界线界上下文之间相邻事件的关系。事件之间是否存在间接触发的关系(参与者为前置事件),须要确定这两个事件所述的限界上下文。判断这两个事件所属的限界上下文,谁是次要的。次要的上下文就是上游。通常,前置事件为上游,或是事件的发布者。上游调用上游。事件依赖关系为单向依赖.防止上游应用上游的的畛域模型(尊奉者模式),由上游来定义参数上和返回值,上游依据状况来决定是否须要定义防腐层。一般来说,事件如果由本人的角色参与者(角色,策略,内部零碎),就与前置事件脱离来关系。畛域剖析建模一个事件只能有一个写模型,如果呈现多个写模型,要么就是这几个写模型存在蕴含关系,要么就是写模型脱漏了对应的事件。对于读模型,要留神它属于那个限界上下文,如果不是以后上下文,则: 定义本人的读模型,通过防腐层进行转换,尽量不要投合上游应用ID值对象(用于建设关联)(根本类型偏执)读模型和写模型就是畛域模型对象 辨认聚合针对畛域分享模型,梳理模型对象之间的关系(继承,合成,聚合,依赖,无关系)确定畛域模型对象是实体还是值对象将具备继承或合成关系的畛域模型对象放在一个聚合边界内依据聚合的实质(概念完整性,概念独立性,不变量Invariant,事务一致性)梳理聚合代码实现角色构造型DomainService来协调单个畛域模型/值对象无奈实现的业务性能,次要是数据长久化,内部接口调用获取数据等AppService则负责业务编排Factory负责封装简单的创立逻辑,用于创立畛域对象

November 25, 2020 · 1 min · jiezi

关于ddd:领域驱动实践从需求到代码

业务需要网约车出行我的项目mvp 作为乘客我心愿创立⼀个出⾏订单,以便于从A地返回B地作为司机我心愿履⾏⼀个订单,以便于获取收⼊作为经营我心愿能勾销订单,以便于乘客分割不上司机时从新下单传统mvc模式传统mvc往往基于数据模型进行开发,通过需要剖析,确定数据模型,而后在数据模型上做CRUD开发imageserver类中汇集类所有的业务代码 所有的操作都是在操作数据,当业务变得越来越简单时,service中的代码越来越臃肿,而后依据业务进行模块拆分,然而因为业务犬牙交错,后续批改业务代码时,可能会须要批改多个模块。 微服务开发微服务的呈现一部分起因就是心愿将业务划分分明,解决模块耦合的问题,借助畛域驱动设计,咱们能够通过一些方法论来进行业务建模和微服务划分。 对立语言针对不同的角色,同一个事务可能有不同定义。 对于乘客来说,出行订单应该是一个行程。乘客关怀的是终点,起点,司机的实时地位,须要收入的费用。对于设计来说,出行订单是一笔生意。司机关怀的是乘客的地位,目的地,该笔行程的支出、处分。对于经营人员来说,出行订单是一个合约,合约的单方是乘客和司机,经营人员关注合约的履约状况,合约的抽成信息等。针对不同的参加角色,咱们定义不同的模型概念。 通过对业务进行限界上下文划分,很容易就能够进行代码的隔离,不同的上下文离开进行编码,上下文之间的业务调用通过api接口方式进行交互,这样后续的业务演进,零碎部署降级以及扩容都绝对独立。然而一个userstor就划分一个微服务必定是不事实的,咱们须要依据业务的相关性来进行,从两个方向来进行组织限界上下文。 语义相关性不同的用例存在语义相关性就能够思考放在一个限界上线文内。例如创立行程,勾销行程都跟行程无关,就适宜放在一个限界上下文来解决。 性能相关性有些用例尽管都是操作雷同的对象,然而在性能上有互相的独立性,应该思考宰割成独立的上下文。例如领取行程,尽管也是在操作行程对象,但其实更侧重于领取动作,后续的业务扩大也多围绕在领取上,如减少领取渠道,减少租金统计等和行程关联不大。 DEMO代码参考:ddd-demo

November 25, 2020 · 1 min · jiezi

关于ddd:领域驱动设计DDD前夜面向对象思想

面向对象面向对象是一种对世界了解和形象的办法。那么对象是什么呢?对象是对世界的了解和形象,世界又代称为万物。了解世界是比较复杂的,然而世界又是由事物组成的。正是这样的一种关系,意识事物是极其重要的。那什么是事物呢?事物:由事和物两个方面组成。事即事件,物即物体,那什么是事件?什么是物体呢? 意志的行为是为事。存在的所有是为物,物体又是由属性组成的。一个事物就是本有属性和行为的组合。因为对象是对事物的了解和形象,所以对象就是对一个事物的属性和行为的了解和形象。正是这样的一种关系,面向对象就是对一个事物的属性和行为的了解和形象的办法。了解对象以及形象“对象”就是在了解和形象事物的属性和行为。 属性和操作面向对象的外围是对象,对象是由属性和办法组合而成的。在应用面向对象进行剖析、设计、编码的时候,你首先应该想到的是属性和办法组合造成的对象。在须要组合的时候就不应该呈现只蕴含属性的对象或者只蕴含办法的对象。 何时须要属性和办法组合的对象呢?何时只须要蕴含属性的对象呢?何时只须要蕴含办法的对象呢?事物由事件和物体组成。事件是行为,物体是属性。 当你须要形象一个事物的事件和物体时就须要属性和办法的组合。当你只须要形象一个事物的物体时就只须要属性。当你只须要形象一个事物的事件时就只须要办法。对象建模在数据库系统中,它们关怀的是事物中的物体,所以在形象事物时它们只形象了事物中的属性。在利用零碎中,它们关怀的是表白事物的三种形式(属性和办法的组合、只蕴含属性、只蕴含办法),所以在形象事物时须要思考你须要那种形式。只有须要形象事物(事件和物体)中的属性,也就是物体的这部分,那有可能是须要长久化的。只有须要长久化,通常是保留到关系型数据库中,在关系型数据库中的表(Table)基本上是与面向对象中的对象(Object)的属性是一一对应的。因为数据库中的表只形象了事物中的属性,所以它有可能是不残缺的。就形象事物的属性来说仍然有两种:只形象事物的属性、形象事物的属性和办法的组合。正是数据库中表的这种形象造成了数据模型,它比照对象模型是不残缺,所以在面向对象分析(OOA)时肯定要采纳对象(事物)形象而不是数据(属性、物体)形象。举个例子:简略金融账户(Account)属性有:账号(id)、余额(balance)、状态(status)操作有:开户(open)、登记(close)、存钱(credit)、取钱(debit)。数据模型的只须要设计字段(fields)和关联关系,所以上面的 SQL 根本已实现。 create table account( id integer, balance integer, status integer);如果把上述 SQL 转换成 Java 的对象的话,失去将是一个用面向对象设计的数据模型,而不是残缺的对象模型。这种模型在 Java 开发中十分广泛,这是数据模型思维所导致的后果。 @Getter@Setterpublic class Account { private int id; private int balance; private AccountStatus status;}如果应用对象模型的思维来设计模型,从接口上来看,他应该是这样的: public interface Account { int getId(); int getBalance(); AccountStatus getStatus(); void open(); void close(); void credit(int amount); void debit(int amount);}如果 Account 接口合乎金融账户的设计,那么 Account 最简略地实现应该如下: @Getterpublic class Account { private int id; private int balance; private AccountStatus status; public void open() { this.status = AccountStatus.OPENED; } public void close() { this.status = AccountStatus.CLOSED; } public void credit(int amount) { this.balance += amount; } public void debit(int amount) { this.balance -= amount; }}这是从两个建模的角度来比照对象模型和数据模型的不同,上面咱们还要从残缺地执行流程来比照。 ...

November 19, 2020 · 2 min · jiezi

关于ddd:领域驱动设计与敢死队驱动设计-Domain-Driven-Desigin-vs-Deadline-Driven-Design

当我做DDD企业培训时候问: “谁晓得畛域驱动设计”,只有不到10%的人会拍板。 当我问谁经验过Deadline驱动的开发,简直所有人都会心领神会。 轻重倒置的DDD上面我分享一下在企业外面DDD落地的一个故事 T学生是我赋能DDD的一个BA,咱们先是在Lab1中用DDD的方法论做了一个MVP我的项目。而后一个星期之后再进行另一个Lab2 MVP的开发设计,咱们又见面了,当时约定由T学生梳理好需要” 一个星期后, 咱们站在上面这张用户故事地图前,T满脸笑容: 无事件风暴的用户情景图 我说:“这是你整理出来的用户故事是吗?事件风暴做了吗?” T调整了一下站姿,侧身小声对我说:“我感觉在Lab1当中事件风暴的输入如同就是用户故事。那么我当初间接就把用户故事整理出来,这样会快一点。” 过后呈现了几秒钟禁止画面。我而后说:“我明确了, 咱们尽管是通过用户故事对接上迭代的开始,然而事件风暴的意义不仅仅是产生用户故事。另外你怎么剖析的用户故事,你怎么确定剖析精确不精确?” 接下来,我又开始讲了一些事件风暴的价值。 “首先,最重要的一点是要让开发者参加进来,要互动起来。事件风暴本省是一个让畛域专业知识和开发人员信息沟通的桥梁和平台。有了这样一个高效,可视化的平台,业务知识才能够从领域专家的头脑中流到开发人员的头脑中,再通过开发人员的双手敲进计算机外面。” “其次,咱们须要确定零碎的不同畛域级别,从策略上定位进去你要构建的零碎的外围域,撑持域,和通用域, 不同域采纳的策略,投入的精力,和优先级是不同的。“ “你还须要剖析出都有哪些聚合,聚合根,这些才是咱们零碎的心智球,没有这些,零碎是不清晰也很难贴合理论的业务流程和场景的。” 看到T脸上神秘的笑容,他说:“好吧,我批准,不过Z总的意思心愿咱们尽快开始迭代”。(Z总是咱们的PO,产品经理) 而后找到Z总,再复读一遍。我最初又加了几句助攻:”我很了解咱们8月底有上线压力。然而磨刀不误砍柴功。事件风暴就设计用来进行疾速业务流程剖析的工具。” 上面是通过一天由BA,开发人员还有教练独特参加实现的局部业务流程的事件风暴样子。 事件风暴地图的一部分 站在这墙前,从表情来看,T如释重负。 我问“做完事件风暴前后的用户故事,你感觉有什么不同?” T说:“有不同,咱们发现了一些之前没有思考到的用户故事和细节。比方:合同注释附件聚合,当我一开始试图再头脑中理清的所有场景时,这一点被忽略了。” 我说:“那你晓得为什么过后会错过这些细节吗?” T说:“当我思考这些需要的时候,想抓住所有的用户故事时,我好像只见森林,不见树木。很多细节都无奈辨认。然而,当咱们开始事件风暴时,我感觉就像是踏进森林的一条条小路,穿过它,很多业务中的流程和信息开始变得清晰了。” 我说:“是的,事件风暴帮忙你把头脑中的业务流程和场景扩大到这些物理墙上,让每个人都能看到它们。它不仅能够帮忙你认清业务流程和细节,更重要的是能够帮忙所有开发人员查看,理解所有细节,从而尽可能地把握整个流程的心智模型。 如果没有这些认知,那么咱们的开发人员就无奈依据正确的业务了解开发性能而是基于一些误会。” T补充说:”我感觉事件风暴特地有用,它解决了我一开始的一些困惑,我晓得当初是什么困扰着我,而后我很分明能够问最终用户一些什么问题。” 我说:“你也答复了很多来自开发团队的问题,对吧?如果他们没有看到所有的细节,参加其中,他们无奈问出这些问题,因为信息极其不对称,程序员他们的关注点,和常识领域和业务专家有很大的不同。这些常识他们不提前问你就会在sprint前期问你。因为他们肯定在开发用户故事的时候卡住,你会看到Scrum看板中,看到进行中的工作会大量沉积,就像草裙一样。 这是一个反模式,就是进行中的工作过多。 你当初晓得为什么了吗?” T笑了,他仿佛对我形容的情景相当相熟。 “当初咱们也晓得应该分多少个域,咱们应该在sprint 1中优先解决哪个域,而不是将很多扩散的用户故事放在一个sprint中,对吧? 咱们甚至明确了咱们打算布局几个微服务,先做哪个后做哪个.” … 最初我想说磨快刀子不耽搁砍柴,事件风暴是一个简略而弱小的用来梳理业务需要和软件设计的工具.

September 18, 2020 · 1 min · jiezi

领域驱动设计在马蜂窝优惠中心重构中的实践

前言正如领域驱动设计之父 Eric Evans 所著一书的书名所述,领域驱动设计(Domain Driven Design)是一种软件核心复杂性应对之道。 在我们解决现实业务问题时,会面对非常复杂的业务逻辑。即使是同一个事物,在多个子业务单元下代表的意思也是不完全一样的。比如「商品」这个词,在商品详情页语境中,是指「商品基本信息」;在下单页语境中,是指「购买项」;而在物流页面语境中,又变成了「被运送的货物」。 DDD 的核心思想就是让正确的领域模型发挥作用。所谓「术业有专攻」,DDD 指导软件开发人员将不同的子业务单元划分为不同的子领域,在各个子领域内部分别对事物进行建模,来应对业务的复杂性。 一、重构优惠中心的背景我们在实际的开发过程中都遇到过这种情况,最初因为业务逻辑比较单一,为了快速实现功能, 以及对成本、风险等因素的综合考虑,我们会为业务统一创建一个大的模型,各个模块都使用这同一个模型。但随着业务的发展,各子领域的逻辑越来越复杂,对这个大模型的修改就会变成一种灾难,有时明明是要改一个 A 子领域的逻辑,却莫名其妙影响到了 B 或者 C 子领域的线上功能。 优惠中心就是一个例子。优惠中心主要负责马蜂窝各业务线商品的优惠活动管理,以及计算不同用户的优惠结果。「商品管理」和「优惠管理」作为两个不同的业务单元,在初期被设计为共用一个商品模型,由商品模块统一管理。 <center>图1 :初期商品模型</center> 出现的问题随着业务的发展,优惠的形式不断推陈出新,业务形态逐渐多样,业务方的需求也越来越个性化,导致后期的优惠中心无论从功能上还是系统上都出现了一些具体的问题: 1. 功能上来说,不够灵活 优惠信息是作为商品信息的一个属性在商品管理模块配置的。比如为了引导用户使用 App 需要设置 A 类型优惠,就通过在商品信息的编辑页面增加一个 A 类型优惠配置项实现;如果某个商品的 A 类型优惠需要在 0:00 分生效,业务同学就必须在电脑前等到 0:00 更新商品信息来上线优惠活动。 另外,如果想要创建针对所有商品都适用的优惠,按照之前的模式,所有的商品都要设置一遍,这几乎是不可接受的。 2. 从系统层面看,不易扩展 优惠信息存储在商品信息中,优惠信息是通过商品管理模块的接口输出的。如果要新增一种优惠类型,商品信息相关的表就要增加字段,商品的表会越来越大;如果要迭代一个优惠的逻辑,就有可能影响到商品管理模块的功能。 3. 不利于迭代 由于优惠信息仅仅作为商品的一个属性,没有自己的生命周期,所以很难去统计某一次设置的优惠的投入产出比,从而指导后续的功能优化。 重构优惠中心的预期系统层面上,要把优惠相关的业务逻辑独立出来,单独设计和实现;应用层面上,优惠中心会有自己的独立后台,负责管理优惠活动;也会有独立的优惠计算接口,负责 C 端用户使用优惠时的计算。二、分什么选择 DDD避免贫血模型基于传统的 MVC 架构开发功能的时候,Model 层本质上是一个 DAO 层,业务逻辑通常会封装在 Service 层,然后 Controller 通过调用 Service 层来完成对外的功能。这种模式下,数据和行为分别被割裂到了 Model 和 Service 两层。我们把这种只承载数据,但没有业务行为的 Model 称为「贫血模型」。 ...

July 12, 2019 · 1 min · jiezi

领域驱动设计-DDD-的思考

写在前面打开 DDD 相关的书籍,你会被一系列生硬、高深的概念充斥,拜读完毕,满头雾水。这不是你的问题,而是 DDD 本身的问题,表现形式太概念化。学习它的内核,就不要被它给出的概念所迷惑,而要去思索这些概念背后所蕴含的设计原则,多问一些为什么,本质无外乎是 SOLID。最重要的,要学会运用设计原则去解决问题,而非所谓的 “设计规范”。 本文将会以系列解答的方式展开,由浅入深,篇幅不长,无妨一看。 啥是 DDD?本质上是一种方法论,提供了一套系统开发的设计方法。面对需要解决的问题,从复杂的现实中抽象出业务模型的思维方式与实践技巧。初衷是清晰设计思路,规范设计过程。 啥是驱动?DDD 强调是说得先把 “领域” 中涉及到的数据、流程、规则等都弄明白了,然后以面向对象的观点为其建立一个模型(即领域模型),而这个模型,决定了你将用什么技术、什么架构、什么平台来实现这个系统。所以技术在这个过程中是 “被动的”,是被 “选来” 实现 “领域模型” 的。对于项目的成败,技术不是决定性因素,领域模型是否符合事物的本质才是关键。 可以看出,领域驱动设计的出发点是业务导向,技术服务于业务。 我有误解?学习 DDD 有一些常见的误区。第一个要避免的就是,你必须要清楚,DDD 对于工程领域没有提出多么创新的技法,它更多是把前人在生产系统中用惯的技法归纳,总结,包装了一下。 我们来看一下 DDD 的初衷 —— DDD 抛开它晦涩的表达和术语,内核无外乎是 SOLID。书中的技法不是作者的发明创造或独门秘籍,而是当时业已存在并已在使用的做法,只不过没有被系统地加以记录 —— 换而言之,只要遵循 SOLID 的原则,这些所谓的概念技法完全可能在无意识的状态下自发出现在生产代码中。这些技法在各种大型系统被多次使用。换而言之,只要你接触足够多的代码、足够多的项目,必然会接触到这些 DDD 的实际应用。只不过在看过 DDD 一书之前,你可能意识不到这是一个成体系的设计手段。—— 如果你无法理解这样一个设计理论,看实际生产代码的应用场景是唯一真正有效的方法,毕竟设计理论本身就是从具体代码中总结出来的。即使你觉得自己懂了,抽象思维和开发经验也可能还未达到正确使用它的水平。它最大的价值在于对设计手段进行了有效的整理,形成了一个完整的系统设计规范,但过于晦涩和概念化的表述,也几乎消解了它对业界的贡献。啥时候用?你可能认为设计模式是一把 “瑞士军刀”,能够解决所有的设计问题,而实际上 “DDD 只是一把锤子”,有个谚语叫做 “如果你手里有一把锤子,那么所有的问题都变成了钉子”,如果你拿着 DDD 这把锤子到处去敲,要么东西被敲坏,要么就不起作用。 为什么说设计模式只是一把锤子呢?作者明确指出,DDD 只适合业务复杂度很大的场景,不适用于技术复杂性很大但业务领域复杂性很低的场景。可以看出,DDD 只专注一个领域,高复杂业务 —— 通过它可以为你的系统建立一个核心、稳定的领域模型,灵活、可扩展的架构。 DDD 是拥抱复杂的,拥抱变化的,但本身也是有成本有前提的;简单系统,没必要搞这么复杂,强上 DDD 就是一种反模式了。所以,当你遇到一个问题就想到 DDD 的时候,一定要注意 “DDD 只是一把锤子”,不要拿着这把锤子到处去敲! 啥是复杂?如何判断业务是否复杂,判断依据不胜繁数。在我看来,没那么复杂,就两个: 宽度:链路广度大,关注多个纬度的消息来源,覆盖了较多的业务场景深度:链路深度深,关注对象整个的生命周期,从数据创建、到变更、再到后期运维,流程运转长只要满足其中一个,我认为就是复杂的。 具体解决啥?领域驱动设计并非 “银弹”,自然也不是解决所有疑难杂症的 “灵丹妙药”。在我看来,它只解决一个问题:过度耦合。 为啥会耦合?业务初期,系统功能大都非常简单,CRUD 就能满足,此时的系统是清晰的。随着业务的不断演化,系统的频繁迭代,代码逻辑变得越来越复杂,系统也越来越冗余。模块彼此关联,谁都很难说清模块的具体功能意图是啥;修改一个功能,往往光回溯该功能需要的修改点就需要很长时间,更别提修改带来的不可预知的影响面。 ...

May 21, 2019 · 2 min · jiezi

CQRS & Event Sourcing — 解决检索应用程序状态问题的一剂良方

现在,每个开发人员都很熟悉MVC标准体系结构设计模式。大多数的应用程序都是基于这种体系结构进行创建的。它允许我们创建可扩展的大型企业应用程序,但近期我们还听到了另外的一些有关于CQRS/ES的相关信息。这些方法应该被放在MVC中一起使用吗?他们可以解决什么问题?现在,让我们一起来看看CQRS/ES是什么,以及他们都有哪些优点和缺点。CQRS — 模式介绍CQRS(Command Query Responsibility Segregation)是一种简单的设计模式。它衍生与CQS,即命令和查询分离,CQS是由Bertrand Meyer所设计。按照这一设计概念,系统中的方法应该分为两种:改变状态的命令和返回值的查询。Greg young将引入了这个设计概念,并将其应用于对象或者组件当中,这就是今天所要将的CQRS。它背后的主要思想是应用程序更改对象或组件状态(Command)应该与获取对象或者组件信息(Query)分开。下面,将通一张图来说明应用程序中有关CQRS部分的组成结构:Commands(命令)—表示用户的操作意图。它们包含了与用户将要对系统执行操作的所有必要信息。Command Bus(命令总线):是一种接收命令并将命令传递给命令处理程序的队列。Command Handler(命令处理程序):包含实际的业务逻辑,用于验证和处理命令中接收到的数据。Command handler负责生成和传播域事件(Event)到事件总线(Event Bus)。Event Bus(事件总线):将事件发布给订阅特定事件类型的事件处理程序。如果存在连续的事件依赖,事件总线可以使用异步或者同步的方式将事件发布出去。Event Handler(事件处理程序):负责处理特定类型的事件。它们的职责是将应用程序的最新状态保存到读库中,并执行终端的相关操作,如发送电子邮件,存储文件等。Query(查询):表示用户实际可用的应用程序状态。获取UI的数据应该通过这些对象完成。下面我们将介绍有关CQRS的诸多优点,它们是:我们可以给处理业务逻辑部分和处理查询部分的开发人员分别分配任务,但需要小心的是,这种模式可能会破坏信息的完整性。通过在多个不同的服务器上扩展Commands和Query,我们可以进一步提升应用程序的读/写性能。使用两个不同的数据库(读库/写库)进行同步,可以实现自动备份,无需额外的干预工作。读取数据时不会涉及到写库的操作,因此在使用事件源是读数据操作会更快。我们可以直接为视图层构建数据,而无需考虑域逻辑,这可以简化视图层的工作并提高性能。尽管使用CQRS模式具有上述诸多的优点,但是在使用前还需要慎重考虑。对于只具有简单域的简单项目,其UI模型与域模型紧密联系的,使用CQRS反而会增加项目的复杂度和冗余度,这无疑是过度的设计项目。此外,对于数据量较少或者性能要求较低的项目实施CQRS模式不会带来显著的性能提升。Event Sourcing — 案例研究有这样一个案例,我们想要检索任何一个域对象的历史状态数据,而且在任何时间都可以生成统计数据。我们想要检查上个月、上个季度或者过去任何时间的状态汇总。想要解决这个问题并不容易。我们可以在特定的时间范围内将额外的数据保存在数据库中,但这种方法也存在一些缺点。我们不知道范围应该是什么样子,以及未来统计数据需要哪些数据项。为了避免这些问题,我们可以每天为所有聚合创建快照,但它们同样会产生大量的冗余数据。Event Sourcing(ES)似乎是目前解决这些问题的最佳方案。Event Sourcing允许我们将Aggregate(聚合)状态的每一个更改事件保存在Event Store的事件存储库中。通过Command Handler将事件写入到事件存储库中,并处理相关的逻辑。要创建Aggregate(聚合)对象的当前状态,我们需要运行创建预期域对象的所有事件并对其执行所有的更改。下面我们将通过一张图来说明这一架构设计方式:下面我们将列举一些使用ES的优点:时间穿梭机:可以及时重建特定聚合的状态。每个事件都包含一个时间戳。根据这些时间戳可以在特定的时间内运行事件或者停止事件。自动审计:我们不需要额外的工作就可以检查出在特定的时间范围内谁做了什么以及改变了什么。这和可以显示更改历史记录的系统日志不同,事件可以告知我们每次更改背后所对应的操作意图。易于引入纠正措施:当数据库中的数据发生错误时,我们可以将应用程序的状态回退到特定的时间点上,并重建当时的应用程序状态。易于调试:如果应用程序出现问题,我们可以将特定事件内的所有事件取出,并逐条的重建应用状态,以检查应用程序可能出现问题的地方。这样我们可以更快的找到问题,缩短调试所需的时间。AggregatesAggregate(聚合)一词在本文中多次被提及,那它到底是什么意思?Aggregate(聚合)来自于领域驱动设计(DDD)的一个概念,它指的是始终保持一致状态的实体或者相关实体组。我们可以简单的理解为接收和处理Command(包含Command Handler)的一个边界,然后根据当前状态生成事件。在通常情况下,Aggregate root(聚合根)由一个域对象构成,但它可以由多个对象组成。我们还需要注意整个应用程序可以包含多个Aggregate(聚合),并且所有事件都存储在同一个存储库中。总结CQRS/ES可以作为特定问题的解决方案。它可以在标准N层架构设计的应用程序的某些层中进行引入,它可以解决非标准问题,常规架构中我们所拿到的是最终状态,在很多情况下,固然当前状态很重要,但我们还需要知道当前状态是如何产生的。CQRS和ES两种概念应该一起使用吗?事实表明,并没有。我们想要统计任何时间范文内的域对象状态,而写库只能存储当前状态。引入CQRS并没能帮助我们解决这一问题。在下一章节中,我们将引入Axon框架,Axon框架时间了CQRS/ES,用于解决某些域对象的一些特定问题,尤其是收集历史统计数据。我们将阐述如何使用Axon框架实现CQRS/ES并实现与Spring Boot应用程的整合。作者:LukaszKucik ,译:谭朝红,原文:CQRS and Event Sourcing as an antidote for problems with retrieving application states,译文:(译)CQRS & EVENT SOURCING — 解决检索应用程序状态问题的一剂良方

April 20, 2019 · 1 min · jiezi

领域驱动设计战术模式--领域事件

使用领域事件来捕获发生在领域中的一些事情。领域驱动实践者发现他们可以通过了解更多发生在问题域中的事件,来更好的理解问题域。这些事件,就是领域事件,主要是与领域专家一起进行知识提炼环节中获得。领域事件,可以用于一个限界上下文内的领域模型,也可以使用消息队列在限界上下文间进行异步通信。1 理解领域事件领域事件是领域专家所关心的发生在领域中的一些事件。将领域中所发生的活动建模成一系列离散事件。每个事件都用领域对象表示。领域事件是领域模型的组成部分,表示领域中所发生的事情。领域事件的主要用途:保证聚合间的数据一致性替换批量处理实现事件源模式进行限界上下文集成2 实现领域事件领域事件表示已经发生的某种事实,该事实在发生后便不会改变。因此,领域事件通常建模成值对象。但,这也有特殊的情况,为了迎合序列化和反序列化框架需求,在建模时,经常会进行一定的妥协。2.1 创建领域事件2.1.1 事件命名在建模领域事件时,我们应该根据限界上下文中的通用语言来命名事件。如果事件由聚合上的命令操作产生,通常根据该操作方法的名字来命名事件。事件名字表明聚合上的命令方法在执行成功后的事实。即事件命名需要反映过去发生过的事情。public class AccountEnabledEvent extends AbstractAggregateEvent<Long, Account> { public AccountEnabledEvent(Account source) { super(source); }}2.1.2 事件属性事件的属性主要用于驱动后续业务流程。当然,也会拥有一些通用属性。事件具有一些通用属性,如:唯一标识occurredOn 发生时间type 事件类型source 事件发生源(只针对由聚合产生的事件)通用属性可以使用事件接口来规范。接口或类含义DomainEvent通用领域事件接口AggregateEvent由聚合发布的通用领域事件接口AbstractDomainEventDomainEvent 实现类,维护 id 和 创建时间AbstractAggregateEventAggregateEvent 实现类,继承子 AbstractDomainEvent,并添加 source 属性但,事件最主要的还是业务属性。我们需要考虑,是谁导致事件的发生,这可能涉及产生事件的聚合或其他参与该操作的聚合,也可能是其他任何类型的操作数据。2.1.3 事件方法事件是事实的描述,本身不会有太多的业务操作。领域事件通常被设计为不变对象,事件所携带的数据已经反映出该事件的来源。事件构造函数完成状态初始化,同时提供属性的 getter 方法。2.1.4 事件唯一标识这里需要注意的是事件唯一标识,通常情况下,事件是不可变的,那为什么会涉及唯一标识的概念呢?对于从聚合中发布出来的领域事件,使用事件的名称、产生事件的标识、事件发生的时间等足以对不同的事件进行区分。但,这样会增加事件比较的复杂性。对于由调用方发布的事件,我们将领域事件建模成聚合,可以直接使用聚合的唯一标识作为事件的标识。事件唯一标识的引入,会大大减少事件比较的复杂性。但,其最大的意义在于限界上下文的集成。当我们需要将领域事件发布到外部的限界上下文时,唯一标识就是一种必然。为了保证事件投递的幂等性,在发送端,我们可能会进行多次发送尝试,直至明确发送成功为止;而在接收端,当接收到事件后,需要对事件进行重复性检测,以保障事件处理的幂等性。此时,事件的唯一标识便可以作为事件去重的依据。事件唯一标识,本身对领域建模影响不大,但对技术处理好处巨大。因此,将它作为通用属性进行管理。2.2 发布领域事件我们如何避免领域事件与处理者间的耦合呢?一种简单高效的方式便是使用观察者模式,这种模式可以在领域事件和外部组件间进行解耦。2.2.1 发布订阅模型为了统一,我们需要定义了一套接口和实现类,以基于观察者模式,完成事件的发布。涉及接口和实现类如下:接口或类含义DomainEventPublisher用于发布领域事件DomainEventHandlerRegistry用于注册 DomainEventHandlerDomainEventBus扩展自 DomainEventPublisher 和 DomainEventHandlerRegistry 用于发布和管理领域事件处理器DefaultDomainEventBusDomainEventBus 默认实现DomainEventHandler用于处理领域事件DomainEventSubscriber用于判断是否接受领域事件DomainEventExecutor用于执行领域事件处理器使用实例如 DomainEventBusTest 所示:public class DomainEventBusTest { private DomainEventBus domainEventBus; @Before public void setUp() throws Exception { this.domainEventBus = new DefaultDomainEventBus(); } @After public void tearDown() throws Exception { this.domainEventBus = null; } @Test public void publishTest(){ // 创建事件处理器 TestEventHandler eventHandler = new TestEventHandler(); // 注册事件处理器 this.domainEventBus.register(TestEvent.class, eventHandler); // 发布事件 this.domainEventBus.publish(new TestEvent(“123”)); // 检测事件处理器是够运行 Assert.assertEquals(“123”, eventHandler.data); } @Value class TestEvent extends AbstractDomainEvent{ private String data; } class TestEventHandler implements DomainEventHandler<TestEvent>{ private String data; @Override public void handle(TestEvent event) { this.data = event.getData(); } }}在构建完发布订阅结构后,需要将其与领域模型进行关联。领域模型如何获取 Publisher,事件处理器如何进行订阅。2.2.2 基于 ThreadLocal 的事件发布比较常用的方案便是将 DomainEventBus 绑定到线程上下文。这样,只要是同一调用线程都可以方便的获取 DomainEventBus 对象。具体的交互如下:DomainEventBusHolder 用于管理 DomainEventBus。public class DomainEventBusHolder { private static final ThreadLocal<DomainEventBus> THREAD_LOCAL = new ThreadLocal<DomainEventBus>(){ @Override protected DomainEventBus initialValue() { return new DefaultDomainEventBus(); } }; public static DomainEventPublisher getPubliser(){ return THREAD_LOCAL.get(); } public static DomainEventHandlerRegistry getHandlerRegistry(){ return THREAD_LOCAL.get(); } public static void clean(){ THREAD_LOCAL.remove(); }}Account 的 enable 直接使用 DomainEventBusHolder 进行发布。public class Account extends JpaAggregate { public void enable(){ AccountEnabledEvent event = new AccountEnabledEvent(this); DomainEventBusHolder.getPubliser().publish(event); }}public class AccountEnabledEvent extends AbstractAggregateEvent<Long, Account> { public AccountEnabledEvent(Account source) { super(source); }}AccountApplication 完成订阅器注册以及业务方法调用。public class AccountApplication extends AbstractApplication { private static final Logger LOGGER = LoggerFactory.getLogger(AccountApplication.class); @Autowired private AccountRepository repository; public void enable(Long id){ // 清理之前绑定的 Handler DomainEventBusHolder.clean(); // 注册 EventHandler AccountEnableEventHandler enableEventHandler = new AccountEnableEventHandler(); DomainEventBusHolder.getHandlerRegistry().register(AccountEnabledEvent.class, enableEventHandler); Optional<Account> accountOptional = repository.getById(id); if (accountOptional.isPresent()) { Account account = accountOptional.get(); // enable 使用 DomainEventBusHolder 直接发布事件 account.enable(); repository.save(account); } } class AccountEnableEventHandler implements DomainEventHandler<AccountEnabledEvent>{ @Override public void handle(AccountEnabledEvent event) { LOGGER.info(“handle enable event”); } }}2.2.3 基于实体缓存的事件发布先将事件缓存在实体中,在实体状态成功持久化到存储后,再进行事件发布。具体交互如下:实例代码如下:public class Account extends JpaAggregate { public void enable(){ AccountEnabledEvent event = new AccountEnabledEvent(this); registerEvent(event); }}Account 的 enable 方法,调用 registerEvent 对事件进行注册。@MappedSuperclasspublic abstract class AbstractAggregate<ID> extends AbstractEntity<ID> implements Aggregate<ID> { private static final Logger LOGGER = LoggerFactory.getLogger(AbstractAggregate.class); @JsonIgnore @QueryTransient @Transient @org.springframework.data.annotation.Transient private final transient List<DomainEventItem> events = Lists.newArrayList(); protected void registerEvent(DomainEvent event) { events.add(new DomainEventItem(event)); } protected void registerEvent(Supplier<DomainEvent> eventSupplier) { this.events.add(new DomainEventItem(eventSupplier)); } @Override @JsonIgnore public List<DomainEvent> getEvents() { return Collections.unmodifiableList(events.stream() .map(eventSupplier -> eventSupplier.getEvent()) .collect(Collectors.toList())); } @Override public void cleanEvents() { events.clear(); } private class DomainEventItem { DomainEventItem(DomainEvent event) { Preconditions.checkArgument(event != null); this.domainEvent = event; } DomainEventItem(Supplier<DomainEvent> supplier) { Preconditions.checkArgument(supplier != null); this.domainEventSupplier = supplier; } private DomainEvent domainEvent; private Supplier<DomainEvent> domainEventSupplier; public DomainEvent getEvent() { if (domainEvent != null) { return domainEvent; } DomainEvent event = this.domainEventSupplier != null ? this.domainEventSupplier.get() : null; domainEvent = event; return domainEvent; } }}registerEvent 方法在 AbstractAggregate 中,registerEvent 方法将事件保存到 events 集合,getEvents 方法获取所有事件,cleanEvents 方法清理缓存的事件。Application 实例如下:@Servicepublic class AccountApplication extends AbstractApplication { private static final Logger LOGGER = LoggerFactory.getLogger(AccountApplication.class); @Autowired private AccountRepository repository; @Autowired private DomainEventBus domainEventBus; @PostConstruct public void init(){ // 使用 Spring 生命周期注册事件处理器 this.domainEventBus.register(AccountEnabledEvent.class, new AccountEnableEventHandler()); } public void enable(Long id){ Optional<Account> accountOptional = repository.getById(id); if (accountOptional.isPresent()) { Account account = accountOptional.get(); // enable 将事件缓存在 account 中 account.enable(); repository.save(account); List<DomainEvent> events = account.getEvents(); if (!CollectionUtils.isEmpty(events)){ // 成功持久化后,对事件进行发布 this.domainEventBus.publishAll(events); } } } class AccountEnableEventHandler implements DomainEventHandler<AccountEnabledEvent>{ @Override public void handle(AccountEnabledEvent event) { LOGGER.info(“handle enable event”); } }}AccountApplication 的 init 方法完成事件监听器的注册,enable 方法在实体成功持久化后,将缓存的事件通过 DomainEventBus 实例 publish 出去。2.2.4 由调用方发布事件通常情况下,领域事件是由聚合的命令方法产生,并在命令方法执行成功后,进行事件的发布。有时,领域事件并不是聚合中的命令方法产生的,而是由用户所发生的请求产生。此时,我们需要将领域事件建模成一个聚合,并且拥有自己的资源库。但,由于领域事件表示的是过去发生的事情,因此资源库只做追加操作,不能对事件进行修改和删除功能。例如,对用户点击事件进行发布。@Entity@Datapublic class ClickAction extends JpaAggregate implements DomainEvent { @Setter(AccessLevel.PRIVATE) private Long userId; @Setter(AccessLevel.PRIVATE) private String menuId; public ClickAction(Long userId, String menuId){ Preconditions.checkArgument(userId != null); Preconditions.checkArgument(StringUtils.isNotEmpty(menuId)); setUserId(userId); setMenuId(menuId); } @Override public String id() { return String.valueOf(getId()); } @Override public Date occurredOn() { return getCreateTime(); }}ClickAction 继承自 JpaAggregate 实现 DomainEvent 接口,并重写 id 和 occurredOn 方法。@Servicepublic class ClickActionApplication extends AbstractApplication { @Autowired private ClickActionRepository repository; @Autowired private DomainEventBus domainEventBus; public void clickMenu(Long id, String menuId){ ClickAction clickAction = new ClickAction(id, menuId); clickAction.prePersist(); this.repository.save(clickAction); domainEventBus.publish(clickAction); }}ClickActionApplication 在成功保存 ClickAction 后,使用 DomainEventBus 对事件进行发布。2.3 订阅领域事件由什么组件向领域事件注册订阅器呢?大多数请求,由应用服务完成,有时也可以由领域服务进行注册。由于应用服务是领域模型的直接客户,它是注册领域事件订阅器的理想场所,即在应用服务调用领域方法之前,就完成了对事件的订阅。基于 ThreadLocal 进行订阅:public void enable(Long id){ // 清理之前绑定的 Handler DomainEventBusHolder.clean(); // 注册 EventHandler AccountEnableEventHandler enableEventHandler = new AccountEnableEventHandler(); DomainEventBusHolder.getHandlerRegistry().register(AccountEnabledEvent.class, enableEventHandler); Optional<Account> accountOptional = repository.getById(id); if (accountOptional.isPresent()) { Account account = accountOptional.get(); // enable 使用 DomainEventBusHolder 直接发布事件 account.enable(); repository.save(account); }}基于实体缓存进行订阅:@PostConstructpublic void init(){ // 使用 Spring 生命周期注册事件处理器 this.domainEventBus.register(AccountEnabledEvent.class, new AccountEnableEventHandler());}public void enable(Long id){ Optional<Account> accountOptional = repository.getById(id); if (accountOptional.isPresent()) { Account account = accountOptional.get(); // enable 将事件缓存在 account 中 account.enable(); repository.save(account); List<DomainEvent> events = account.getEvents(); if (!CollectionUtils.isEmpty(events)){ // 成功持久化后,对事件进行发布 this.domainEventBus.publishAll(events); } }}2.4 处理领域事件完成事件发布后,让我们一起看下事件处理。2.4.1 保证聚合间的数据一致性我们通常将领域事件用于维护模型的一致性。在聚合建模中有一个原则,就是在一个事务中,只能对一个聚合进行修改,由此产生的变化必须在独立的事务中运行。在这种情况下,需要谨慎处理的事务的传播性。应用服务控制着事务。不要在事件通知过程中修改另一个聚合实例,因为这样会破坏聚合的一大原则:在一个事务中,只能对一个聚合进行修改。对于简单场景,我们可以使用特殊的事务隔离策略对聚合的修改进行隔离。具体流程如下:但,最佳方案是使用异步处理。及每一个定义方都在各自独立的事务中修改额外的聚合实例。事件订阅方不应该在另一个聚合上执行命令方法,因为这样将破坏“在单个事务中只修改单个聚合实例”的原则。所有聚合实例间的最终一致性必须通过异步方式处理。详见,异步处理领域事件。2.4.2 替换批量处理批处理过程通常需要复杂的查询,并且需要庞大的事务支持。如果在接收到领域事件时,系统就立即处理,业务需求不仅得到了更快的满足,而且杜绝了批处理操作。在系统的非高峰时期,通常使用批处理进行一些系统的维护,比如删除过期数据、创建新的对象、通知用户、更新统计信息等。这些批处理往往需要复杂的查询,并需要庞大的事务支持。如果我们监听系统中的领域事件,在接收领域事件时,系统立即处理。这样,原本批量集中处理的过程就被分散成许多小的处理单元,业务需要也能更快的满足,用户可以可以及时的进行下一步操作。2.4.3 实现事件源模式对于单个限界上下文中的所有领域事件,为它们维护一个事件存储具有很多的好处。对事件进行存储可以:将事件存储作为消息队列来使用,然后将领域事件通过消息设施发布出去。将事件存储用于基于 Rest 的事件通知。检查模型命名方法产生结果的历史记录。使用事件存储来进行业务预测和分析。使用事件来重建聚合实例。执行聚合的撤销操作。事件存储是个比较大的课题,将有专门章节进行讲解。2.4.4 进行限界上下文集成基于领域事件的限界上下文集成,主要由消息队列和 REST 事件两种模式。在此,重心讲解基于消息队列的上下文集成。在不同的上下文中采用消息系统时,我们必须保证最终一致性。在这种情况下,我们至少需要在两种存储之间保存最终一致性:领域模型所使用的存储和消息队列所使用的持久化存储。我们必须保证在持久化领域模型时,对于的事件也已经成功发布。如果两种不同步,模型可能会处于不正确的状态。一般情况下,有三种方式:领域模型和消息共享持久化存储。在这种情况下,模型和事件的提交在一个事务中完成,从而保证两种的一致性。领域模型和消息由全局事务控制。这种情况下,模型和消息所用的持久化存储可以分离,但会降低系统性能。在领域持久化存储中,创建一个特殊的存储区域用于存储事件(也就是事件存储),从而在本地事务中完成领域和事件的存储。然后,通过后台服务将事件异步发送到消息队列中。一般情况下,第三种,是比较优雅的解决方案。在一致性要求不高时,可以通过领域事件订阅器直接向消息队列发送事件。具体流程如下:对一致性要求高时,需要先将事件存储,然后通过后台线程加载并分发到消息队列。具体流程如下:2.5 异步处理领域事件领域事件可以与异步工作流程协同,包括限界上下文间使用消息队列进行异步通信。当然,在同一个限界上下文中,也可以启动异步处理流程。作为事件的发布者,不应关心是否执行异步处理。异常处理是由事件执行者决定。DomainEventExecutor 提供对异步处理的支持。DomainEventExecutor eventExecutor = new ExecutorBasedDomainEventExecutor(“EventHandler”, 1, 100);this.domainEventBus.register(AccountEnabledEvent.class, eventExecutor, new AccountEnableEventHandler());异步处理,就意味着放弃数据库事务的 ACID 特性,而选择使用最终一致性。2.6 内部事件与外部事件使用领域事件时需要对事件进行区分,以避免技术实现的问题。认识内部事件和外部事件之间的区别至关重要。内部事件,是一个领域模型内部的事件,不在有界上下文间进行共享。外部事件,是对外发布的事件,在多个有界上下文中进行共享。一般情况下,在典型的业务用例中,可能会有很多的内部事件,而只有一两个外部事件。2.6.1 内部事件内部事件存在于限界上下文内部,受限界上下文边界保护。内部事件被限制在单个有界上下文边界内部,所以可以直接引用领域对象。public interface AggregateEvent<ID, A extends Aggregate<ID>> extends DomainEvent{ A source(); default A getSource(){ return source(); }}比如 AggregateEvent 中的 source 指向发布该事件的聚合。public class LikeSubmittedEvent extends AbstractAggregateEvent<Long, Like> { public LikeSubmittedEvent(Like source) { super(source); } public LikeSubmittedEvent(String id, Like source) { super(id, source); }}LikeSubmittedEvent 类直接引用 Like 聚合。2.6.2 外部事件外部事件存在于限界上下文间,被多个上下文共享。一般情况下,外部事件,只作为数据载体存在。常常采用平面结构,并公开所有属性。@Datapublic class SubmittedEvent { private Owner owner; private Target target;}SubmittedEvent 为扁平化结构,主要是对数据的封装。由于外部事件被多个上下文共享,版本管理就显得非常重要,以避免重大更改对其服务造成影响。3 实现领域事件模式领域事件是一种通用模式,它的本质是将领域概念添加到发布-订阅模式。3.1 封装领域事件的“发布-订阅”模式发布-订阅是比较成熟的设计模式,具有很高的通用性。因此,建议针对领域需求进行封装。比如直接使用 geekhalo-ddd 相关模块。定义领域事件:@Valuepublic class LikeCancelledEvent extends AbstractAggregateEvent<Long, Like> { public LikeCancelledEvent(Like source) { super(source); }}订阅领域事件:this.domainEventBus.register(LikeCancelledEvent.class, likeCancelledEvent->{ CanceledEvent canceledEvent = new CanceledEvent(); canceledEvent.setOwner(likeCancelledEvent.getSource().getOwner()); canceledEvent.setTarget(likeCancelledEvent.getSource().getTarget()); this.redisBasedQueue.pushLikeEvent(canceledEvent); });异步执行领域事件:DomainEventExecutor eventExecutor = new ExecutorBasedDomainEventExecutor(“LikeEventHandler”, 1, 100);this.domainEventBus.register(LikeCancelledEvent.class, eventExecutor, likeCancelledEvent->{ CanceledEvent canceledEvent = new CanceledEvent(); canceledEvent.setOwner(likeCancelledEvent.getSource().getOwner()); canceledEvent.setTarget(likeCancelledEvent.getSource().getTarget()); this.redisBasedQueue.pushLikeEvent(canceledEvent); });3.2 内存总线处理内部事件,消息队列处理外部事件内存总线简单高效,同时支持同步、异步两个处理方案,比较适合处理繁杂的内部事件;消息队列虽然复杂,但擅长解决服务间通信问题,适合处理外部事件。3.3 使用实体缓存领域事件理论上,只有在业务成功完成后,才应该对外发布事件。因此,将领域事件缓存在实体中,并在完成业务操作后将其进行发布,是一种较好的解决方案。相比,使用 ThreadLocal 管理订阅器,并在事件 publish 时进行订阅回调,事件缓存方案有明显的优势。3.4 使用 IOC 容器的事件发布功能IOC 容器为我们提供了很多使用功能,其中也包括发布-订阅功能,如 Spring。通常情况下,领域模型不应该直接依赖于 Spring 容器。因此,在领域中我们仍然使用内存总线,为其添加一个订阅者,将内存总线中的事件转发到 Spring 容器中。class SpringEventDispatcher implements ApplicationEventPublisherAware { @Autowired private DomainEventBus domainEventBus; private ApplicationEventPublisher eventPublisher; @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.eventPublisher = applicationEventPublisher; } @PostConstruct public void addListener(){ this.domainEventBus.register(event->true, event -> {this.eventPublisher.publishEvent(event);}); }}此时,我们就可以直接使用 Spring 的 EventListener 机制对领域事件进行处理。@Componentpublic class RedisBasedQueueExporter { @Autowired private RedisBasedQueue redisBasedQueue; @EventListener public void handle(LikeSubmittedEvent likeSubmittedEvent){ SubmittedEvent submittedEvent = new SubmittedEvent(); submittedEvent.setOwner(likeSubmittedEvent.getSource().getOwner()); submittedEvent.setTarget(likeSubmittedEvent.getSource().getTarget()); this.redisBasedQueue.pushLikeEvent(submittedEvent); } @EventListener public void handle(LikeCancelledEvent likeCancelledEvent){ CanceledEvent canceledEvent = new CanceledEvent(); canceledEvent.setOwner(likeCancelledEvent.getSource().getOwner()); canceledEvent.setTarget(likeCancelledEvent.getSource().getTarget()); this.redisBasedQueue.pushLikeEvent(canceledEvent); }}4 小结领域事件是发生在问题域中的事实,它是通用语言的一部分。领域事件优先使用发布-订阅模式,会发布事件并且触发相应的事件处理器。限界上下文内,优先使用内部事件和内存总线;限界上下文间,优先使用外部事件和消息队列。领域事件使异步操作变得简单。领域事件为聚合间提供了最终一致性。领域事件可以将大的批量操作简化为许多小的业务操作。领域事件可以完成强大的事件存储。领域事件可以完成限界上下文间的集成。领域事件是更复杂架构(CQRS)的一种支持。 ...

April 16, 2019 · 5 min · jiezi

战术模式--领域服务

在建模时,有时会遇到一些业务逻辑的概念,它放在实体或值对象中都不太合适。这就是可能需要创建领域服务的一个信号。1 理解领域服务从概念上说,领域服务代表领域概念,它们是存在于问题域中的行为,它们产生于与领域专家的对话中,并且是领域模型的一部分。模型中的领域服务表示一个无状态的操作,他用于实现特定于某个领域的任务。当领域中某个操作过程或转化过程不是实体或值对象的职责时,我们便应该将该操作放在一个单独的元素中,即领域服务。同时务必保持该领域服务与通用语言是一致的,并且保证它是无状态的。领域服务有几个重要的特征:它代表领域概念。它与通用语言保存一致,其中包括命名和内部逻辑。它无状态。领域服务与聚合在同一包中。1.1 何时使用领域服务如果某操作不适合放在聚合和值对象上时,最好的方式便是将其建模成领域服务。一般情况下,我们使用领域服务来组织实体、值对象并封装业务概念。领域服务适用场景如下:执行一个显著的业务操作过程。对领域对象进行转换。以多个领域对象作为输入,进行计算,产生一个值对象。1.2 避免贫血领域模型当你认同并非所有的领域行为都需要封装在实体或值对象中,并明确领域服务是有用的建模手段后,就需要当心了。不要将过多的行为放到领域服务中,这样将导致贫血领域模型。如果将过多的逻辑推入领域服务中,将导致不准确、难理解、贫血并且低概念的领域模型。显然,这样会抵消 DDD 的很多好处。领域服务是排在值对象、实体模式之后的一个选项。有时,不得已为之是个比较好的方案。1.3 与应用服务的对比应用服务,并不会处理业务逻辑,它是领域模型直接客户,进而是领域服务的客户方。领域服务代表了存在于问题域内部的概念,他们的接口存在于领域模型中。相反,应用服务不表示领域概念,不包含业务规则,通常,他们不存在于领域模型中。应用服务存在于服务层,处理像事务、订阅、存储等基础设施问题,以执行完整的业务用例。应用服务从用户用例出发,是领域的直接用户,与领域关系密切,会有专门章节进行详解。1.4 与基础设施服务的对比基础设施服务,从技术角度出发,为解决通用问题而进行的抽象。比较典型的如,邮件发送服务、短信发送服务、定时服务等。2. 实现领域服务2.1 封装业务概念领域服务的执行一般会涉及实体或值对象,在其基础之上将行为封装成业务概念。比较常见的就是银行转账,首先银行转账具有明显的领域概念,其次,由于同时涉及两个账号,该行为放在账号聚合中不太合适。因此,可以将其建模成领域服务。public class Account extends JpaAggregate { private Long totalAmount; public void checkBalance(Long amount) { if (amount > this.totalAmount){ throw new IllegalArgumentException(“余额不足”); } } public void reduce(Long amount) { this.totalAmount = this.totalAmount - amount; } public void increase(Long amount) { this.totalAmount = this.totalAmount + amount; }}Account 提供余额检测、扣除和添加等基本功能。public class TransferService implements DomainService { public void transfer(Account from, Account to, Long amount){ from.checkBalance(amount); from.reduce(amount); to.increase(amount); }}TransferService 按照业务规则,指定转账流程。TransferService 明确定义了一个存在于通用语言的一个领域概念。领域服务存在于领域模型中,包含重要的业务规则。2.2 业务计算业务计算,主要以实体或值对象作为输入,通过计算,返回一个实体或值对象。常见场景如计算一个订单应用特定优惠策略后的应付金额。public class OrderItem { private Long price; private Integer count; public Long getTotalPrice(){ return price * count; }}OrderItem 中包括产品单价和产品数量,getTotalPrice 通过计算获取总价。public class Order { private List<OrderItem> items = Lists.newArrayList(); public Long getTotalPrice(){ return this.items.stream() .mapToLong(orderItem -> orderItem.getTotalPrice()) .sum(); }}Order 由多个 OrderItem 组成,getTotalPrice 遍历所有的 OrderItem,计算订单总价。public class OrderAmountCalculator { public Long calculate(Order order, PreferentialStrategy preferentialStrategy){ return preferentialStrategy.calculate(order.getTotalPrice()); }}OrderAmountCalculator 以实体 Order 和领域服务 PreferentialStrategy 为输入,在订单总价基础上计算折扣价格,返回打折之后的价格。2.3 规则切换根据业务流程,动态对规则进行切换。还是以订单的优化策略为例。public interface PreferentialStrategy { Long calculate(Long amount);}PreferentialStrategy 为策略接口。public class FullReductionPreferentialStrategy implements PreferentialStrategy{ private final Long fullAmount; private final Long reduceAmount; public FullReductionPreferentialStrategy(Long fullAmount, Long reduceAmount) { this.fullAmount = fullAmount; this.reduceAmount = reduceAmount; } @Override public Long calculate(Long amount) { if (amount > fullAmount){ return amount - reduceAmount; } return amount; }}FullReductionPreferentialStrategy 为满减策略,当订单总金额超过特定值时,直接进行减免。public class FixedDiscountPreferentialStrategy implements PreferentialStrategy{ private final Double descount; public FixedDiscountPreferentialStrategy(Double descount) { this.descount = descount; } @Override public Long calculate(Long amount) { return Math.round(amount * descount); }}FixedDiscountPreferentialStrategy 为固定折扣策略,在订单总金额基础上进行固定折扣。2.4 基础设施(第三方接口)隔离领域概念本身属于领域模型,但具体实现依赖于基础设施。此时,我们需要将领域概念建模成领域服务,并将其置于模型层。将依赖于基础设施的具体实现类,放置于基础设施层。比较典型的例子便是密码加密,加密服务应该位于领域中,但具体的实现依赖基础设施,应该放在基础设施层。public interface PasswordEncoder { String encode(CharSequence rawPassword); boolean matches(CharSequence rawPassword, String encodedPassword);}PasswordEncoder 提供密码加密和密码验证功能。public class BCryptPasswordEncoder implements PasswordEncoder { private Pattern BCRYPT_PATTERN = Pattern .compile("\A\$2a?\$\d\d\$[./0-9A-Za-z]{53}"); private final Log logger = LogFactory.getLog(getClass()); private final int strength; private final SecureRandom random; public BCryptPasswordEncoder() { this(-1); } public BCryptPasswordEncoder(int strength) { this(strength, null); } public BCryptPasswordEncoder(int strength, SecureRandom random) { if (strength != -1 && (strength < BCrypt.MIN_LOG_ROUNDS || strength > BCrypt.MAX_LOG_ROUNDS)) { throw new IllegalArgumentException(“Bad strength”); } this.strength = strength; this.random = random; } public String encode(CharSequence rawPassword) { String salt; if (strength > 0) { if (random != null) { salt = BCrypt.gensalt(strength, random); } else { salt = BCrypt.gensalt(strength); } } else { salt = BCrypt.gensalt(); } return BCrypt.hashpw(rawPassword.toString(), salt); } public boolean matches(CharSequence rawPassword, String encodedPassword) { if (encodedPassword == null || encodedPassword.length() == 0) { logger.warn(“Empty encoded password”); return false; } if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) { logger.warn(“Encoded password does not look like BCrypt”); return false; } return BCrypt.checkpw(rawPassword.toString(), encodedPassword); }}BCryptPasswordEncoder 提供基于 BCrypt 的实现。public class SCryptPasswordEncoder implements PasswordEncoder { private final Log logger = LogFactory.getLog(getClass()); private final int cpuCost; private final int memoryCost; private final int parallelization; private final int keyLength; private final BytesKeyGenerator saltGenerator; public SCryptPasswordEncoder() { this(16384, 8, 1, 32, 64); } public SCryptPasswordEncoder(int cpuCost, int memoryCost, int parallelization, int keyLength, int saltLength) { if (cpuCost <= 1) { throw new IllegalArgumentException(“Cpu cost parameter must be > 1.”); } if (memoryCost == 1 && cpuCost > 65536) { throw new IllegalArgumentException(“Cpu cost parameter must be > 1 and < 65536.”); } if (memoryCost < 1) { throw new IllegalArgumentException(“Memory cost must be >= 1.”); } int maxParallel = Integer.MAX_VALUE / (128 * memoryCost * 8); if (parallelization < 1 || parallelization > maxParallel) { throw new IllegalArgumentException(“Parallelisation parameter p must be >= 1 and <= " + maxParallel + " (based on block size r of " + memoryCost + “)”); } if (keyLength < 1 || keyLength > Integer.MAX_VALUE) { throw new IllegalArgumentException(“Key length must be >= 1 and <= " + Integer.MAX_VALUE); } if (saltLength < 1 || saltLength > Integer.MAX_VALUE) { throw new IllegalArgumentException(“Salt length must be >= 1 and <= " + Integer.MAX_VALUE); } this.cpuCost = cpuCost; this.memoryCost = memoryCost; this.parallelization = parallelization; this.keyLength = keyLength; this.saltGenerator = KeyGenerators.secureRandom(saltLength); } public String encode(CharSequence rawPassword) { return digest(rawPassword, saltGenerator.generateKey()); } public boolean matches(CharSequence rawPassword, String encodedPassword) { if (encodedPassword == null || encodedPassword.length() < keyLength) { logger.warn(“Empty encoded password”); return false; } return decodeAndCheckMatches(rawPassword, encodedPassword); } private boolean decodeAndCheckMatches(CharSequence rawPassword, String encodedPassword) { String[] parts = encodedPassword.split(”\$”); if (parts.length != 4) { return false; } long params = Long.parseLong(parts[1], 16); byte[] salt = decodePart(parts[2]); byte[] derived = decodePart(parts[3]); int cpuCost = (int) Math.pow(2, params >> 16 & 0xffff); int memoryCost = (int) params >> 8 & 0xff; int parallelization = (int) params & 0xff; byte[] generated = SCrypt.generate(Utf8.encode(rawPassword), salt, cpuCost, memoryCost, parallelization, keyLength); if (derived.length != generated.length) { return false; } int result = 0; for (int i = 0; i < derived.length; i++) { result |= derived[i] ^ generated[i]; } return result == 0; } private String digest(CharSequence rawPassword, byte[] salt) { byte[] derived = SCrypt.generate(Utf8.encode(rawPassword), salt, cpuCost, memoryCost, parallelization, keyLength); String params = Long .toString(((int) (Math.log(cpuCost) / Math.log(2)) << 16L) | memoryCost << 8 | parallelization, 16); StringBuilder sb = new StringBuilder((salt.length + derived.length) * 2); sb.append("$”).append(params).append(’$’); sb.append(encodePart(salt)).append(’$’); sb.append(encodePart(derived)); return sb.toString(); } private byte[] decodePart(String part) { return Base64.getDecoder().decode(Utf8.encode(part)); } private String encodePart(byte[] part) { return Utf8.decode(Base64.getEncoder().encode(part)); }}SCryptPasswordEncoder 提供基于 SCrypt 的实现。2.5 模型概念转化在限界上下文集成时,经常需要对上游限界上下文中的概念进行转换,以避免概念的混淆。例如,在用户成功激活后,自动为其创建名片。在用户激活后,会从 User 限界上下文中发出 UserActivatedEvent 事件,Card 上下文监听事件,并将用户上下文内的概念转为为名片上下文中的概念。@Valuepublic class UserActivatedEvent extends AbstractDomainEvent { private final String name; private final Long userId; public UserActivatedEvent(String name, Long userId) { this.name = name; this.userId = userId; }}UserActivatedEvent 是用户上下文,在用户激活后向外发布的领域事件。@Servicepublic class UserEventHandlers { @EventListener public void handle(UserActivatedEvent event){ Card card = new Card(); card.setUserId(event.getUserId()); card.setName(event.getName()); }}UserEventHandlers 在收到 UserActivatedEvent 事件后,将来自用户上下文中的概念转化为自己上下文中的概念 Card。2.6 在服务层中使用领域服务领域服务可以在应用服务中使用,已完成特定的业务规则。最常用的场景为,应用服务从存储库中获取相关实体并将它们传递到领域服务中。public class OrderApplication { @Autowired private OrderRepository orderRepository; @Autowired private OrderAmountCalculator orderAmountCalculator; @Autowired private Map<String, PreferentialStrategy> strategyMap; public Long calculateOrderTotalPrice(Long orderId, String strategyName){ Order order = this.orderRepository.getById(orderId).orElseThrow(()->new AggregateNotFountException(String.valueOf(orderId))); PreferentialStrategy strategy = this.strategyMap.get(strategyName); Preconditions.checkArgument(strategy != null); return this.orderAmountCalculator.calculate(order, strategy); }}OrderApplication 首先通过 OrderRepository 获取 Order 信息,然后获取对应的 PreferentialStrategy,最后调用 OrderAmountCalculator 完成金额计算。在服务层使用,领域服务和其他领域对象可以根据需求很容易的拼接在一起。当然,我们也可以将领域服务作为业务方法的参数进行传递。public class UserApplication extends AbstractApplication { @Autowired private PasswordEncoder passwordEncoder; @Autowired private UserRepository userRepository; public void updatePassword(Long userId, String password){ updaterFor(this.userRepository) .id(userId) .update(user -> user.updatePassword(password, this.passwordEncoder)) .call(); } public boolean checkPassword(Long userId, String password){ return this.userRepository.getById(userId) .orElseThrow(()-> new AggregateNotFountException(String.valueOf(userId))) .checkPassword(password, this.passwordEncoder); }}UserApplication 中的 updatePassword 和 checkPassword 在流程中都需要使用领域服务 PasswordEncoder,我们可以通过参数将 UserApplication 所保存的 PasswordEncoder 传入到业务方法中。2.7 在领域层中使用领域服务由于实体和领域服务拥有不同的生命周期,在实体依赖领域服务时,会变的非常棘手。有时,一个实体需要领域服务来执行操作,以避免在应用服务中的拼接。此时,我们需要解决的核心问题是,在实体中如何获取服务的引用。通常情况下,有以下几种方式。2.7.1 手工链接如果一个实体依赖领域服务,同时我们自己在管理对象的构建,那么最简单的方式便是将相关服务通过构造函数传递进去。还是以 PasswordEncoder 为例。@Datapublic class User extends JpaAggregate { private final PasswordEncoder passwordEncoder; private String password; public User(PasswordEncoder passwordEncoder) { this.passwordEncoder = passwordEncoder; } public void updatePassword(String pwd){ setPassword(passwordEncoder.encode(pwd)); } public boolean checkPassword(String pwd){ return passwordEncoder.matches(pwd, getPassword()); }}如果,我们完全手工维护 User 的创建,可以在构造函数中传入领域服务。当然,如果实体是通过 ORM 框架获取的,通过构造函数传递将变得比较棘手,我们可以为其添加一个 init 方法,来完成服务的注入。@Datapublic class User extends JpaAggregate { private PasswordEncoder passwordEncoder; private String password; public void init(PasswordEncoder passwordEncoder){ this.setPasswordEncoder(passwordEncoder); } public User(PasswordEncoder passwordEncoder) { this.passwordEncoder = passwordEncoder; } public void updatePassword(String pwd){ setPassword(passwordEncoder.encode(pwd)); } public boolean checkPassword(String pwd){ return passwordEncoder.matches(pwd, getPassword()); }}通过 ORM 框架获取 User 后,调用 init 方法设置 PasswordEncoder。2.7.2 依赖注入如果在使用 Spring 等 IOC 框架,我们可以在从 ORM 框架中获取实体后,使用依赖注入完成领域服务的注入。@Datapublic class User extends JpaAggregate { @Autowired private PasswordEncoder passwordEncoder; private String password; public void updatePassword(String pwd){ setPassword(passwordEncoder.encode(pwd)); } public boolean checkPassword(String pwd){ return passwordEncoder.matches(pwd, getPassword()); }}User 直接使用 @Autowired 注入领域服务。public class UserApplication extends AbstractApplication { @Autowired private AutowireCapableBeanFactory beanFactory; @Autowired private UserRepository userRepository; public void updatePassword(Long userId, String password){ User user = this.userRepository.getById(userId).orElseThrow(() -> new AggregateNotFountException(String.valueOf(userId))); this.beanFactory.autowireBean(user); user.updatePassword(password); this.userRepository.save(user); } public boolean checkPassword(Long userId, String password){ User user = this.userRepository.getById(userId).orElseThrow(() -> new AggregateNotFountException(String.valueOf(userId))); this.beanFactory.autowireBean(user); return user.checkPassword(password); }}UserApplication 在获取 User 对象后,首先调用 autowireBean 完成 User 对象的依赖绑定,然后在进行业务处理。2.7.3 服务定位器有时在实体中添加字段以维持领域服务引用,会使的实体变得臃肿。此时,我们可以通过服务定位器进行领域服务的查找。一般情况下,服务定位器会提供一组静态方法,以方便的获取其他服务。@Componentpublic class ServiceLocator implements ApplicationContextAware { private static ApplicationContext APPLICATION; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { APPLICATION = applicationContext; } public static <T> T getService(Class<T> service){ return APPLICATION.getBean(service); }}ServiceLocator 实现 ApplicationContextAware 接口,通过 Spring 回调将 ApplicationContext 绑定到静态字段 APPLICATION 上。getService 方法直接使用 ApplicationContext 获取领域服务。@Datapublic class User extends JpaAggregate { private String password; public void updatePassword(String pwd){ setPassword(ServiceLocator.getService(PasswordEncoder.class).encode(pwd)); } public boolean checkPassword(String pwd){ return ServiceLocator.getService(PasswordEncoder.class).matches(pwd, getPassword()); }}User 对象直接使用静态方法获取领域服务。以上模式重点解决如果将领域服务注入到实体中,而 领域事件 模式从相反方向努力,解决如何阻止注入的发生。2.7.4 领域事件解耦一种完全避免将领域服务注入到实体中的模式是领域事件。当重要的操作发生时,实体可以发布一个领域事件,注册了该事件的订阅器将处理该事件。此时,领域服务驻留在消息的订阅方内,而不是驻留在实体中。比较常见的实例是用户通知,例如,在用户激活后,为用户发送一个短信通知。@Datapublic class User extends JpaAggregate { private UserStatus status; private String name; private String password; public void activate(){ setStatus(UserStatus.ACTIVATED); registerEvent(new UserActivatedEvent(getName(), getId())); }}首先,User 在成功 activate 后,将自动注册 UserActivatedEvent 事件。public class UserApplication extends AbstractApplication { @Autowired private PasswordEncoder passwordEncoder; @Autowired private UserRepository userRepository; private DomainEventBus domainEventBus = new DefaultDomainEventBus(); @PostConstruct public void init(){ this.domainEventBus.register(UserActivatedEvent.class, event -> { sendSMSNotice(event.getUserId(), event.getName()); }); } private void sendSMSNotice(Long userId, String name) { // 发送短信通知 } public void activate(Long userId){ updaterFor(this.userRepository) .publishBy(domainEventBus) .id(userId) .update(user -> user.activate()) .call(); }}UserApplication 通过 Spring 的回调方法 init,订阅 UserActivatedEvent 事件,在事件触发后执行发短信逻辑。activate 方法在成功更新 User 后,将对缓存的事件进行发布。3. 领域服务建模模式3.1 独立接口是否有必要很多情况下,独立接口时没有必要的。我们只需创建一个实现类即可,其命名与领域服务相同(名称来自通用语言)。但在下面情况下,独立接口时有必要的(独立接口对解耦是有好处的):存在多个实现。领域服务的实现依赖基础框架的支持。测试环节需要 mock 对象。3.2 避免静态方法对于行为建模,很多人第一反应是使用静态方法。但,领域服务比静态方法存在更多的好处。领域服务比静态方法要好的多:通过多态,适配多个实现,同时可以使用模板方法模式,对结构进行优化;通过依赖注入,获取其他资源;类名往往比方法名更能表达领域概念。从表现力角度出发,类的表现力大于方法,方法的表现力大于代码。3.3 优先使用领域事件进行解耦领域事件是最优雅的解耦方案,基本上没有之一。我们将在领域事件中进行详解。3.4 策略模式当领域服务存在多个实现时,天然形成了策略模式。当领域服务存在多个实现时,可以根据上下文信息,动态选择具体的实现,以增加系统的灵活性。详见 PreferentialStrategy 实例。4. 小结有时,行为不属于实体或值对象,但它是一个重要的领域概念,这就暗示我们需要使用领域服务模式。领域服务代表领域概念,它是对通用语言的一种建模。领域服务主要使用实体或值对象组成无状态的操作。领域服务位于领域模型中,对于依赖基础设施的领域服务,其接口定义位于领域模型中。过多的领域服务会导致贫血模型,使之与问题域无法很好的配合。过少的领域服务会导致将不正确的行为添加到实体或值对象上,造成概念的混淆。当实体依赖领域服务时,可以使用手工注入、依赖注入和领域事件等多种方式进行处理。 ...

April 9, 2019 · 7 min · jiezi

领域驱动设计战术篇--实体

在问题空间中存在很多具有固有身份的概念,通常情况下,这些概念将建模为实体。实体是具有唯一标识的概念,找到领域中的实体并对其进行建模是非常重要的环节。如果理解一个概念是一个实体,就应该追问领域专家相关的细节,比如概念生命周期、核心数据、具体操作、不变规则等;从技术上来说,我们可以应用实体相关模式和实践。1 理解实体一个实体是一个唯一的东西,并且可以在相当长的一段时间内持续变化。实体是一个具有身份和连贯性的概念。身份 是一个重要的领域概念,应该显示建模以提高其在领域中的表达性。连贯性 指通过唯一身份来让某个概念,在生命周期的各阶段被发现、被更新甚至被删除。一个实体就是一个独立的事物。每个实体都拥有一个 唯一标识符 (也就是身份),并通过 标识 与和 类型 对实体进行区分开。通常情况下,实体是可变的,也就是说,他的状态随着时间发生变化。唯一身份标识 和 可变性特征 将实体对象和值对象区分开来。由于从数据建模出发,通常情况下,CRUD 系统不能创建出好的业务模型。在使用 DDD 的情况下,我们会将数据模型转化成实体模型。从根本上说,实体主要与身份有关,它关注“谁”而非 “什么”。2 实现实体大多数实体都有类似的特征,因此存在一些设计和实现上的技巧,其中包括唯一标识、属性、行为、验证等。在实体设计早期,我们刻意将关注点放在能体现实体 唯一性属性 和 行为 上,同时还将关注如何对实体进行查询。2.1 唯一标识有时,实体具有明确的自然标识,可以通过对概念的建模来实现;有时,可能没有已存的自然标识,将由应用程序生成并分配一个合理的标识,并将其用于数据存储。值对象 作为实体的唯一标识,能够更好的表达领域概念。标识具有稳定性 在为实体分配标识后,我们绝对不允许对其进行修改。如果键改变了,那么系统中所有引用该键的地方都需要同步更新,通常情况下,这是不可能做到的。不然,将导致严重的业务问题。2.1.1 自然键作为唯一标识在考虑实体身份时,首先考虑该实体所在问题空间是否已经存在唯一标识符,这些标识符被称为自然键。通常情况下,以下几类信息可以作为自然键使用:身份证号国家编号税务编号书籍 ISBN…在使用时,我们通常使用值对象模式对自然键进行建模,然后为实体添加一个构造函数,并在构造函数中完成唯一标识的分配。首先,需要对书籍 ISBN 值对象建模:@Valuepublic class ISBN { private String value;}然后,对 Book 实体建模:@Datapublic class Book { private ISBN id; public Book(ISBN isbn){ this.setId(isbn); } public ISBN getId(){ return this.id; } private void setId(ISBN id){ Preconditions.checkArgument(id != null); this.id = id; }}Book 在构造函数中完成 id 的赋值,之后便不会修改,以保护实体标识的稳定性。自然键,在实际研发中,很少使用。特别是在需要用户手工输入的情况下,难免会造成输入错误。对标识的修改会导致引用失效,因此,我们很少使用用户提供的唯一标识。通常情况下,会将用户输入作为实体属性,这些属性可以用于对象匹配,但是我们并不将这样的属性作为唯一身份标识。2.1.2 应用程序生成唯一标识当问题域中没有唯一标识时,我们需要决定标识生成策略并生成它。最常见的生成方式包括自增数值、全局唯一标识符(UUID、GUID等)以及字符串等。自增数值数字通常具有最小的空间占用,非常利于持久化,但需要维护分配 ID 的全局计数器。我们可以使用全局的静态变量作为全局计数器,如:public final class NumberGenerator { private static final AtomicLong ATOMIC_LONG = new AtomicLong(1); public static Long nextNumber(){ return ATOMIC_LONG.getAndIncrement(); }}但是,但应用崩溃或重启时,静态变量就会丢失它的值,这意味着会生成重复的 ID,从而导致业务问题。为了纠正这个问题,我们需要利用全局持久化资源构建计数器。我们可以使用 Redis 或 DB 构建自己的全局计数器。基于 Redis inc 指令的全局计数器:@Componentpublic class RedisBasedNumberGenerator { private static final String NUMBER_GENERATOR_KEY = “number-generator”; @Autowired private RedisTemplate<String, Long> redisTemplate; public Long nextNumber(){ return this.redisTemplate.boundValueOps(NUMBER_GENERATOR_KEY) .increment(); }}基于 DB 乐观锁的全局计数器:首先,定义用于生成 Number 的表结构:create table tb_number_gen( id bigint auto_increment primary key, version bigint not null, type varchar(16) not null, current_number bigint not null );create unique index ‘unq_type’ on tb_number_gen (’type’);然后,使用乐观锁完成 Number 生成逻辑:@Componentpublic class DBBasedNumberGenerator { private static final String NUMBER_KEY = “common”; private JdbcTemplate jdbcTemplate; @Autowired public void setDataSource(DataSource dataSource){ this.jdbcTemplate = new JdbcTemplate(dataSource); } public Long nextNumber(){ do { try { Long number = nextNumber(NUMBER_KEY); if (number != null){ return number; } }catch (Exception e){ // 乐观锁更新失败,进行重试// LOGGER.error(“opt lock failure to generate number, retry …”); } }while (true); } /** * 表结构: * create table tb_number_gen * ( * id bigint auto_increment primary key, * version bigint not null, * type varchar(16) not null, * current_number bigint not null * ); * add unique index ‘unq_type’ on tb_number_gen (’type’); * * @param type * @return / private Long nextNumber(String type){ NumberGen numberGen = jdbcTemplate.queryForObject( “select id, type, version, current_number as currentNumber " + “from tb_number_gen " + “where type = ‘” + type +”’”, NumberGen.class); if (numberGen == null){ // 不存在时,创建新记录 int result = jdbcTemplate.update(“insert into tb_number_gen (type, version, current_number) value (’” + type +" ‘, ‘0’, ‘1’)"); if (result > 0){ return 1L; }else { return null; } }else { // 存在时,使用乐观锁 version 更新记录 int result = jdbcTemplate.update(“update tb_number_gen " + “set version = version + 1,” + “current_number = current_number + 1 " + “where " + “id = " + numberGen.getId() + " " + " and " + “version = " + numberGen.getVersion() ); // 更新成功,说明从读取到更新这段时间,数据没有发生变化,numberGen 有效,结果为 number + 1 if (result > 0){ return numberGen.getCurrentNumber() + 1; }else { // 更新失败,说明从读取到更新这段时间,数据发生变化,numberGen 无效,获取 number 失败 return null; } } } @Data class NumberGen{ private Long id; private String type; private int version; private Long currentNumber; }}全局唯一标识符GUID 生成非常方便,并且自身就保障是唯一的,不过在持久化时会占用更多的存储空间。这些额外的空间相对来说微不足道,因此对大多数应用来说,GUID 是默认方法。有很多算法可以生成全局唯一的标识,如 UUID、GUID 等。生成策略,需要参考很多因子,以产生唯一标识:计算节点当前时间,以毫秒记;计算节点的 IP 地址;虚拟机中工厂对象实例的对象标识;虚拟机中由同一个随机数生成器生成的随机数但,我们没有必要自己写算法构建唯一标识。Java 中的 UUID 是一种快速生成唯一标识的方法。@Componentpublic class UUIDBasedNumberGenerator { public String nextId(){ return UUID.randomUUID().toString(); }}如果对性能有很高要求的场景,可以将 UUID 实例缓存起来,通过后台线程不断的向缓存中添加新的 UUID 实例。@Componentpublic class UUIDBasedPoolNumberGenerator { private static final Logger LOGGER = LoggerFactory.getLogger(UUIDBasedPoolNumberGenerator.class); private final BlockingQueue<String> idQueue = new LinkedBlockingQueue<>(100); private Thread createThread; /* * 直接从队列中获取已经生成的 ID * @return / public String nextId(){ try { return idQueue.take(); } catch (InterruptedException e) { LOGGER.error(“failed to take id”); return null; } } /* * 创建后台线程,生成 ID 并放入到队列中 / @PostConstruct public void init(){ this.createThread = new Thread(new CreateTask()); this.createThread.start(); } /* * 销毁线程 / @PreDestroy public void destroy(){ this.createThread.interrupt(); } /* * 不停的向队列中放入 UUID / class CreateTask implements Runnable{ @Override public void run() { while (!Thread.currentThread().isInterrupted()){ try { idQueue.put(UUID.randomUUID().toString()); } catch (InterruptedException e) { LOGGER.error(“failed to create uuid”); } } } }}当在浏览器中创建一个实体并提交回多个后端 API 时,GUID 就会非常有用。如果没有 ID 后端服务将无法对相同实体进行识别。这时,最好使用 JavaScript 在客户端创建一个 GUID 来解决。在浏览器中生成 GUID,可以有效控制提交数据的幂等性。字符串字符串常用于自定义 ID 格式,比如基于时间戳、多特征组合等。如下例订单唯一标识:public class OrderIdUtils { public static String createOrderId(String day, String owner, Long number){ return String.format("%s-%s-%s”, day, owner, number); }}一个订单 ID 由日期、所有者和序号三者组成。对于标识,使用 String 来维护并不是很好的方法,无法对其生成策略、具体格式进行有效限制。使用一个值对象会更加合适。@Valuepublic class OrderId { private final String day; private final String owner; private final Long number; public OrderId(String day, String owner, Long number) { this.day = day; this.owner = owner; this.number = number; } public String getValue(){ return String.format("%s-%s-%s”, getDay(), getOwner(), getNumber()); } @Override public String toString(){ return getValue(); }}相比之下,OrderId 比 String 拥有更强的表达力。2.1.3 持久化存储生成唯一标识将唯一标识的生成委派给持久化机制是最简单的方案。我们从数据库获取的序列总是递增,结果总是唯一的。大多数数据库(如 MySQL)都原生支持 ID 的生成。我们把新建实体传递到数据访问框架,在事务成功完成后,实体便有了 ID 标识。一个使用 JPA 持久化的实例如下:首先,定义 Entity 实体:@Data@Entitypublic class Person { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private Date birthAt;}实体类上添加 @Entity 注解标记为实体;@Id 标记该属性为标识;@GeneratedValue(strategy = GenerationType.IDENTITY) 说明使用数据库自增主键生成方式。然后,定义 PersonRepository :public interface PersonRepository extends JpaRepository<Person, Long> {}PersonRepository 继承于 JpaRepository,具体的实现类会在运行时由 Spring Data Jpa 自动创建,我们只需直接使用即可。@Servicepublic class PersonApplication { @Autowired private PersonRepository personRepository; public Long save(Person person){ this.personRepository.save(person); return person.getId(); }}在成功调用 save(person) 后,JPA 框架负责将数据库生成的 ID 绑定到 Person 的 id 属性上,person.getId() 方法便能获取 id 信息。性能可能是这种方法的一个缺点。2.1.4 使用另一个限界上下文提供的唯一标识通过集成上下文,可以从另一个限界上下文中获取唯一标识。但一般不会直接使用其他限界上下文的标识,而是需要将其翻译成本地限界上下文的概念。这也是比较常见的一种策略。例如,在用户成功注册后,系统自动为其生成唯一名片,此时,名片唯一标识便可以直接使用用户 ID。当用户注册成功后,User 限界上下文将发布 UserRegisteredEvent 事件。@Valuepublic class UserRegisteredEvent { private final UserId userId; private final String userName; private final Date birthAt;}Card 限界上下文,从 MQ 中获取 UserRegisteredEvent 事件,并将 UserId 翻译成本地的 CardId,然后基于 CardId 进行业务处理。具体如下:@Componentpublic class UserEventHandler { @EventListener public void handle(UserRegisteredEvent event){ UserId userId = event.getUserId(); CardId cardId = new CardId(userId.getValue()); … }}2.1.5 唯一标识生成时间实体唯一标识的生成既可以发生在对象创建的时候,也可以发生在持久化对象的时候。标识生成时间:及早标识 生成和赋值发生在持久化实体之前。延迟标识 生成和赋值发生在持久化实体的时候。在某些情况下,将标识生成延迟到实例持久化会有些问题:事件创建时,需要知道持久化实体的 ID。如果将实体放入 Set 中,会因为没有 ID,从而导致逻辑错误。相比之下,及早生成实体标识是比较推荐的做法。2.1.6 委派标识有些 ORM 框架,需要通过自己的方式来处理对象标识。为了解决这个问题,我们需要使用两种标识,一种为领域使用,一种为 ORM 使用。这个在 ORM 使用的标识,我们称为委派标识。委派标识和领域中的实体标识没有任何关系,委派标识只是为了迎合 ORM 而创建的。对于外界来说,我们最好将委派标识隐藏起来,因为委派标识并不是领域模型的一部分,将委派标识暴露给外界可能造成持久化漏洞。首先,我们需要定义一个公共父类 IdentitiedObject,用于对委派标识进行集中管理。@MappedSuperclasspublic class IdentitiedObject { @Setter(AccessLevel.PRIVATE) @Getter(AccessLevel.PRIVATE) @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long _id;}委派标识的 setter 和 getter 都是 private 级别,禁止程序对其进行修改(JPA 框架通过反射对其进行访问)。然后,定义 IdentitiedPerson 实体类:@Data@Entitypublic class IdentitiedPerson extends IdentitiedObject{ @Setter(AccessLevel.PRIVATE) private PersonId id; private String name; private Date birthAt; private IdentitiedPerson(){ } public IdentitiedPerson(PersonId id){ setId(id); }}IdentitiedPerson 实体以 PersonId 作为自己的业务标识,并且只能通过构造函数对其进行赋值。这样在隐藏委派标识的同时,完成了业务建模。领域标识不需要作为数据库的主键,但大多数情况下,需要设置为唯一键。2.1.7 本地标识和全局标识在聚合边界内,我们可以将缩短后的标识作为实体的本地标识。而作为聚合根的实体需要全局的唯一标识。聚合内部实体,只能通过聚合根进行间接访问。因此,只需保障在聚合内部具有唯一性即可。例如,聚合根 Order 拥有一个 OrderItem 的集合,对于 OrderItem 的访问必须通过 Order 聚合根,因此,OrderItem 只需保障局部唯一即可。@Valuepublic class OrderItemId { private Integer value;}@Data@Entitypublic class OrderItem extends IdentitiedObject{ @Setter(AccessLevel.PRIVATE) private OrderItemId id; private String productName; private Integer price; private Integer count; private OrderItem(){ } public OrderItem(OrderItemId id, String productName, Integer price, Integer count){ setId(id); setProductName(productName); setPrice(price); setCount(count); }}OrderItemId 为 Integer 类型,由 Order 完成其分配。@Entitypublic class Order extends IdentitiedObject{ @Setter(AccessLevel.PRIVATE) private OrderId id; @OneToMany private List<OrderItem> items = Lists.newArrayList(); public void addItem(String productName, Integer price, Integer count){ OrderItemId itemId = createItemId(); OrderItem item = new OrderItem(itemId, productName, price, count); this.items.add(item); } private OrderItemId createItemId() { Integer maxId = items.stream() .mapToInt(item->item.getId().getValue()) .max() .orElse(0); return new OrderItemId(maxId + 1); }}createItemId 方法获取现有 OrderItem 集合中最大的 id,并通过自增的方式,生成新的 id,从而保证在 Order 范围内的唯一性。相反,聚合根 Order 需要进行全局访问,因此,OrderId 需要全局唯一。@Valuepublic class OrderId { private final String day; private final String owner; private final Long number; public OrderId(String day, String owner, Long number) { this.day = day; this.owner = owner; this.number = number; } public String getValue(){ return String.format("%s-%s-%s”, getDay(), getOwner(), getNumber()); } @Override public String toString(){ return getValue(); }}2.2 实体行为实体专注于身份和连续性,如果将过多的职责添加到实体上,容易使实体变的臃肿。通常需要将相关行为委托给值对象和领域服务。2.2.1 将行为推入值对象值对象可合并、可比较和自验证,并方便测试。这些特征使其非常适用于承接实体的行为。在一个分期付款的场景中,我们需要将总金额按照分期次数进行拆分,如果发生不能整除的情况,将剩下的金额合并到最后一笔中。@Entity@Datapublic class Loan { private Money total; public List<Money> split(int size){ return this.total.split(size); }}其中,核心的查分逻辑在值对象 Money 中。public class Money implements ValueObject { public static final String DEFAULT_FEE_TYPE = “CNY”; @Column(name = “total_fee”) private Long totalFee; @Column(name = “fee_type”) private String feeType; private static final BigDecimal NUM_100 = new BigDecimal(100); private Money() { } private Money(Long totalFee, String feeType) { Preconditions.checkArgument(totalFee != null); Preconditions.checkArgument(StringUtils.isNotEmpty(feeType)); Preconditions.checkArgument(totalFee.longValue() > 0); this.totalFee = totalFee; this.feeType = feeType; } public static Money apply(Long totalFee){ return apply(totalFee, DEFAULT_FEE_TYPE); } public static Money apply(Long totalFee, String feeType){ return new Money(totalFee, feeType); } private void checkInput(Money money) { if (money == null){ throw new IllegalArgumentException(“input money can not be null”); } if (!this.getFeeType().equals(money.getFeeType())){ throw new IllegalArgumentException(“must be same fee type”); } } public List<Money> split(int count){ if (getTotalFee() < count){ throw new IllegalArgumentException(“total fee can not lt count”); } List<Money> result = Lists.newArrayList(); Long pre = getTotalFee() / count; for (int i=0; i< count; i++){ if (i == count-1){ Long fee = getTotalFee() - (pre * (count - 1)); result.add(Money.apply(fee, getFeeType())); }else { result.add(Money.apply(pre, getFeeType())); } } return result; }}可见,通过将功能推到值对象,不仅避免了实体 Loan 的臃肿,而且通过值对象 Money 的封装,大大增加了重用性。2.2.2 将行为推入领域服务领域服务没有标识、没有状态,对逻辑进行封装。非常适合承接实体的行为。我们看一个秘密加密需求:@Entity@Datapublic class User { private String password; public boolean checkPassword(PasswordEncoder encoder, String pwd){ return encoder.matches(pwd, password); } public void changePassword(PasswordEncoder encoder, String pwd){ setPassword(encoder.encode(pwd)); }}其中 PasswordEncoder 为领域服务public interface PasswordEncoder { /* * 秘密编码 / String encode(CharSequence rawPassword); /* * 验证密码有效性 * @return true if the raw password, after encoding, matches the encoded password from * storage / boolean matches(CharSequence rawPassword, String encodedPassword);}通过将密码加密和验证逻辑推到领域服务,不仅降低了实体 User 的臃肿,还可以使用策略模式对加密算法进行灵活替换。2.2.3 重视行为命名实体是业务操作的承载者,行为命名代表着很强的领域概念,需要使用通用语言中的动词,应极力避免 setter 方式的命名规则。假设,一个新闻存在 上线 和 下线 两个状态。public enum NewsStatus { ONLINE, // 上线 OFFLINE; // 下线}假如直接使用 setter 方法,上线和下线两个业务概念很难表达出来,从而导致概念的丢失。@Entity@Datapublic class News { @Setter(AccessLevel.PRIVATE) private NewsStatus status; /* * 直接的 setter 无法表达业务含义 * @param status / public void setStatus(NewsStatus status){ this.status = status; }}setStatus 体现的是数据操作,而非业务概念。此时,我们需要使用具有业务含义的方法命名替代 setter 方法。@Entity@Datapublic class News { @Setter(AccessLevel.PRIVATE) private NewsStatus status; public void online(){ setStatus(NewsStatus.ONLINE); } public void offline(){ setStatus(NewsStatus.OFFLINE); }}与 setStatus 不同,online 和 offline 具有明确的业务含义。2.2.4 发布领域事件在实体行为成功执行之后,常常需要将变更通知给其他模块或系统,以触发后续流程。因此,需要向外发布领域事件。发布领域事件,最大的问题是,在实体中如何获取发布事件接口 DomainEventPublisher 。常见的有以下几种模式:作为业务方法的参数进行传递。通过 ThreadLocal 与线程绑定。将事件暂存在实体中,在持久化完成后,获取并发布。首先,我们需要定义事件相关接口。DomainEvent:定义领域事件。public interface DomainEvent<ID, E extends Entity<ID>> { E getSource(); default String getType() { return this.getClass().getSimpleName(); }}DomainEventPublisher:用于发布领域事件。public interface DomainEventPublisher { <ID, EVENT extends DomainEvent> void publish(EVENT event); default <ID, EVENT extends DomainEvent> void publishAll(List<EVENT> events) { events.forEach(this::publish); }}DomainEventSubscriber: 事件订阅器,用于筛选待处理事件。public interface DomainEventSubscriber<E extends DomainEvent> { boolean accept(E e);}DomainEventHandler: 用于处理领域事件。public interface DomainEventHandler<E extends DomainEvent> { void handle(E event);}DomainEventHandlerRegistry : 对 DomainEventSubscriber 和 DomainEventHandler 注册。public interface DomainEventHandlerRegistry { default <E extends DomainEvent>void register(DomainEventSubscriber<E> subscriber, DomainEventHandler<E> handler){ register(subscriber, new DomainEventExecutor.SyncExecutor(), handler); } default <E extends DomainEvent>void register(Class<E> eventCls, DomainEventHandler<E> handler){ register(event -> event.getClass() == eventCls, new DomainEventExecutor.SyncExecutor(), handler); } default <E extends DomainEvent>void register(Class<E> eventCls, DomainEventExecutor executor, DomainEventHandler<E> handler){ register(event -> event.getClass() == eventCls, executor, handler); } <E extends DomainEvent>void register(DomainEventSubscriber<E> subscriber, DomainEventExecutor executor, DomainEventHandler<E> handler); <E extends DomainEvent> void unregister(DomainEventSubscriber<E> subscriber); <E extends DomainEvent> void unregisterAll(DomainEventHandler<E> handler);}DomainEventBus: 继承自 DomainEventPublisher 和 DomainEventHandlerRegistry, 提供事件发布和订阅功能。public interface DomainEventBus extends DomainEventPublisher, DomainEventHandlerRegistry{}DomainEventExecutor: 事件执行器,指定事件执行策略。public interface DomainEventExecutor { Logger LOGGER = LoggerFactory.getLogger(DomainEventExecutor.class); default <E extends DomainEvent> void submit(DomainEventHandler<E> handler, E event){ submit(new Task<>(handler, event)); } <E extends DomainEvent> void submit(Task<E> task); @Value class Task<E extends DomainEvent> implements Runnable{ private final DomainEventHandler<E> handler; private final E event; @Override public void run() { try { this.handler.handle(this.event); }catch (Exception e){ LOGGER.error(“failed to handle event {} use {}”, this.event, this.handler, e); } } } class SyncExecutor implements DomainEventExecutor{ @Override public <E extends DomainEvent> void submit(Task<E> task) { task.run(); } }}作为业务方法的参数进行传递 是最简单的策略,具体如下:public class Account extends JpaAggregate { public void enable(DomainEventPublisher publisher){ AccountEnabledEvent event = new AccountEnabledEvent(this); publisher.publish(event); }}这种实现方案虽然简单,但是很琐碎,每次都需要传递 DomainEventPublisher 参数,无形中提高了调用方的复杂性。通过 ThreadLocal 与线程绑定 将 EventPublisher 绑定到线程上下文中,在使用时,直接通过静态方法获取并进行事件发布。public class Account extends JpaAggregate { public void enable(){ AccountEnabledEvent event = new AccountEnabledEvent(this); DomainEventPublisherHolder.getPubliser().publish(event); }}DomainEventPublisherHolder 实现如下:public class DomainEventPublisherHolder { private static final ThreadLocal<DomainEventBus> THREAD_LOCAL = new ThreadLocal<DomainEventBus>(){ @Override protected DomainEventBus initialValue() { return new DefaultDomainEventBus(); } }; public static DomainEventPublisher getPubliser(){ return THREAD_LOCAL.get(); } public static DomainEventHandlerRegistry getHandlerRegistry(){ return THREAD_LOCAL.get(); }}将事件暂存在实体 是比较推荐的方法,具有很大的灵活性。public class Account extends JpaAggregate { public void enable(){ AccountEnabledEvent event = new AccountEnabledEvent(this); registerEvent(event); }}registerEvent 方法在 AbstractAggregate 类中,将 Event 暂存到 events 中。@MappedSuperclasspublic abstract class AbstractAggregate<ID> extends AbstractEntity<ID> implements Aggregate<ID> { private static final Logger LOGGER = LoggerFactory.getLogger(AbstractAggregate.class); @JsonIgnore @QueryTransient @Transient @org.springframework.data.annotation.Transient private final transient List<DomainEventItem> events = Lists.newArrayList(); protected void registerEvent(DomainEvent event) { events.add(new DomainEventItem(event)); } protected void registerEvent(Supplier<DomainEvent> eventSupplier) { this.events.add(new DomainEventItem(eventSupplier)); } @Override @JsonIgnore public List<DomainEvent> getEvents() { return Collections.unmodifiableList(events.stream() .map(eventSupplier -> eventSupplier.getEvent()) .collect(Collectors.toList())); } @Override public void cleanEvents() { events.clear(); } private class DomainEventItem { DomainEventItem(DomainEvent event) { Preconditions.checkArgument(event != null); this.domainEvent = event; } DomainEventItem(Supplier<DomainEvent> supplier) { Preconditions.checkArgument(supplier != null); this.domainEventSupplier = supplier; } private DomainEvent domainEvent; private Supplier<DomainEvent> domainEventSupplier; public DomainEvent getEvent() { if (domainEvent != null) { return domainEvent; } DomainEvent event = this.domainEventSupplier != null ? this.domainEventSupplier.get() : null; domainEvent = event; return domainEvent; } }}完成暂存后,在成功持久化后,进行事件发布。// 持久化实体this.aggregateRepository.save(a);if (this.eventPublisher != null){ // 对实体中保存的事件进行发布 this.eventPublisher.publishAll(a.getEvents()); // 清理事件 a.cleanEvents();}2.2.5 变化跟踪跟踪变化最实用的方法是领域事件和事件存储。当命令操作执行完成后,系统发出领域事件。事件的订阅者可以接收发生在模型上的事件,在接收事件后,订阅方将事件保存在事件存储中。变化跟踪,通常与事件存储一并使用,稍后详解。2.3 实体验证除了身份标识外,使用实体的一个重要需求是保证他们是自验证,并总是有效的。尽管实体具有生命周期,其状态不断变化,我们需要保证在整个变化过程中,实体总是有效的。验证的主要目的在于检查实体的正确性,检查对象可以是某个属性,也可以是整个对象,甚至是多个对象的组合。即便领域对象的各个属性都是合法的,也不能表示该对象作为一个整体是合法的;同样,单个对象合法也并不能保证对象组合是合法的。2.3.1 属性合法性验证可以使用自封装来验证属性。自封装性要求无论以哪种方式访问数据,即使从对象内部访问数据,都必须通过 getter 和 setter 方法。一般情况下,我们可以在 setter 方法中,对属性进行合法性验证,比如是否为空、字符长度是否符合要求、邮箱格式是否正确等。@Entitypublic class Person extends JpaAggregate { private String name; private Date birthDay; public Person(){ } public Person(String name, Date birthDay) { setName(name); setBirthDay(birthDay); } public String getName() { return name; } public void setName(String name) { // 对输入参数进行验证 Preconditions.checkArgument(StringUtils.isNotEmpty(name)); this.name = name; } public Date getBirthDay() { return birthDay; } public void setBirthDay(Date birthDay) { // 对输入参数进行验证 Preconditions.checkArgument(birthDay != null); this.birthDay = birthDay; }}在构造函数中,我也仍需调用 setter 方法完成属性赋值。2.3.2 验证整个对象要验证整个实体,我们需要访问整个对象的状态—-所有对象属性。验证整个对象,主要用于保证实体满足不变性条件。不变条件来源于明确的业务规则,往往需要获取对象的整个状态以完成验证。延迟验证 就是一种到最后一刻才进行验证的方法。验证过程应该收集所有的验证结果,而不是在一开始遇到非法状态就抛出异常。当发现非法状态时,验证类将通知客户方或记录下验证结果以便稍后使用。@Entitypublic class Person extends JpaAggregate { private String name; private Date birthDay; @Override public void validate(ValidationHandler handler){ if (StringUtils.isEmpty(getName())){ handler.handleError(“Name can not be empty”); } if (getBirthDay() == null){ handler.handleError(“BirthDay can not be null”); } }}其中 ValidationHandler 用于收集所有的验证信息。public interface ValidationHandler { void handleError(String msg);}有时候,验证逻辑比领域对象本身的变化还快,将验证逻辑嵌入在领域对象中会使领域对象承担太多的职责。此时,我们可以创建一个单独的组件来完成模型验证。在 Java 中设计单独的验证类时,我们可以将该类放在和实体同样的包中,将属性的 getter 方法生命为包级别,这样验证类便能访问这些属性了。假如,我们不想将验证逻辑全部放在 Person 实体中。可以新建 PersonValidator:public class PersonValidator implements Validator { private final Person person; public PersonValidator(Person person) { this.person = person; } @Override public void validate(ValidationHandler handler) { if (StringUtils.isEmpty(this.person.getName())){ handler.handleError(“Name can not be empty”); } if (this.person.getBirthDay() == null){ handler.handleError(“BirthDay can not be null”); } }}然后,在 Person 中调用 PersonValidator:@Entitypublic class Person extends JpaAggregate { private String name; private Date birthDay; @Override public void validate(ValidationHandler handler){ new PersonValidator(this).validate(handler); }}这样将最大限度的避免 Person 的臃肿。2.3.3 验证对象组合相比之下,验证对象组合会复杂很多,也比较少见。最常用的方式是把这种验证过程创建成一个领域服务。领域服务,我们稍后详解。2.4 关注行为,而非数据实体应该面向行为,这意味着实体应该公开领域行为,而不是公开状态。专注于实体行为非常重要,它使得领域模型更具表现力。通过对象的封装特性,其状态只能被封装它的实例进行操作,这意味着任何修改状态的行为都属于实体。专注于实体行为,需要谨慎公开 setter 和 getter 方法。特别是 setter 方法,一旦公开将使状态更改直接暴露给用户,从而绕过领域概念直接对状态进行更新。典型的还是 News 上下线案例。@Entity@Datapublic class News { @Setter(AccessLevel.PRIVATE) private NewsStatus status; public void online(){ setStatus(NewsStatus.ONLINE); } public void offline(){ setStatus(NewsStatus.OFFLINE); } /* * 直接的 setter 无法表达业务含义 * @param status / private void setStatus(NewsStatus status){ this.status = status; }}2.5 实体创建当我们新建一个实体时,希望通过构造函数来初始化足够多的状态。这样,一方面有助于表明该实体的身份,另一方面可以帮助客户端更容易的查找该实体。2.5.1 构造函数如果实体的不变条件要求该实体所包含的对象不能为 null,或者由其他状态计算所得,那么这些状态需要作为参数传递给构造函数。构造函数对实体变量赋值时,它把操作委派给实例变量的 setter 方法,这样便保证了实体变量的自封装性。见 Person 实例,将无参构造函数设为 private,以服务于框架;通过 public 暴露所有参数的构造函数,并调用 setter 方法对实体有效性进行验证。@Entitypublic class Person extends JpaAggregate { private String name; private Date birthDay; private Person(){ } public Person(String name, Date birthDay) { setName(name); setBirthDay(birthDay); } public String getName() { return name; } public void setName(String name) { // 对输入参数进行验证 Preconditions.checkArgument(StringUtils.isNotEmpty(name)); this.name = name; } public Date getBirthDay() { return birthDay; } public void setBirthDay(Date birthDay) { // 对输入参数进行验证 Preconditions.checkArgument(birthDay != null); this.birthDay = birthDay; }}2.5.2 静态方法对于使用一个实体承载多个类型的场景,我们可以使用实体上的静态方法,对不同类型进行不同构建。@Setter(AccessLevel.PRIVATE)@Entitypublic class BaseUser extends JpaAggregate { private UserType type; private String name; private BaseUser(){ } public static BaseUser createTeacher(String name){ BaseUser baseUser = new BaseUser(); baseUser.setType(UserType.TEACHER); baseUser.setName(name); return baseUser; } public static BaseUser createStudent(String name){ BaseUser baseUser = new BaseUser(); baseUser.setType(UserType.STUDENT); baseUser.setName(name); return baseUser; }}相对,构造函数,静态方法 createTeacher 和 createStudent 具有更多的业务含义。2.5.3 工厂对于那些非常复杂的创建实体的情况,我们可以使用工厂模式。这个不仅限于实体,对于复杂的实体、值对象、聚合都可应用工厂。并且,此处所说的工厂,也不仅限于工厂模式,也可以使用 Builder 模式。总之,就是将复杂对象的创建与对象本身功能进行分离,从而完成对象的瘦身。2.6 分布式设计分布式系已经成为新的标准,我们需要在新标准下,思考对领域设计的影响。2.6.1 不要分布单个实体强烈建议不要分布单个实体。在本质上,这意味着一个实体应该被限制成单个有界上下文内部的单个领域模型中的单个类(或一组类)。假如,我们将单实体的不同部分分布在一个分布式系统之上。为了实现实体的一致性,可能需要全局事务保障,大大增加了系统的复杂度。要加载这个实体的话,查询多个不同系统也是一种必然。分布式系统中的网络开销将会放大,从而导致严重的性能问题。上图,将 OrderItem 和 ProductInfo 与 Order 进行分布式部署,在获取 Oder 时会导致大量的 RPC 调用,降低系统性能。正确的部分方案为:2.6.2 可以分布多个实体对于多个实体间,进行分布式部署,可以将压力进行分散,大大增加系统性能。这种部署方式是推荐方式。3 实体建模模式建模模式有利于提升实体的表达性和可维护性。3.1 妥善处理唯一标识唯一标识是实体的身份,在完成分配后,绝对不允许修改。对于程序生成:@Datapublic class Book { private ISBN id; private Book(){ } public Book(ISBN isbn){ this.setId(isbn); } public ISBN getId(){ return this.id; } private void setId(ISBN id){ Preconditions.checkArgument(id != null); this.id = id; }}由构造函数传入 id,并将 setter 方法设置为私有,以避免被改变。对于持久化生成:@Data@MappedSuperclasspublic abstract class JpaAggregate extends AbstractAggregate<Long> { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Setter(AccessLevel.PRIVATE) @Column(name = “id”) private Long id; @Override public Long getId() { return id; }}使用 private 属性和 setter 方法,避免被修改。同时提供 public 的 getter 方法,用于获取生成的 id。3.2 使用 Specification 进行规格建模Specification 也称规格模式,主要针对领域模型中的描述规格进行建模。规范模式是一种软件设计模式,可用于封装定义所需对象状态的业务规则。这是一种非常强大的方法,可以减少耦合并提高可扩展性,以选择与特定条件匹配的对象子集。这些规格可以使用逻辑运算符组合,从而形成复合规范。规格 Specification 模式是将一段领域知识封装到一个单元中,称为规格。然后,在不同的场景中重用。主要有三种这样的场景:数据检索 是从存储中获取数据,查找与规范匹配的记录。内存中验证 是指检查某个对象是否符合规格描述。创建新对象的场景非常罕见,我们暂且忽略。这很有用,因为它允许你避免域知识重复。当向用户显示数据时,相同的规格类可用于验证传入数据和从数据库中过滤数据。在了解完 Specification 的特征 后,我们需要一个框架,它提供了 Specification 相关 API,既能从存储中检索数据,也能对内存对象进行验证。在这,我们使用 Querydsl 进行构建。一个 News 实体,存在两种状态,一个是用户自己设置的 NewsStatus,用于标记当前是上线还是下线状态;一个是管理员设置的 NewsAuditStatus,用于标记当前是审核通过还是审核拒绝状态。只有在用户设置为上线同时管理员审核通过,该 News 才可显示。首先,我们先定义规则。public class NewsPredicates { /* * 获取可显示规则 * @return / public static PredicateWrapper<News> display(){ return new Display(); } /* * 可显示规则 / static class Display extends AbstractPredicateWrapper<News>{ protected Display() { super(QNews.news); } @Override public Predicate getPredicate() { Predicate online = QNews.news.status.eq(NewsStatus.ONLINE); Predicate passed = QNews.news.auditStatus.eq(NewsAuditStatus.PAASED); return new BooleanBuilder() .and(online) .and(passed); } }}该规则可以应用于内存对象。@Entity@Data@QueryEntitypublic class News { @Setter(AccessLevel.PRIVATE) private NewsAuditStatus auditStatus; @Setter(AccessLevel.PRIVATE) private NewsStatus status; /* * 判断是否是可显示的 * @return / public boolean isDisplay(){ return NewsPredicates.display().accept(this); }}同时,该规则也可以用于数据检索。public interface NewsRepository extends Repository<News, Long>, QuerydslPredicateExecutor<News> { /* * 查找可显示的信息 * @param pageable * @return */ default Page<News> getDispaly(Pageable pageable){ return findAll(NewsPredicates.display().getPredicate(), pageable); }}可显示规则全部封装于 NewsPredicates 中,如果规则发生变化,只需对 NewsPredicates 进行调整即可。3.3 使用 Enum 简化状态模式实体拥有自己的生命周期,往往会涉及状态管理。对状态建模是实体建模的重要部分。管理实体状态,状态设计模式具有很大的诱惑。比如一个简单的审核流程。graph TB已提交–通过–>审核通过已提交–修改–>已提交已提交–拒绝–>审核拒绝审核拒绝–修改–>已提交使用状态模式如下:首先,定义状态接口。public interface AuditStatus { AuditStatus pass(); AuditStatus reject(); AuditStatus edit();}该接口中包含所有操作。然后,定义异常类。public class StatusNotSupportedException extends RuntimeException{}在当前状态不允许执行某些操作时,直接抛出异常,以中断流程。然后,定义各个状态类,如下:SubmittedStatuspublic class SubmittedStatus implements AuditStatus{ @Override public AuditStatus pass() { return new PassedStatus(); } @Override public AuditStatus reject() { return new RejectedStatus(); } @Override public AuditStatus edit() { return new SubmittedStatus(); }}PassedStatuspublic class PassedStatus implements AuditStatus{ @Override public AuditStatus pass() { throw new StatusNotSupportedException(); } @Override public AuditStatus reject() { throw new StatusNotSupportedException(); } @Override public AuditStatus edit() { throw new StatusNotSupportedException(); }}RejectedStatuspublic class RejectedStatus implements AuditStatus{ @Override public AuditStatus pass() { throw new StatusNotSupportedException(); } @Override public AuditStatus reject() { throw new StatusNotSupportedException(); } @Override public AuditStatus edit() { return new SubmittedStatus(); }}但,状态模式导致大量的模板代码,对于简单业务场景显得有些冗余。同时太多的状态类为持久化造成了不少麻烦。此时,我们可以使用 Enum 对其进行简化。public enum AuditStatusEnum { SUBMITED(){ @Override public AuditStatusEnum pass() { return PASSED; } @Override public AuditStatusEnum reject() { return REJECTED; } @Override public AuditStatusEnum edit() { return SUBMITED; } }, PASSED(){ }, REJECTED(){ @Override public AuditStatusEnum edit() { return SUBMITED; } }; public AuditStatusEnum pass(){ throw new StatusNotSupportedException(); } public AuditStatusEnum reject(){ throw new StatusNotSupportedException(); } public AuditStatusEnum edit(){ throw new StatusNotSupportedException(); }}AuditStatusEnum 与 之前的状态模式功能完全一致,但代码要紧凑的多。另外,使用显示建模也是一种解决方案。这种方式会为每个状态创建一个类,通过类型检测机制严格控制能操作的方法,但对于存储有些不大友好,在实际开发中,使用的不多。3.4 使用业务方法和 DTO 替换 setter之前提过,实体不应该绕过业务方法,直接使用 setter 对状态进行修改。如果业务方法拥有过长的参数列表,在使用上也会导致一定的混淆。最常见策略是,使用 DTO 对业务所需数据进行传递,并在业务方法中调用 getter 方法获取对于数据。@Entity@Datapublic class User { private String name; private String nickName; private Email email; private Mobile mobile; private Date birthDay; private String password; public boolean checkPassword(PasswordEncoder encoder, String pwd){ return encoder.matches(pwd, password); } public void changePassword(PasswordEncoder encoder, String pwd){ setPassword(encoder.encode(pwd)); } public void update(String name, String nickName, Email email, Mobile mobile, Date birthDay){ setName(name); setNickName(nickName); setEmail(email); setMobile(mobile); setBirthDay(birthDay); } public void update(UserDto userDto){ setName(userDto.getName()); setNickName(userDto.getNickName()); setEmail(userDto.getEmail()); setMobile(userDto.getMobile()); setBirthDay(userDto.getBirthDay()); }}3.5 使用备忘录或 DTO 处理数据显示实体存储的数据,往往需要读取出来,在 UI 中显示,或被其他系统使用。实体作为领域概念,不允许脱离领域层,而在 UI 中直接使用。此时,我们需要使用备忘录或 DTO 模式,将实体与数据解耦。3.6 避免副作用方法方法的副作用,是指一个方法的执行,如果在返回一个值之外还导致某些“状态”发生变化,则称该方法产生了副作用。根据副作用概念,我们可以提取出两类方法:Query 方法 有返回值,但不改变内部状态。Command 方法 没有返回值,但会改变内部状态。在实际开发中,需要对两者进行严格区分。在 Application 中,Command 方法需要开启写事务;Query 方法只需开启读事务即可。@Servicepublic class NewsApplication extends AbstractApplication { @Autowired private NewsRepository repository; @Transactional(readOnly = false) public Long createNews(String title, String content){ return creatorFor(this.repository) .instance(()-> News.create(title, content)) .call() .getId(); } @Transactional(readOnly = false) public void online(Long id){ updaterFor(this.repository) .id(id) .update(News::online) .call(); } @Transactional(readOnly = false) public void offline(Long id){ updaterFor(this.repository) .id(id) .update(News::offline) .call(); } @Transactional(readOnly = false) public void reject(Long id){ updaterFor(this.repository) .id(id) .update(News::reject) .call(); } @Transactional(readOnly = false) public void pass(Long id){ updaterFor(this.repository) .id(id) .update(News::pass) .call(); } @Transactional(readOnly = true) public Optional<News> getById(Long id){ return this.repository.getById(id); } @Transactional(readOnly = true) public Page<News> getDisplay(Pageable pageable){ return this.repository.getDispaly(pageable); }}其中,有一个比较特殊的方法,创建方法,由于采用的是数据库生成主键策略,需要将生成的主键返回。3.7 使用乐观锁进行并发控制实体主要职责是维护业务不变性,当多个用户同时修改一个实体时,会将事情复杂化,从而导致业务规则的破坏。对此,需要在实体上使用乐观锁进行并发控制,保障只有一个用户更新成功,从而保护业务不变性。Jpa 框架自身便提供了对乐观锁的支持,只需添加 @Version 字段即可。@Getter(AccessLevel.PUBLIC)@MappedSuperclasspublic abstract class AbstractEntity<ID> implements Entity<ID> { private static final Logger LOGGER = LoggerFactory.getLogger(AbstractEntity.class); @Version @Setter(AccessLevel.PRIVATE) @Column(name = “version”, nullable = false) private int version;}4 总结实体是问题域中具有唯一身份的领域概念。与值对象不同,实体的相等性严格基于唯一标识。实体具有明确的生命周期。在实体生命周期中,需要严格遵从业务不变性条件。应该将实体定位为值对象的容器,把行为推到值对象和领域服务中,从而避免实体的臃肿。实体可以提供属性、对象、对象组等多种验证规则,从而保护业务。实体的唯一标识,可以来自领域概念、程序生成、存储生成等。规格模式是处理实体规则描述的一大利器。乐观锁的使用,将大大减少并发导致的业务错误。 ...

April 1, 2019 · 13 min · jiezi

领域驱动设计战术模式---值对象

值对象虽然经常被掩盖在实体的阴影之下,但它却是非常重要的 DDD 概念。值对象不具有身份,它纯粹用于描述实体的特性。处理不具有身份的值对象是很容易的,尤其是不变性与可组合性是支持易用性的两个特征。1 理解值对象值对象用于度量和描述事物,我们可以非常容易的对值对象进行创建、测试、使用、优化和维护。一个值对象,或者更简单的说,值,是对一个不变的概念整体建立的模型。在这个模型中,值就真的只有一个值。和实体不一样,他没有唯一标识,而是通过封装属性的对比来决定相等性。一个值对象不是事物,而是用来描述、量化或测量实体的。当你关系某个对象的属性时,该对象便是一个值对象。为其添加有意义的属性,并赋予相应的行为。我们需要将值对象看成不变对象,不要给他任何身份标识,还应该尽量避免像实体对象一样的复杂性。即使一个领域概念必须建模成实体,在设计时也应该更偏向于将其作为值对象的容器。当决定一个领域概念是否应该建模成值对象时,需要考虑是否拥有一些特性:度量或描述领域中的一件东西。可以作为不变对象。将不同的相关属性组合成一个概念整体。当度量或描述改变时,可以使用另一个值对象予以替换。可以与其他值对象进行相等性比较。不对对协作对象造成负面影响。在使用这个特性分析模型时,你会发现很多领域概念都应该建模成值对象,而非实体。值对象的特征汇总如下:度量或描述。只是度量或描述领域中某件东西的一个概念。不变性。值对象在创建后,就不会发生改变,如果需要改变的话,将创建一个新的值对象并对原有对象进行替换。概念整体性。一个值对象可以只有一个属性,也可以拥有一组相关属性。如果一组属性联合起来并不能表达一个整体上的概念,那就没有什么意义。有效性。值对象的构造函数应该用于保障概念整体性的有效性。可替换性。如果需要改变的话,我们需要将整个值对象替换成一个新的值对象实例。属性相等性。通过比较两个对象的类型和属性来决定其相等性。方法无副作用。由于不变性,值对象的方法一般为一个无副作用函数,这个函数表示对某个对象的操作,它只用于产生输出,不会修改对象状态。2 何时使用值对象值对象是实体的状态,它描述与实体相关的概念。2.1 表示描述性的、缺失身份的概念当一个概念缺乏明显的身份时,基本可以断定它大概率是一个值对象。比较典型的例子便是 Money,大多数情况下,我们只关心它所代表的实际金额,为其分配标识是一个没有意义的操作。@Data@Setter(AccessLevel.PRIVATE)@Embeddablepublic class Money implements ValueObject { public static final String DEFAULT_FEE_TYPE = “CNY”; @Column(name = “total_fee”) private Long totalFee; @Column(name = “fee_type”) private String feeType; …}2.2 增强确定性领域驱动设计的一切都是为了明确传递业务规则和领域逻辑。像整数和字符串这样的技术单元并不适合这种情况。比如邮箱可以使用字符串进行描述,但会丢失很多邮箱的特性,此时,需要将其建模成值对象。@Embeddable@Data@Setter(AccessLevel.PRIVATE)public class Email implements ValueObject { @Column(name = “email_name”) private String name; @Column(name = “email_domain”) private String domain; private Email() { } private Email(String name, String domain) { Preconditions.checkArgument(StringUtils.isNotEmpty(name), “name can not be null”); Preconditions.checkArgument(StringUtils.isNotEmpty(domain), “domain can not be null”); this.setName(name); this.setDomain(domain); } public static Email apply(String email) { Preconditions.checkArgument(StringUtils.isNotEmpty(email), “email can not be null”); String[] ss = email.split("@"); Preconditions.checkArgument(ss.length == 2, “not Email”); return new Email(ss[0], ss[1]); } @Override public String toString() { return this.getName() + “@” + this.getDomain(); }}此时,邮箱是一个明确的领域概念,相比字符串方案,其拥有验证逻辑,同时享受编译器类型校验。3 实现值对象值对象是不可变的、无副作用并且易于测试的。3.1 欠缺身份缺失身份是值对象和实体最大的区别。由于值对象没有身份,且描述了领域中重要的概念,通常,我们会先定义实体,然后找出与实体相关的值对象。一般情况下,值对象需要实体提供上下文相关性。3.2 基于属性的相等性如果实体具有相同的类型和标识,则会认为是相等的。相反,值对象要具有相同的值才会认为是相等的。如果两个 Money 对象表示相等的金额,他们就被认为是相等的。而不管他们是指向同一个实例还是不同的实例。在 Money 类中使用 lombok 插件自动生成 hashCode 和 equals 方法,查看 Money.class 可以看到。//// Source code recreated from a .class file by IntelliJ IDEA// (powered by Fernflower decompiler)//public class Mobile implements ValueObject { public boolean equals(final Object o) { if (o == this) { return true; } else if (!(o instanceof Mobile)) { return false; } else { Mobile other = (Mobile)o; if (!other.canEqual(this)) { return false; } else { Object this$dcc = this.getDcc(); Object other$dcc = other.getDcc(); if (this$dcc == null) { if (other$dcc != null) { return false; } } else if (!this$dcc.equals(other$dcc)) { return false; } Object this$mobile = this.getMobile(); Object other$mobile = other.getMobile(); if (this$mobile == null) { if (other$mobile != null) { return false; } } else if (!this$mobile.equals(other$mobile)) { return false; } return true; } } } protected boolean canEqual(final Object other) { return other instanceof Mobile; } public int hashCode() { int PRIME = true; int result = 1; Object $dcc = this.getDcc(); int result = result * 59 + ($dcc == null ? 43 : $dcc.hashCode()); Object $mobile = this.getMobile(); result = result * 59 + ($mobile == null ? 43 : $mobile.hashCode()); return result; } public String toString() { return “Mobile(dcc=” + this.getDcc() + “, mobile=” + this.getMobile() + “)”; }}3.3 富含行为值对象应该尽可能多的暴露面向领域概念的行为。在 Money 值对象中,可以看到暴露的方法:方法含义apply创建 MoneyaddMoney 相加subtractMoney 相减multiplyMoney 相乘splitMoney 切分,将无法查分的误差汇总到最后的 Money 中@Data@Setter(AccessLevel.PRIVATE)@Embeddablepublic class Money implements ValueObject { public static final String DEFAULT_FEE_TYPE = “CNY”; @Column(name = “total_fee”) private Long totalFee; @Column(name = “fee_type”) private String feeType; private static final BigDecimal NUM_100 = new BigDecimal(100); private Money() { } private Money(Long totalFee, String feeType) { Preconditions.checkArgument(totalFee != null); Preconditions.checkArgument(StringUtils.isNotEmpty(feeType)); Preconditions.checkArgument(totalFee.longValue() > 0); this.totalFee = totalFee; this.feeType = feeType; } public static Money apply(Long totalFee){ return apply(totalFee, DEFAULT_FEE_TYPE); } public static Money apply(Long totalFee, String feeType){ return new Money(totalFee, feeType); } public Money add(Money money){ checkInput(money); return Money.apply(this.getTotalFee() + money.getTotalFee(), getFeeType()); } private void checkInput(Money money) { if (money == null){ throw new IllegalArgumentException(“input money can not be null”); } if (!this.getFeeType().equals(money.getFeeType())){ throw new IllegalArgumentException(“must be same fee type”); } } public Money subtract(Money money){ checkInput(money); if (getTotalFee() < money.getTotalFee()){ throw new IllegalArgumentException(“money can not be minus”); } return Money.apply(this.getTotalFee() - money.getTotalFee(), this.getFeeType()); } public Money multiply(int var){ return Money.apply(this.getTotalFee() * var, getFeeType()); } public List<Money> split(int count){ if (getTotalFee() < count){ throw new IllegalArgumentException(“total fee can not lt count”); } List<Money> result = Lists.newArrayList(); Long pre = getTotalFee() / count; for (int i=0; i< count; i++){ if (i == count-1){ Long fee = getTotalFee() - (pre * (count - 1)); result.add(Money.apply(fee, getFeeType())); }else { result.add(Money.apply(pre, getFeeType())); } } return result; }}3.4 内聚通常情况下,值对象会内聚封装度量值和度量单位。在 Money 中可以看到这一点。当然,并不局限于此,对于拥有概念整体性的对象,都具有很强的内聚性。比如,英文名称,由 firstName,lastName 组成。@Data@Setter(AccessLevel.PRIVATE)public class EnglishName{ private String firstName; private String lastName; private EnglishName(String firstName, String lastName){ Preconditions.checkArgument(StringUtils.isNotEmpty(firstName)); Preconditions.checkArgument(StringUtils.isNotEmpty(lastName)); setFirstName(firstName); setLastName(lastName); } public static EnglishName apply(String firstName, String lastName){ return new EnglishName(firstName, lastName); }}3.5 不变性一旦创建完成后,值对象就永远不能改变。如果需要改变值对象,应该创建新的值对象,并由新的值对象替换旧值对象。比如,Money 的 subtract 方法。public Money subtract(Money money){ checkInput(money); if (getTotalFee() < money.getTotalFee()){ throw new IllegalArgumentException(“money can not be minus”); } return Money.apply(this.getTotalFee() - money.getTotalFee(), this.getFeeType());}只会创建新的 Money 对象,不会对原有对象进行修改。在技术实现上,对于一个不可变对象,需要将所有字段设置为 final,并通过构造函数为其赋值。但,有时为了迎合一些框架需求,需求进行部分妥协,及将 setter 方法设置为 private,从而对外隐藏修改方法。3.6 可组合性对于用于度量的值对象,通常会有数值,此时,可以将其组合起来以创建新的值。比如 Money 的 add 方法,Money 加上 Money 会得到一个新的 Money。public Money add(Money money){ checkInput(money); return Money.apply(this.getTotalFee() + money.getTotalFee(), getFeeType());}3.7 自验证性值对象作为一个概念整体,决不应该变成无效状态,它自身就应该负责对其进行验证。通常情况下,在创建一个值对象实例时,如果参数与业务规则不一致,则构造函数应该抛出异常。还是看我们的 Money 类,需要进行如下检验:单位不能为 null;金额不能为 null;金额不能为负值。private Money(Long totalFee, String feeType) { Preconditions.checkArgument(totalFee != null); Preconditions.checkArgument(StringUtils.isNotEmpty(feeType)); Preconditions.checkArgument(totalFee.longValue() > 0); this.totalFee = totalFee; this.feeType = feeType;}当然,如果值对象的构建过程过于复杂,可以使用 Factory 模式进行构建。此时,应该在 Factory 中对值对象的有效性进行验证。3.8 可测试性不变性、内聚性和可组合性使值对象变的可测试。还是看我们的 Money 对象的测试类。public class MoneyTest { @Test public void add() { Money m1 = Money.apply(100L); Money m2 = Money.apply(200L); Money money = m1.add(m2); Assert.assertEquals(300L, money.getTotalFee().longValue()); Assert.assertEquals(m1.getFeeType(), money.getFeeType()); Assert.assertEquals(m2.getFeeType(), money.getFeeType()); } @Test public void subtract() { Money m1 = Money.apply(300L); Money m2 = Money.apply(200L); Money money = m1.subtract(m2); Assert.assertEquals(100L, money.getTotalFee().longValue()); Assert.assertEquals(m1.getFeeType(), money.getFeeType()); Assert.assertEquals(m2.getFeeType(), money.getFeeType()); } @Test public void multiply() { Money m1 = Money.apply(100L); Money money = m1.multiply(3); Assert.assertEquals(300L, money.getTotalFee().longValue()); Assert.assertEquals(m1.getFeeType(), money.getFeeType()); } @Test public void split() { Money m1 = Money.apply(100L); List<Money> monies = m1.split(33); Assert.assertEquals(33, monies.size()); monies.forEach(m -> Assert.assertEquals(m1.getFeeType(), m.getFeeType())); long total = monies.stream() .mapToLong(m->m.getTotalFee()) .sum(); Assert.assertEquals(100L, total); }}4 值对象建模模式通过一些常用的值对象建模模式,可以提高值对象的处理体验。4.1 静态工厂方法静态工厂方法是更简单、更具有表达性的一种技巧。比如 java 中的 Instant 的静态工厂方法。public static Instant now() { …}public static Instant ofEpochSecond(long epochSecond) { …}public static Instant ofEpochMilli(long epochMilli){ …}通过方法签名就能很清楚的了解其含义。4.2 微类型通过使用更具体的领域模型类型封装技术类型,使其更具表达能力。典型的就是 Mobile 封装,其本质是一个 String。通过 Mobile 封装,使其具有字符串无法表达的含义。@Setter(AccessLevel.PRIVATE)@Data@Embeddablepublic class Mobile implements ValueObject { public static final String DEFAULT_DCC = “0086”; @Column(name = “dcc”) private String dcc; @Column(name = “mobile”) private String mobile; private Mobile() { } private Mobile(String dcc, String mobile){ Preconditions.checkArgument(StringUtils.isNotEmpty(dcc)); Preconditions.checkArgument(StringUtils.isNotEmpty(mobile)); setDcc(dcc); setMobile(mobile); } public static Mobile apply(String mobile){ return apply(DEFAULT_DCC, mobile); } public static Mobile apply(String dcc, String mobile){ return new Mobile(dcc, mobile); }}4.3 避免集合通常情况下,需要尽量避免使用值对象集合。这种表达方式无法正确的表达领域概念。使用值对象集合通常意味着需要使用某种形式来取出特定项,这就相当于为值对象添加了身份。比如 List<Email> 第一个代表是主邮箱,第二个表示是副邮箱,最佳的表达方式是直接用属性进行表式,如:@Data@Setter(AccessLevel.PRIVATE)public class Person{ private Email primary; private Email second; public void updateEmail(Email primary, Email second){ Preconditions.checkArgument(primary != null); Preconditions.checkArgument(second != null); setPrimary(primary); setSecond(second); }}5 持久化处理值对象最难的点就在他们的持久化。一般情况下,不会直接对其进行持久化,值对象会作为实体的属性,一并进行持久化处理。持久化过程即将对象序列化成文本格式或二进制格式,然后保存到计算机磁盘中。在面向文档数据存储时,问题会少很多。我们可以在同一个文档中存储实体和值对象;然而,使用 SQL 数据库就麻烦的多,这将导致很多变化。5.1 NoSQL许多 NoSQL 数据库都使用了数据反规范化,为我们提供了很大便利。在 NoSQL 中,整个实体都可以作为一个文档来建模。在 SQL 中的表连接、规范化数据和 ORM 延迟加载等相关问题都不存在了。在值对象上下文中,这就意味着他们会与实体一起存储。@Data@Setter(AccessLevel.PRIVATE)@Documentpublic class PersonAsMongo { private Email primary; private Email second; public void updateEmail(Email primary, Email second){ Preconditions.checkArgument(primary != null); Preconditions.checkArgument(second != null); setPrimary(primary); setSecond(second); }}面向文档的 NoSQL 数据库会将文档持久化为 JSON,上例中 Person 的 primary 和 second 会作为 JSON 文档的属性进行存储。5.2 SQL在 SQL 数据库中存储值对象,可以遵循标准的 SQL 约定,也可以使用范模式。多数情况下,持久化值对象时,我们都是通过一种非范式的方式完成,即所有的属性和实体都保存在相同的数据库表中。有时,值对象需要以实体的身份进行持久化。比如聚合中维护一个值对象集合时。5.2.1 多列存储单个值对象基本思路就是将值对象与其所在的实体对象保存在同一张表中,值对象的每个属性保存为一列。这种方式,是最常见的值对象序列化方式,也是冲突最小的方式,可以在查询中使用连接语句进行查询。Jpa 提供 @Embeddable 和 @Embedded 两个注解,以支持这种方式。首先,在值对象上添加 @Embeddable 注解,以标注其为可嵌入对象。@Embeddable@Data@Setter(AccessLevel.PRIVATE)public class Email implements ValueObject { @Column(name = “email_name”) private String name; @Column(name = “email_domain”) private String domain; private Email() { } private Email(String name, String domain) { Preconditions.checkArgument(StringUtils.isNotEmpty(name), “name can not be null”); Preconditions.checkArgument(StringUtils.isNotEmpty(domain), “domain can not be null”); this.setName(name); this.setDomain(domain); } public static Email apply(String email) { Preconditions.checkArgument(StringUtils.isNotEmpty(email), “email can not be null”); String[] ss = email.split("@"); Preconditions.checkArgument(ss.length == 2, “not Email”); return new Email(ss[0], ss[1]); } @Override public String toString() { return this.getName() + “@” + this.getDomain(); }}然后,在实体对于属性上添加 @Embedded 注解,标注该属性将展开存储。@Data@Entitypublic class Person1 { @Embedded private Email primary;}5.2.2 单列存储单个值对象值对象的所有属性保存为一列。当不希望在查询中使用额外语句来连接他们时,这是一个很好的选择。一般情况下,会涉及以下几个操作:创建持久化格式。在保存时进行数据转换。在加载时解析值。如,对于 Email 值对象,我们采用 JSON 作为持久化格式:public class EmailSerializer { public static Email toEmail(String json){ if (StringUtils.isEmpty(json)){ return null; } return JSON.parseObject(json, Email.class); } public static String toJson(Email email){ if (email == null){ return null; } return JSON.toJSONString(email); }}JPA 中提供了 Converter 扩展,以完成值对象到数据、数据到值对象的转化:public class EmailConverter implements AttributeConverter<Email, String> { @Override public String convertToDatabaseColumn(Email attribute) { return EmailSerializer.toJson(attribute); } @Override public Email convertToEntityAttribute(String dbData) { return EmailSerializer.toEmail(dbData); }}Converter 完成后,需要将其配置在对应的属性上:@Data@Setter(AccessLevel.PRIVATE)public class PersonAsJpa { @Convert(converter = EmailConverter.class) private Email primary; @Convert(converter = EmailConverter.class) private Email second; public void updateEmail(Email primary, Email second){ Preconditions.checkArgument(primary != null); Preconditions.checkArgument(second != null); setPrimary(primary); setSecond(second); }}此时,就完成了单个值对象的持久化。5.2.3 多个值对象序列化到单个列中这种应用是前种方案的扩展。将整个集合序列化成某种形式的文本,然后将该文本保存到单个数据库列中。需要考虑的问题:列宽。数据库列的长度不好确定。不方便查询。由于值对象集合被序列化到扁平化文本中,值对象的属性不能使用 SQL 进行查询。需要自定义类型。持久化框架对该类型的映射没有提供支撑,需要对其进行扩展。如,对于 List<Email> 选择 JSON 作为持久化格式:public class EmailListSerializer { public static List<Email> toEmailList(String json){ if (StringUtils.isEmpty(json)){ return null; } return JSON.parseArray(json, Email.class); } public static String toJson(List<Email> email){ if (email == null){ return null; } return JSON.toJSONString(email); }}扩展 JPA 的 Converter:public class EmailListConverter implements AttributeConverter<List<Email>, String> { @Override public String convertToDatabaseColumn(List<Email> attribute) { return EmailListSerializer.toJson(attribute); } @Override public List<Email> convertToEntityAttribute(String dbData) { return EmailListSerializer.toEmailList(dbData); }}属性配置:@Data@Setter(AccessLevel.PRIVATE)public class PersonEmailListAsJpa { @Convert(converter = EmailListConverter.class) private List<Email> emails; }5.2.4 使用数据库实体保存多个值对象我们应该首先考虑将领域概念建模成值对象,而不是实体。我们可以使用委派主键的方式,使用两层的层超类型。在上层隐藏委派主键。这样我们可以自由的将其映射成数据库实体,同时在领域模型中将其建模成值对象。首先,定义 IdentitiedObject 用以隐藏数据库 ID。@MappedSuperclasspublic class IdentitiedObject { @Setter(AccessLevel.PRIVATE) @Getter(AccessLevel.PRIVATE) @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;}然后,从 IdentitiedObject 派生出 IdentitiedEmail 类,用以完成值对象建模。@Data@Setter(AccessLevel.PRIVATE)@Entitypublic class IdentitiedEmail extends IdentitiedObject implements ValueObject { @Column(name = “email_name”) private String name; @Column(name = “email_domain”) private String domain; private IdentitiedEmail() { } private IdentitiedEmail(String name, String domain) { Preconditions.checkArgument(StringUtils.isNotEmpty(name), “name can not be null”); Preconditions.checkArgument(StringUtils.isNotEmpty(domain), “domain can not be null”); this.setName(name); this.setDomain(domain); } public static IdentitiedEmail apply(String email) { Preconditions.checkArgument(StringUtils.isNotEmpty(email), “email can not be null”); String[] ss = email.split("@"); Preconditions.checkArgument(ss.length == 2, “not Email”); return new IdentitiedEmail(ss[0], ss[1]); } @Override public String toString() { return this.getName() + “@” + this.getDomain(); }}此时,就可以使用 JPA 的 @OneToMany 特性存储多个值:@Data@Entitypublic class PersonOneToMany { @OneToMany private List<IdentitiedEmail> emails = Lists.newArrayList();}5.2.5 ORM 与 枚举状态对象大多持久化框架都提供了对枚举类型的支持。要么使用枚举值得 String,要么使用枚举值得 Index,其实都不是最佳方案,对以后得重构不太友好,建议使用自定义 code 进行持久化处理。定义枚举:public enum PersonStatus implements CodeBasedEnum<PersonStatus> { ENABLE(1), DISABLE(0); private final int code; PersonStatus(int code) { this.code = code; } @Override public int getCode() { return this.code; } public static PersonStatus parseByCode(Integer code){ for (PersonStatus status : values()){ if (code.intValue() == status.getCode()){ return status; } } return null; }}扩展枚举 Converter:public class PersonStatusConverter implements AttributeConverter<PersonStatus, Integer> { @Override public Integer convertToDatabaseColumn(PersonStatus attribute) { return attribute != null ? attribute.getCode() : null; } @Override public PersonStatus convertToEntityAttribute(Integer dbData) { return dbData == null ? null : PersonStatus.parseByCode(dbData); }}配置属性:@Data@Setter(AccessLevel.PRIVATE)public class Person{ @Embedded private Email primary; @Embedded private Email second; @Convert(converter = PersonStatusConverter.class) private PersonStatus status; public void updateEmail(Email primary, Email second){ Preconditions.checkArgument(primary != null); Preconditions.checkArgument(second != null); setPrimary(primary); setSecond(second); }}此时,通过枚举对象中的 code 进行持久化。5.2.6 阻抗在使用 DB 进行值对象持久化时,经常遇到阻抗。当面临阻抗时,我们应该从领域模型角度,而不是持久化角度去思考问题。根据领域模型来来设计数据模型,而不是通过数据模型来设计领域模型。报表和商业智能应该由专门的数据模型进行处理,而不是生产环境的数据模型。6 值对象其他用途6.1 用值对象表示标准类型标准类型是用于表示事物类型的描述性对象。Java 的枚举时实现标准类型的一种简单方法。枚举提供了一组有限数量的值对象,它是非常轻量的,并且无副作用。一个共享的不变值对象,可以从持久化存储中获取,此时可以使用标准类型的领域服务和工厂来获取值对象。我们应该为每组标准类型创建一个领域服务或工厂。如果打算使用常规值对象来表示标准类型,可以使用领域服务或工厂来静态的创建值对象实例。6.2 最小集成当模型概念从上游上下文流入下游上下文中,尽量使用值对象来表示这些概念。在有可能的情况下,使用值对象完成上下文之间的集成。7 小结值对象是 DDD 建模结构体,它用于表示像度量这样的描述概念。值对象没有身份,比实体要简单得多。建议将数字和字符串封装成值对象,以更好的表示领域概念。值对象是不可变的,他们的值在创建后,就不在发生变化。值对象是内聚的,将多个特征封装成一个完整的概念。可以通过组合值对象来创建新的值对象,而不改变原始值。值对象是自验证的,它不应该处于无效状态。可以使用静态工厂、微类型等模式提高值对象的易用性。对于 NoSQL 的存储,直接使用反规范持久化值对象,面向文档数据库是首选。对于 SQL 存储,相对要麻烦下,存在大量的阻抗。 ...

March 25, 2019 · 7 min · jiezi

ddd-lite-codegen 样板代码终结者

ddd-lite-codegen基于 ddd lite 和 ddd lite spring 体系构建,基于领域模型对象自动生成其他非核心代码。0. 运行原理ddd lite codegen 构建于 apt 技术之上。框架提供若干注解和注解处理器,在编译阶段,自动生成所需的 Base 类。这些 Base 类随着领域对象的重构而变化,从而大大减少样板代码。如有特殊需求,可以通过子类进行扩展,而无需修改 Base 类的内容(每次编译,Base 类都会自动生成)。1. 配置该框架采用两步处理,第一步由 maven 的 apt plugin 完成,第二步由编译器调用 apt 组件完成。对于 maven 项目,需要添加相关依赖和插件,具体如下:<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.geekhalo</groupId> <artifactId>gh-ddd-lite-demo</artifactId> <version>1.0.0-SNAPSHOT</version> <parent> <groupId>com.geekhalo</groupId> <artifactId>gh-base-parent</artifactId> <version>1.0.0-SNAPSHOT</version> </parent> <properties> <service.name>demo</service.name> <server.name>gh-${service.name}-service</server.name> <server.version>v1</server.version> <server.description>${service.name} Api</server.description> <servlet.basePath>/${service.name}-api</servlet.basePath> </properties> <dependencies> <!– 添加 ddd 相关支持–> <dependency> <groupId>com.geekhalo</groupId> <artifactId>gh-ddd-lite</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.geekhalo</groupId> <artifactId>gh-ddd-lite-spring</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> <!– 添加 code gen 依赖,将自动启用 EndpointCodeGenProcessor 处理器–> <!–编译时有效即可,运行时,不需要引用–> <dependency> <groupId>com.geekhalo</groupId> <artifactId>gh-ddd-lite-codegen</artifactId> <version>1.0.1-SNAPSHOT</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <scope>provided</scope> </dependency> <!– 持久化主要由 Spring Data 提供支持–> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-mongodb</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency> <!– 添加测试支持–> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!– 添加 Swagger 支持–> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>com.mysema.maven</groupId> <artifactId>apt-maven-plugin</artifactId> <version>1.1.3</version> <executions> <execution> <goals> <goal>process</goal> </goals> <configuration> <outputDirectory>target/generated-sources/java</outputDirectory> <processors> <!–添加 Querydsl 处理器–> <processor>com.querydsl.apt.QuerydslAnnotationProcessor</processor> <!–添加 DDD 处理器–> <processor>com.geekhalo.ddd.lite.codegen.DDDCodeGenProcessor</processor> </processors> </configuration> </execution> </executions> </plugin> </plugins> </build></project>2. GenCreator领域对象为限界上下文中受保护对象,绝对不应该将其暴露到外面。因此,在创建一个新的领域对象时,需要一种机制将所需数据传递到模型中。常用的机制就是将创建时所需数据封装成一个 dto 对象,通过这个 dto 对象来传递数据,领域对象从 dto 中提取所需数据,完成对象创建工作。creator 就是这种特殊的 Dto,在封装创建对象所需的数据的同时,提供数据到领域对象的绑定操作。2.1 常规做法假设,现在有一个 Person 类:@Datapublic class Person { private String name; private Date birthday; private Boolean enable;}我们需要创建新的 Person 对象,比较正统的方式便是,创建一个 PersonCreator,用于封装所需数据:@Datapublic class PersonCreator { private String name; private Date birthday; private Boolean enable;}然后,在 Person 中添加创建方法,如:public static Person create(PersonCreator creator){ Person person = new Person(); person.setName(creator.getName()); person.setBirthday(creator.getBirthday()); person.setEnable(creator.getEnable()); return person;}大家有没有发现问题:Person 和 PersonCreator 包含的属性基本相同如果在 Person 中添加、移除、修改属性,会同时调整三处(Person、PersonCreator、create 方法),遗漏任何一处,都会导致逻辑错误对于这种机械而且有规律的场景,是否可以采用自动化方式完成?2.2 @GenCreator@GenCreator 便是基于此场景产生的。2.2.1 启用 GenCreator新建 Person 类,在类上添加 @GenCreator 注解。@GenCreator@Datapublic class Person extends JpaAggregate{ private String name; private Date birthday; private Boolean enable;}2.2.2 编译代码,生成 BaseXXXXCreator 类执行 mvn clean compile 命令,在 target/generated-sources/java 对应包下,会出现一个 BasePersonCreator 类,如下:@Datapublic abstract class BasePersonCreator<T extends BasePersonCreator> { @Setter(AccessLevel.PUBLIC) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “birthday” ) private Date birthday; @Setter(AccessLevel.PUBLIC) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “enable” ) private Boolean enable; @Setter(AccessLevel.PUBLIC) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “name” ) private String name; public void accept(Person target) { target.setBirthday(getBirthday()); target.setEnable(getEnable()); target.setName(getName()); }}该类含有与 Person 一样的属性,并提供 accept 方法,对 person 对象执行对应属性的 set 操作。2.2.3 构建 PersonCreator基于 BasePersonCreator 创建 PersonCreator 类。public class PersonCreator extends BasePersonCreator<PersonCreator>{}2.2.4 添加静态 create 方法使用 PersonCreator 为 Person 提供静态工厂方法。@GenCreator@Datapublic class Person extends JpaAggregate{ private String name; private Date birthday; private Boolean enable; public static Person create(PersonCreator creator){ Person person = new Person(); creator.accept(person); return person; }}以后 Person 属性的变化,将自动应用于 BasePersonCreator 中,程序的其他部分没有任何改变。2.3 运行原理@GenCreator 运行原理如下:自动读取当前类的 setter 方法;筛选 public 和 protected 访问级别的 setter 方法,将其作为属性添加到 BaseXXXCreator 类中;创建 accept 方法,读取 BaseXXXXCreator 的属性,并通过 setter 方法写回业务数据。对于不需要添加到 Creator 的 setter 方法,可以使用 @GenCreatorIgnore 忽略该方法。细心的同学可能注意到,在 BaseXXXXCreator 类的属性上存在 @ApiModelProperty 注解,该注解为 Swagger 注解,用于生成 Swagger 文档。我们可以使用 @Description 注解,标注字段描述信息,这些信息会自动添加的 Swagger 文档中。3. GenUpdaterGenUpdater 和 GenCreator 非常相似,主要应用于对象修改场景。相对于创建,对象修改场景有点特殊,即对 null 的处理,当用户传递 null 进来,不知道是属性不修改还是属性设置为 null。针对这种场景,常用方案是将其包装在一个 Optional 中,如果 Optional 对应的属性为 null,表示对该属性不做处理;如果 Optional 中包含的 value 为null,表示将属性值设置为 null。3.1 启用 GenUpdater在 Person 类上添加 @GenUpdater 注解。@GenUpdater@GenCreator@Datapublic class Person extends JpaAggregate{ @Description(“名称”) private String name; private Date birthday; private Boolean enable; public static Person create(PersonCreator creator){ Person person = new Person(); creator.accept(person); return person; }}3.2 编译代码,生成 Base 类执行 mvn clean compile, 生成 BasePersonUpdater。@Datapublic abstract class BasePersonUpdater<T extends BasePersonUpdater> { @Setter(AccessLevel.PRIVATE) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “birthday” ) private DataOptional<Date> birthday; @Setter(AccessLevel.PRIVATE) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “enable” ) private DataOptional<Boolean> enable; @Setter(AccessLevel.PRIVATE) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “名称”, name = “name” ) private DataOptional<String> name; public T birthday(Date birthday) { this.birthday = DataOptional.of(birthday); return (T) this; } public T acceptBirthday(Consumer<Date> consumer) { if(this.birthday != null){ consumer.accept(this.birthday.getValue()); } return (T) this; } public T enable(Boolean enable) { this.enable = DataOptional.of(enable); return (T) this; } public T acceptEnable(Consumer<Boolean> consumer) { if(this.enable != null){ consumer.accept(this.enable.getValue()); } return (T) this; } public T name(String name) { this.name = DataOptional.of(name); return (T) this; } public T acceptName(Consumer<String> consumer) { if(this.name != null){ consumer.accept(this.name.getValue()); } return (T) this; } public void accept(Person target) { this.acceptBirthday(target::setBirthday); this.acceptEnable(target::setEnable); this.acceptName(target::setName); }}该类与 BasePersonCreator 存在一些差异:属性使用 DataOptional<T> 进行包装;每个属性提供 T fieldName(FieldType fieldName) 方法,用于设置对应的属性值;每个属性提供 T acceptFieldName(Consumer<FieldType> consumer) 方法,在 DataOptional 属性不为空的时候,进行业务处理;提供 void accept(Target target) 方法,将 BaseXXXXUpdater 中的数据写回到 Target 对象中。与 BaseXXXCreator 类似,BaseXXXUpdater 也提供 @GenUpdaterIgnore 注解,对方法进行忽略;也可使用 @Description 注解生成 Swagger 文档描述。备注 GenUpdate 与 GenCreator 最大差别在于,Updater 机制,只会应用于 public 的 setter 方法。因此,对于不需要更新的属性,可以使用 protected 访问级别,这样只会在 creator 中存在。3.3 创建 PersonUpdater 类创建 PersonUpdater 类继承 BasePersonUpdater。public class PersonUpdater extends BasePersonUpdater<PersonUpdater>{}3.4 创建 update 方法为 Person 类 添加 update 方法public void update(PersonUpdater updater){ updater.accept(this);}4. genDtodto 是大家最熟悉的模式,但这里的 dto,只针对返回数据。请求数据,统一使用 Creator 和 Updater 完成。4.1 启用 GenDto为 Person 类添加 @GenDto 注解。@GenUpdater@GenCreator@GenDto@Datapublic class Person extends JpaAggregate { @Description(“名称”) private String name; @Setter(AccessLevel.PROTECTED) private Date birthday; private Boolean enable; public static Person create(PersonCreator creator){ Person person = new Person(); creator.accept(person); return person; } public void update(PersonUpdater updater){ updater.accept(this); }}4.2 编译代码,生成 Base 类执行 mvn clean compile 生成 BasePersonDto,如下:@Datapublic abstract class BasePersonDto extends AbstractAggregateDto implements Serializable { @Setter(AccessLevel.PACKAGE) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “birthday” ) private Date birthday; @Setter(AccessLevel.PACKAGE) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “enable” ) private Boolean enable; @Setter(AccessLevel.PACKAGE) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “名称”, name = “name” ) private String name; protected BasePersonDto(Person source) { super(source); this.setBirthday(source.getBirthday()); this.setEnable(source.getEnable()); this.setName(source.getName()); }}4.3 新建 PersonDto新建 PersonDto 继承自 BasePersonDto。public class PersonDto extends BasePersonDto{ public PersonDto(Person source) { super(source); }}4.3 @GenDto 生成策略@GenDto 生成策略如下:查找类所有的 public getter 方法;为每个 getter 方法添加属性;新建构造函数,在构造函数中完成目标对象到 BaseXXXDto 的属性赋值。5. genConverterconverter 主要针对使用 Jpa 作为存储的场景。5.1 设计背景Jpa 对 enum 类型提供了两种存储方式:存储 enum 的名称;存储 enum 的定义顺序。这两者在使用上都存在一定的问题,通常情况下,需要存储自定义 code,因此,需要实现枚举类型的 Converter。5.2 @GenCodeBasedEnumConverter5.2.1 启用 GenCodeBasedEnumConverter新建 PersonStatus 枚举,实现 CodeBasedEnum 接口,添加 @GenCodeBasedEnumConverter 注解。@GenCodeBasedEnumConverterpublic enum PersonStatus implements CodeBasedEnum<PersonStatus> { ENABLE(1), DISABLE(0); private final int code; PersonStatus(int code) { this.code = code; } @Override public int getCode() { return this.code; }}5.2.2 编译代码,生成 CodeBasedPersonStatusConverter执行 mvn clean compile 命令,自动生成 CodeBasedPersonStatusConverter 类public final class CodeBasedPersonStatusConverter implements AttributeConverter<PersonStatus, Integer> { public Integer convertToDatabaseColumn(PersonStatus i) { return i == null ? null : i.getCode(); } public PersonStatus convertToEntityAttribute(Integer i) { if (i == null) return null; for (PersonStatus value : PersonStatus.values()){ if (value.getCode() == i){ return value; } } return null; }}5.2.3 应用 CodeBasedPersonStatusConverter在 Person 中使用 CodeBasedPersonStatusConverter 转化器:public class Person extends JpaAggregate { @Description(“名称”) private String name; @Setter(AccessLevel.PROTECTED) private Date birthday; private Boolean enable; @Convert(converter = CodeBasedPersonStatusConverter.class) private PersonStatus status;}6. genRepositoryRepository 是领域驱动设计中很重要的一个组件,一个聚合根对于一个 Repository。Repository 与基础设施关联紧密,框架通过 @GenSpringDataRepository 提供了 Spring Data Repository 的支持。6.1 启用 GenSpringDataRepository在 Person 上添加 @GenSpringDataRepository 注解。@GenSpringDataRepository@Datapublic class Person extends JpaAggregate { @Description(“名称”) private String name; @Setter(AccessLevel.PROTECTED) private Date birthday; private Boolean enable; @Convert(converter = CodeBasedPersonStatusConverter.class) private PersonStatus status;}6.2 编译代码,生成 Base 类执行 mvn clean compile 生成 BasePersonRepository 类interface BasePersonRepository extends AggregateRepository<Long, Person>, Repository<Person, Long>, QuerydslPredicateExecutor<Person> {}该接口实现了 AggregateRepository<Long, Person>、Repository<Person, Long>、QuerydslPredicateExecutor<Person> 三个接口,其中 AggregateRepository 为 ddd-lite 框架接口,另外两个为 spring data 接口。6.3 创建 PersonRepository创建 PersonRepository 继承自 BasePersonRepository。public interface PersonRepository extends BasePersonRepository{}6.4 使用 PersonRepositoryPersonRepository 为 Spring Data 标准定义接口,Spring Data 会为其自动创建代理类,无需我们实现便可以直接注入使用。6.5 Index 支持一般情况下,PersonRepository 中的方法能够满足我们大多数需求,如果存在关联关系,可以使用 @Index 处理。在 Person 中,添加 @Index({“name”, “status”}) 和 @QueryEntity 注解@GenSpringDataRepository@Index({“name”, “status”})@QueryEntity@Datapublic class Person extends JpaAggregate { @Description(“名称”) private String name; @Setter(AccessLevel.PROTECTED) private Date birthday; private Boolean enable; @Convert(converter = CodeBasedPersonStatusConverter.class) private PersonStatus status;}执行 mvn clean compile,查看生成的 PersonRepositoryinterface BasePersonRepository extends AggregateRepository<Long, Person>, Repository<Person, Long>, QuerydslPredicateExecutor<Person> { Long countByName(String name); default Long countByName(String name, Predicate predicate) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QPerson.person.name.eq(name));; booleanBuilder.and(predicate); return this.count(booleanBuilder.getValue()); } Long countByNameAndStatus(String name, PersonStatus status); default Long countByNameAndStatus(String name, PersonStatus status, Predicate predicate) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QPerson.person.name.eq(name));; booleanBuilder.and(QPerson.person.status.eq(status));; booleanBuilder.and(predicate); return this.count(booleanBuilder.getValue()); } List<Person> getByName(String name); List<Person> getByName(String name, Sort sort); default List<Person> getByName(String name, Predicate predicate) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QPerson.person.name.eq(name));; booleanBuilder.and(predicate); return Lists.newArrayList(findAll(booleanBuilder.getValue())); } default List<Person> getByName(String name, Predicate predicate, Sort sort) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QPerson.person.name.eq(name));; booleanBuilder.and(predicate); return Lists.newArrayList(findAll(booleanBuilder.getValue(), sort)); } List<Person> getByNameAndStatus(String name, PersonStatus status); List<Person> getByNameAndStatus(String name, PersonStatus status, Sort sort); default List<Person> getByNameAndStatus(String name, PersonStatus status, Predicate predicate) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QPerson.person.name.eq(name));; booleanBuilder.and(QPerson.person.status.eq(status));; booleanBuilder.and(predicate); return Lists.newArrayList(findAll(booleanBuilder.getValue())); } default List<Person> getByNameAndStatus(String name, PersonStatus status, Predicate predicate, Sort sort) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QPerson.person.name.eq(name));; booleanBuilder.and(QPerson.person.status.eq(status));; booleanBuilder.and(predicate); return Lists.newArrayList(findAll(booleanBuilder.getValue(), sort)); } Page<Person> findByName(String name, Pageable pageable); default Page<Person> findByName(String name, Predicate predicate, Pageable pageable) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QPerson.person.name.eq(name));; booleanBuilder.and(predicate); return findAll(booleanBuilder.getValue(), pageable); } Page<Person> findByNameAndStatus(String name, PersonStatus status, Pageable pageable); default Page<Person> findByNameAndStatus(String name, PersonStatus status, Predicate predicate, Pageable pageable) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QPerson.person.name.eq(name));; booleanBuilder.and(QPerson.person.status.eq(status));; booleanBuilder.and(predicate); return findAll(booleanBuilder.getValue(), pageable); }}根据索引信息,BasePersonRepository 自动生成了各种查询,这些查询也不用我们自己去实现,直接使用即可。7. GenApplicationapplication 是领域模型最直接的使用者。通常情况下,Application 会涵盖聚合根中的 Command 方法、Repository 中的查询方法、DomainService 的操作方法。其中又以聚合根中的 Command 方法和 Repository 中的查询方法为主。框架为此提供了自动生成 BaseXXXApplication 的支持。框架提供 @GenApplication 注解,作为自动生成的入口。7.1 聚合根的 Command将 @GenApplication 添加到聚合根上,框架自动识别 Command 的方法,并将其添加到 BaseApplication 中。7.1.1 启用 GenApplication在 Person 中添加 @GenApplication 注解,为了更好的演示 Command 方法,新增 enable 和 disable 两个方法:@GenApplication@Datapublic class Person extends JpaAggregate { @Description(“名称”) private String name; @Setter(AccessLevel.PROTECTED) private Date birthday; private Boolean enable; @Convert(converter = CodeBasedPersonStatusConverter.class) private PersonStatus status; public static Person create(PersonCreator creator){ Person person = new Person(); creator.accept(person); return person; } public void update(PersonUpdater updater){ updater.accept(this); } public void enable(){ setStatus(PersonStatus.ENABLE); } public void disable(){ setStatus(PersonStatus.DISABLE); }}7.1.2 编译代码,生成 Base 类执行 mvn clean compile,生成 BasePersonApplication 接口和 BasePersonApplicationSupport 实现类。BasePersonApplication 如下:public interface BasePersonApplication { Long create(PersonCreator creator); void disable(@Description(“主键”) Long id); void update(@Description(“主键”) Long id, PersonUpdater updater); void enable(@Description(“主键”) Long id);}BasePersonApplication 主要做了如下工作:对于 Person 的 create 静态工厂方法,将自动创建 create 方法;对于 Person 的返回为 void 方法(非 setter 方法),将自创建为 command 方法,为其增加一个主键参数。BasePersonApplicationSupport 如下:abstract class BasePersonApplicationSupport extends AbstractApplication implements BasePersonApplication { @Autowired private DomainEventBus domainEventBus; @Autowired private PersonRepository personRepository; protected BasePersonApplicationSupport(Logger logger) { super(logger); } protected BasePersonApplicationSupport() { } protected PersonRepository getPersonRepository() { return this.personRepository; } protected DomainEventBus getDomainEventBus() { return this.domainEventBus; } @Transactional public Long create(PersonCreator creator) { Person result = creatorFor(this.getPersonRepository()) .publishBy(getDomainEventBus()) .instance(() -> Person.create(creator)) .call(); logger().info(“success to create {} using parm {}",result.getId(), creator); return result.getId(); } @Transactional public void disable(@Description(“主键”) Long id) { Person result = updaterFor(this.getPersonRepository()) .publishBy(getDomainEventBus()) .id(id) .update(agg -> agg.disable()) .call(); logger().info(“success to disable for {} using parm “, id); } @Transactional public void update(@Description(“主键”) Long id, PersonUpdater updater) { Person result = updaterFor(this.getPersonRepository()) .publishBy(getDomainEventBus()) .id(id) .update(agg -> agg.update(updater)) .call(); logger().info(“success to update for {} using parm {}”, id, updater); } @Transactional public void enable(@Description(“主键”) Long id) { Person result = updaterFor(this.getPersonRepository()) .publishBy(getDomainEventBus()) .id(id) .update(agg -> agg.enable()) .call(); logger().info(“success to enable for {} using parm “, id); }}BasePersonApplicationSupport 主要完成工作如下:自动注入 DomainEventBus 和 PersonRepository 等相关资源;实现聚合根中的 Command 方法,并为其开启事务支持。7.1.3 创建 PersonApplication 和 PersonApplicationImpl创建 PersonApplication 和 PersonApplicationImpl 类,具体如下:PersonApplication:public interface PersonApplication extends BasePersonApplication{}PersonApplicationImpl:@Servicepublic class PersonApplicationImpl extends BasePersonApplicationSupport implements PersonApplication {}7.2 Repository 中的 Query将 @GenApplication 添加到 Repository 上,框架将当前接口中的方法作为 Query 方法,自动添加到 Application 中。7.2.1 启用 GenApplication在 PersonRepository 添加 @GenApplication 注解;@GenApplicationpublic interface PersonRepository extends BasePersonRepository{ }7.2.2 添加查询方法在 PersonRepository 中添加要暴露的方法:@GenApplicationpublic interface PersonRepository extends BasePersonRepository{ @Override Page<Person> findByName(String name, Pageable pageable); @Override Page<Person> findByNameAndStatus(String name, PersonStatus status, Pageable pageable);}7.2.3 编译代码,生成 Base 类执行 mvn clean compile 查看现在的 BasePersonApplication 和 BasePersonApplicationSupport。BasePersonApplication:public interface BasePersonApplication { Long create(PersonCreator creator); void update(@Description(“主键”) Long id, PersonUpdater updater); void enable(@Description(“主键”) Long id); void disable(@Description(“主键”) Long id); Page<PersonDto> findByName(String name, Pageable pageable); Page<PersonDto> findByNameAndStatus(String name, PersonStatus status, Pageable pageable);}可见,查询方法已经添加到 BasePersonApplication 中。BasePersonApplicationSupport:abstract class BasePersonApplicationSupport extends AbstractApplication implements BasePersonApplication { @Autowired private DomainEventBus domainEventBus; @Autowired private PersonRepository personRepository; protected BasePersonApplicationSupport(Logger logger) { super(logger); } protected BasePersonApplicationSupport() { } protected PersonRepository getPersonRepository() { return this.personRepository; } protected DomainEventBus getDomainEventBus() { return this.domainEventBus; } @Transactional public Long create(PersonCreator creator) { Person result = creatorFor(this.getPersonRepository()) .publishBy(getDomainEventBus()) .instance(() -> Person.create(creator)) .call(); logger().info(“success to create {} using parm {}",result.getId(), creator); return result.getId(); } @Transactional public void update(@Description(“主键”) Long id, PersonUpdater updater) { Person result = updaterFor(this.getPersonRepository()) .publishBy(getDomainEventBus()) .id(id) .update(agg -> agg.update(updater)) .call(); logger().info(“success to update for {} using parm {}”, id, updater); } @Transactional public void enable(@Description(“主键”) Long id) { Person result = updaterFor(this.getPersonRepository()) .publishBy(getDomainEventBus()) .id(id) .update(agg -> agg.enable()) .call(); logger().info(“success to enable for {} using parm “, id); } @Transactional public void disable(@Description(“主键”) Long id) { Person result = updaterFor(this.getPersonRepository()) .publishBy(getDomainEventBus()) .id(id) .update(agg -> agg.disable()) .call(); logger().info(“success to disable for {} using parm “, id); } protected <T> List<T> convertPersonList(List<Person> src, Function<Person, T> converter) { if (CollectionUtils.isEmpty(src)) return Collections.emptyList(); return src.stream().map(converter).collect(Collectors.toList()); } protected <T> Page<T> convvertPersonPage(Page<Person> src, Function<Person, T> converter) { return src.map(converter); } protected abstract PersonDto convertPerson(Person src); protected List<PersonDto> convertPersonList(List<Person> src) { return convertPersonList(src, this::convertPerson); } protected Page<PersonDto> convvertPersonPage(Page<Person> src) { return convvertPersonPage(src, this::convertPerson); } @Transactional( readOnly = true ) public <T> Page<T> findByName(String name, Pageable pageable, Function<Person, T> converter) { Page<Person> result = this.getPersonRepository().findByName(name, pageable); return convvertPersonPage(result, converter); } @Transactional( readOnly = true ) public Page<PersonDto> findByName(String name, Pageable pageable) { Page<Person> result = this.getPersonRepository().findByName(name, pageable); return convvertPersonPage(result); } @Transactional( readOnly = true ) public <T> Page<T> findByNameAndStatus(String name, PersonStatus status, Pageable pageable, Function<Person, T> converter) { Page<Person> result = this.getPersonRepository().findByNameAndStatus(name, status, pageable); return convvertPersonPage(result, converter); } @Transactional( readOnly = true ) public Page<PersonDto> findByNameAndStatus(String name, PersonStatus status, Pageable pageable) { Page<Person> result = this.getPersonRepository().findByNameAndStatus(name, status, pageable); return convvertPersonPage(result); }}与上个版本相比,新增以下逻辑:添加 convertPersonList、convertPersonPage等转化方法;添加 convertPerson 抽象方法,用于完成 Person 到 PersonDto 的转化;添加 findByNameAndStatus 和 findByName 相关查询方法,并将其标准为只读。7.2.4 调整 PersonApplicationImpl为 PersonApplicationImpl 添加 convertPerson 实现。@Servicepublic class PersonApplicationImpl extends BasePersonApplicationSupport implements PersonApplication { @Override protected PersonDto convertPerson(Person src) { return new PersonDto(src); }}至此,对领域的支持就介绍完了,我们看下我们的 Person 类。@GenUpdater@GenCreator@GenDto@GenSpringDataRepository@Index({“name”, “status”})@QueryEntity@GenApplication@Datapublic class Person extends JpaAggregate { @Description(“名称”) private String name; @Setter(AccessLevel.PROTECTED) private Date birthday; private Boolean enable; @Convert(converter = CodeBasedPersonStatusConverter.class) private PersonStatus status; public static Person create(PersonCreator creator){ Person person = new Person(); creator.accept(person); return person; } public void update(PersonUpdater updater){ updater.accept(this); } public void enable(){ setStatus(PersonStatus.ENABLE); } public void disable(){ setStatus(PersonStatus.DISABLE); }}在 Person 上有一堆的 @GenXXXX,感觉有点泛滥,对此,框架提供了两个符合注解,针对聚合和实体进行优化。8. EnableGenForEntity统一开启实体相关的代码生成器。@EnableGenForEntity 等同于同时开启如下注解:注解含义@GenCreator自动生成 BaseXXXCreator@GenDto自动生成 BaseXXXXDto@GenUpdater自动生成 BaseXXXXUpdater9. EnableGenForAggregate统一开启聚合相关的代码生成器。@EnableGenForAggregate 等同于同时开启如下注解:注解含义@GenCreator自动生成 BaseXXXCreator@GenDto自动生成 BaseXXXXDto@GenUpdater自动生成 BaseXXXXUpdaterGenSpringDataRepository自动生成基于 Spring Data 的 BaseXXXRepository@GenApplication自动生成 BaseXXXXApplication 以及实现类 BaseXXXXXApplicationSupport对于领域对象的支持,已经非常完成,那对于 Application 的调用者呢?框架提供了对于 Controller 的支持。10. GenController将 @GenController 添加到 Application 接口上,将启用对 Controller 的支持。10.1 启用 GenController在 PersonApplication 启用 Controller 支持。在 PersonApplication 接口上添加 @GenController(“com.geekhalo.ddd.lite.demo.controller.BasePersonController”)@GenController(“com.geekhalo.ddd.lite.demo.controller.BasePersonController”)public interface PersonApplication extends BasePersonApplication{}10.2 编译代码,生成 Base 类执行 mvn clean compile,查看生成的 BasePersonController 类:abstract class BasePersonController { @Autowired private PersonApplication application; protected PersonApplication getApplication() { return this.application; } @ResponseBody @ApiOperation( value = “”, nickname = “create” ) @RequestMapping( value = “/_create”, method = RequestMethod.POST ) public ResultVo<Long> create(@RequestBody PersonCreator creator) { return ResultVo.success(this.getApplication().create(creator)); } @ResponseBody @ApiOperation( value = “”, nickname = “disable” ) @RequestMapping( value = “{id}/_disable”, method = RequestMethod.POST ) public ResultVo<Void> disable(@PathVariable(“id”) Long id) { this.getApplication().disable(id); return ResultVo.success(null); } @ResponseBody @ApiOperation( value = “”, nickname = “update” ) @RequestMapping( value = “{id}/_update”, method = RequestMethod.POST ) public ResultVo<Void> update(@PathVariable(“id”) Long id, @RequestBody PersonUpdater updater) { this.getApplication().update(id, updater); return ResultVo.success(null); } @ResponseBody @ApiOperation( value = “”, nickname = “enable” ) @RequestMapping( value = “{id}/_enable”, method = RequestMethod.POST ) public ResultVo<Void> enable(@PathVariable(“id”) Long id) { this.getApplication().enable(id); return ResultVo.success(null); } @ResponseBody @ApiOperation( value = “”, nickname = “findByNameAndStatus” ) @RequestMapping( value = “/_find_by_name_and_status”, method = RequestMethod.POST ) public ResultVo<PageVo<PersonDto>> findByNameAndStatus(@RequestBody FindByNameAndStatusReq req) { return ResultVo.success(PageVo.apply(this.getApplication().findByNameAndStatus(req.getName(), req.getStatus(), req.getPageable()))); } @ResponseBody @ApiOperation( value = “”, nickname = “findByName” ) @RequestMapping( value = “/_find_by_name”, method = RequestMethod.POST ) public ResultVo<PageVo<PersonDto>> findByName(@RequestBody FindByNameReq req) { return ResultVo.success(PageVo.apply(this.getApplication().findByName(req.getName(), req.getPageable()))); }}该类主要完成:自动注入 PersonApplication;为 Command 中的 create 方法创建对于方法,并返回创建后的主键;为 Command 中的 update 方法创建对于方法,在 path 中添加主键参数,并返回 Void;为 Query 方法创建对于的方法;统一使用 ResultVo 作为返回值;对 Spring Data 中的 Pageable 和 Page 进行封装;对于多参数方法,创建封装类,使用封装类收集数据;添加 Swagger 相关注解。10.3 新建 PersonController新建 PersonController 实现 BasePersonController。并添加 RequestMapping,设置 base path。@RequestMapping(“person”)@RestControllerpublic class PersonController extends BasePersonController{}10.4 启动,查看 Swagger 文档至此,Controller 就开发好了,启动项目,输入 http://127.0.0.1:8080/swagger-ui.html 便可以看到相关接口。 ...

March 11, 2019 · 11 min · jiezi

领域驱动设计,构建简单的新闻系统,20分钟够吗?

让我们使用领域驱动的方式,构建一个简单的系统。1. 需求新闻系统的需求如下:创建新闻类别;修改新闻类别,只能更改名称;禁用新闻类别,禁用后的类别不能添加新闻;启用新闻类别;根据类别id获取类别信息;指定新闻类别id,创建新闻;更改新闻信息,只能更改标题和内容;禁用新闻;启用新闻;分页查找给定类别的新闻,禁用的新闻不可见。2. 工期估算大家觉得,针对上面需求,大概需要多长时间可以完成,可以先写下来。3. 起航3.1. 项目准备构建项目,使用 http://start.spring.io 或使用模板工程,构建我们的项目(Sprin Boot 项目),在这就不多叙述。3.1.1. 添加依赖首先,添加 gh-ddd-lite 相关依赖和插件。<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.geekhalo</groupId> <artifactId>gh-ddd-lite-demo</artifactId> <version>1.0.0-SNAPSHOT</version> <parent> <groupId>com.geekhalo</groupId> <artifactId>gh-base-parent</artifactId> <version>1.0.0-SNAPSHOT</version> </parent> <properties> <service.name>demo</service.name> <server.name>gh-${service.name}-service</server.name> <server.version>v1</server.version> <server.description>${service.name} Api</server.description> <servlet.basePath>/${service.name}-api</servlet.basePath> </properties> <dependencies> <dependency> <groupId>com.geekhalo</groupId> <artifactId>gh-ddd-lite</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.geekhalo</groupId> <artifactId>gh-ddd-lite-spring</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.geekhalo</groupId> <artifactId>gh-ddd-lite-codegen</artifactId> <version>1.0.1-SNAPSHOT</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.hibernate.javax.persistence</groupId> <artifactId>hibernate-jpa-2.1-api</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> <configuration> <executable>true</executable> <layout>ZIP</layout> </configuration> </plugin> <plugin> <groupId>com.mysema.maven</groupId> <artifactId>apt-maven-plugin</artifactId> <version>1.1.3</version> <executions> <execution> <goals> <goal>process</goal> </goals> <configuration> <outputDirectory>target/generated-sources/java</outputDirectory> <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor> <!–<processor>com.querydsl.apt.QuerydslAnnotationProcessor</processor>–> </configuration> </execution> </executions> </plugin> </plugins> </build></project>3.1.2. 添加配置信息在 application.properties 文件中添加数据库相关配置。spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driverspring.datasource.url=jdbc:mysql://127.0.0.1:3306/db_test?useUnicode=true&characterEncoding=utf8&useSSL=falsespring.datasource.username=rootspring.datasource.password=spring.application.name=ddd-lite-demoserver.port=8090management.endpoint.beans.enabled=truemanagement.endpoint.conditions.enabled=truemanagement.endpoints.enabled-by-default=falsemanagement.endpoints.web.exposure.include=beans,conditions,env3.1.3. 添加入口类新建 UserApplication 作为应用入口类。@SpringBootApplication@EnableSwagger2public class UserApplication { public static void main(String… args){ SpringApplication.run(UserApplication.class, args); }}使用 SpringBootApplication 和 EnableSwagger2 启用 Spring Boot 和 Swagger 特性。3.2. NewsCategory 建模首先,我们对新闻类型进行建模。3.2.1. 建模 NewsCategory 状态新闻类别状态,用于描述启用、禁用两个状态。在这使用 enum 实现。/** * GenCodeBasedEnumConverter 自动生成 CodeBasedNewsCategoryStatusConverter 类 /@GenCodeBasedEnumConverterpublic enum NewsCategoryStatus implements CodeBasedEnum<NewsCategoryStatus> { ENABLE(1), DISABLE(0); private final int code; NewsCategoryStatus(int code) { this.code = code; } @Override public int getCode() { return code; }}3.2.2. 建模 NewsCategoryNewsCategory 用于描述新闻类别,其中包括状态、名称等。3.2.2.1. 新建 NewsCategory/* * EnableGenForAggregate 自动创建聚合相关的 Base 类 /@EnableGenForAggregate@Data@Entity@Table(name = “tb_news_category”)public class NewsCategory extends JpaAggregate { private String name; @Setter(AccessLevel.PRIVATE) @Convert(converter = CodeBasedNewsCategoryStatusConverter.class) private NewsCategoryStatus status;}3.2.2.2. 自动生成 Base 代码在命令行或ida中执行maven命令,以对项目进行编译,从而触发代码的自动生成。mvn clean compile3.2.2.3. 建模 NewsCategory 创建逻辑我们使用 NewsCategory 的静态工厂,完成其创建逻辑。首先,需要创建 NewsCategoryCreator,作为工程参数。public class NewsCategoryCreator extends BaseNewsCategoryCreator<NewsCategoryCreator>{}其中 BaseNewsCategoryCreator 为框架自动生成的,具体如下:@Datapublic abstract class BaseNewsCategoryCreator<T extends BaseNewsCategoryCreator> { @Setter(AccessLevel.PUBLIC) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “name” ) private String name; public void accept(NewsCategory target) { target.setName(getName()); }}接下来,需要创建静态工程,并完成 NewsCategory 的初始化。/* * 静态工程,完成 NewsCategory 的创建 * @param creator * @return /public static NewsCategory create(NewsCategoryCreator creator){ NewsCategory category = new NewsCategory(); creator.accept(category); category.init(); return category;}/* * 初始化,默认状态位 ENABLE /private void init() { setStatus(NewsCategoryStatus.ENABLE);}3.2.2.4. 建模 NewsCategory 更新逻辑更新逻辑,只对 name 进行更新操作。首先,创建 NewsCategoryUpdater 作为,更新方法的参数。public class NewsCategoryUpdater extends BaseNewsCategoryUpdater<NewsCategoryUpdater>{}同样,BaseNewsCategoryUpdater 也是框架自动生成,具体如下:@Datapublic abstract class BaseNewsCategoryUpdater<T extends BaseNewsCategoryUpdater> { @Setter(AccessLevel.PRIVATE) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “name” ) private DataOptional<String> name; public T name(String name) { this.name = DataOptional.of(name); return (T) this; } public T acceptName(Consumer<String> consumer) { if(this.name != null){ consumer.accept(this.name.getValue()); } return (T) this; } public void accept(NewsCategory target) { this.acceptName(target::setName); }}添加 update 方法:/* * 更新 * @param updater /public void update(NewsCategoryUpdater updater){ updater.accept(this);} 3.2.2.5. 建模 NewsCategory 启用逻辑启用,主要是对 status 的操作.代码如下:/* * 启用 /public void enable(){ setStatus(NewsCategoryStatus.ENABLE);}3.2.2.6. 建模 NewsCategory 禁用逻辑禁用,主要是对 status 的操作。代码如下:/* * 禁用 /public void disable(){ setStatus(NewsCategoryStatus.DISABLE);}至此,NewsCategory 的 Command 就建模完成,让我们总体看下 NewsCategory:/* * EnableGenForAggregate 自动创建聚合相关的 Base 类 /@EnableGenForAggregate@Data@Entity@Table(name = “tb_news_category”)public class NewsCategory extends JpaAggregate { private String name; @Setter(AccessLevel.PRIVATE) @Convert(converter = CodeBasedNewsCategoryStatusConverter.class) private NewsCategoryStatus status; private NewsCategory(){ } /* * 静态工程,完成 NewsCategory 的创建 * @param creator * @return / public static NewsCategory create(NewsCategoryCreator creator){ NewsCategory category = new NewsCategory(); creator.accept(category); category.init(); return category; } /* * 更新 * @param updater / public void update(NewsCategoryUpdater updater){ updater.accept(this); } /* * 启用 / public void enable(){ setStatus(NewsCategoryStatus.ENABLE); } /* * 禁用 / public void disable(){ setStatus(NewsCategoryStatus.DISABLE); } /* * 初始化,默认状态位 ENABLE / private void init() { setStatus(NewsCategoryStatus.ENABLE); }}3.2.2.7. 建模 NewsCategory 查找逻辑查找逻辑主要由 NewsCategoryRepository 完成。新建 NewsCategoryRepository,如下:/* * GenApplication 自动将该接口中的方法添加到 BaseNewsCategoryRepository 中 /@GenApplicationpublic interface NewsCategoryRepository extends BaseNewsCategoryRepository{ @Override Optional<NewsCategory> getById(Long aLong);}同样, BaseNewsCategoryRepository 也是自动生成的。interface BaseNewsCategoryRepository extends SpringDataRepositoryAdapter<Long, NewsCategory>, Repository<NewsCategory, Long>, QuerydslPredicateExecutor<NewsCategory> {}领域对象 NewsCategory 不应该暴露到其他层,因此,我们使用 DTO 模式处理数据的返回,新建 NewsCategoryDto,具体如下:public class NewsCategoryDto extends BaseNewsCategoryDto{ public NewsCategoryDto(NewsCategory source) { super(source); }}BaseNewsCategoryDto 为框架自动生成,如下:@Datapublic abstract class BaseNewsCategoryDto extends JpaAggregateVo implements Serializable { @Setter(AccessLevel.PACKAGE) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “name” ) private String name; @Setter(AccessLevel.PACKAGE) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “status” ) private NewsCategoryStatus status; protected BaseNewsCategoryDto(NewsCategory source) { super(source); this.setName(source.getName()); this.setStatus(source.getStatus()); }}3.2.3. 构建 NewsCategoryApplication至此,领域的建模工作已经完成,让我们对 Application 进行构建。/* * GenController 自动将该类中的方法,添加到 BaseNewsCategoryController 中 /@GenController(“com.geekhalo.ddd.lite.demo.controller.BaseNewsCategoryController”)public interface NewsCategoryApplication extends BaseNewsCategoryApplication{ @Override NewsCategory create(NewsCategoryCreator creator); @Override void update(Long id, NewsCategoryUpdater updater); @Override void enable(Long id); @Override void disable(Long id); @Override Optional<NewsCategoryDto> getById(Long aLong);}自动生成的 BaseNewsCategoryApplication 如下:public interface BaseNewsCategoryApplication { Optional<NewsCategoryDto> getById(Long aLong); NewsCategory create(NewsCategoryCreator creator); void update(@Description(“主键”) Long id, NewsCategoryUpdater updater); void enable(@Description(“主键”) Long id); void disable(@Description(“主键”) Long id);}得益于我们的 EnableGenForAggregate 和 GenApplication 注解,BaseNewsCategoryApplication 包含我们想要的 Command 和 Query 方法。接口已经准备好了,接下来,处理实现类,具体如下:@Servicepublic class NewsCategoryApplicationImpl extends BaseNewsCategoryApplicationSupport implements NewsCategoryApplication { @Override protected NewsCategoryDto convertNewsCategory(NewsCategory src) { return new NewsCategoryDto(src); }}自动生成的 BaseNewsCategoryApplicationSupport 如下:abstract class BaseNewsCategoryApplicationSupport extends AbstractApplication implements BaseNewsCategoryApplication { @Autowired private DomainEventBus domainEventBus; @Autowired private NewsCategoryRepository newsCategoryRepository; protected BaseNewsCategoryApplicationSupport(Logger logger) { super(logger); } protected BaseNewsCategoryApplicationSupport() { } protected NewsCategoryRepository getNewsCategoryRepository() { return this.newsCategoryRepository; } protected DomainEventBus getDomainEventBus() { return this.domainEventBus; } protected <T> List<T> convertNewsCategoryList(List<NewsCategory> src, Function<NewsCategory, T> converter) { if (CollectionUtils.isEmpty(src)) return Collections.emptyList(); return src.stream().map(converter).collect(Collectors.toList()); } protected <T> Page<T> convvertNewsCategoryPage(Page<NewsCategory> src, Function<NewsCategory, T> converter) { return src.map(converter); } protected abstract NewsCategoryDto convertNewsCategory(NewsCategory src); protected List<NewsCategoryDto> convertNewsCategoryList(List<NewsCategory> src) { return convertNewsCategoryList(src, this::convertNewsCategory); } protected Page<NewsCategoryDto> convvertNewsCategoryPage(Page<NewsCategory> src) { return convvertNewsCategoryPage(src, this::convertNewsCategory); } @Transactional( readOnly = true ) public <T> Optional<T> getById(Long aLong, Function<NewsCategory, T> converter) { Optional<NewsCategory> result = this.getNewsCategoryRepository().getById(aLong); return result.map(converter); } @Transactional( readOnly = true ) public Optional<NewsCategoryDto> getById(Long aLong) { Optional<NewsCategory> result = this.getNewsCategoryRepository().getById(aLong); return result.map(this::convertNewsCategory); } @Transactional public NewsCategory create(NewsCategoryCreator creator) { NewsCategory result = creatorFor(this.getNewsCategoryRepository()) .publishBy(getDomainEventBus()) .instance(() -> NewsCategory.create(creator)) .call(); logger().info(“success to create {} using parm {}",result.getId(), creator); return result; } @Transactional public void update(@Description(“主键”) Long id, NewsCategoryUpdater updater) { NewsCategory result = updaterFor(this.getNewsCategoryRepository()) .publishBy(getDomainEventBus()) .id(id) .update(agg -> agg.update(updater)) .call(); logger().info(“success to update for {} using parm {}”, id, updater); } @Transactional public void enable(@Description(“主键”) Long id) { NewsCategory result = updaterFor(this.getNewsCategoryRepository()) .publishBy(getDomainEventBus()) .id(id) .update(agg -> agg.enable()) .call(); logger().info(“success to enable for {} using parm “, id); } @Transactional public void disable(@Description(“主键”) Long id) { NewsCategory result = updaterFor(this.getNewsCategoryRepository()) .publishBy(getDomainEventBus()) .id(id) .update(agg -> agg.disable()) .call(); logger().info(“success to disable for {} using parm “, id); }}该类中包含我们想要的所有实现。3.2.4. 构建 NewsCategoryControllerNewsInfoApplication 构建完成后,新建 NewsCategoryController 将其暴露出去。新建 NewsCategoryController, 如下:@RequestMapping(“news_category”)@RestControllerpublic class NewsCategoryController extends BaseNewsCategoryController{}是的,核心逻辑都在自动生成的 BaseNewsCategoryController 中:abstract class BaseNewsCategoryController { @Autowired private NewsCategoryApplication application; protected NewsCategoryApplication getApplication() { return this.application; } @ResponseBody @ApiOperation( value = “”, nickname = “create” ) @RequestMapping( value = “/_create”, method = RequestMethod.POST ) public ResultVo<NewsCategory> create(@RequestBody NewsCategoryCreator creator) { return ResultVo.success(this.getApplication().create(creator)); } @ResponseBody @ApiOperation( value = “”, nickname = “update” ) @RequestMapping( value = “{id}/_update”, method = RequestMethod.POST ) public ResultVo<Void> update(@PathVariable(“id”) Long id, @RequestBody NewsCategoryUpdater updater) { this.getApplication().update(id, updater); return ResultVo.success(null); } @ResponseBody @ApiOperation( value = “”, nickname = “enable” ) @RequestMapping( value = “{id}/_enable”, method = RequestMethod.POST ) public ResultVo<Void> enable(@PathVariable(“id”) Long id) { this.getApplication().enable(id); return ResultVo.success(null); } @ResponseBody @ApiOperation( value = “”, nickname = “disable” ) @RequestMapping( value = “{id}/_disable”, method = RequestMethod.POST ) public ResultVo<Void> disable(@PathVariable(“id”) Long id) { this.getApplication().disable(id); return ResultVo.success(null); } @ResponseBody @ApiOperation( value = “”, nickname = “getById” ) @RequestMapping( value = “/{id}”, method = RequestMethod.GET ) public ResultVo<NewsCategoryDto> getById(@PathVariable Long id) { return ResultVo.success(this.getApplication().getById(id).orElse(null)); }}3.2.5. 数据库准备至此,我们的代码就完全准备好了,现在需要准备建表语句。使用 Flyway 作为数据库的版本管理,在 resources/db/migration 新建 V1.002__create_news_category.sql 文件,具体如下:create table tb_news_category( id bigint auto_increment primary key, name varchar(32) null, status tinyint null, create_time bigint not null, update_time bigint not null, version tinyint not null);3.2.6. 测试至此,我们就完成了 NewsCategory 的开发。执行 maven 命令,启动项目:mvn clean spring-boot:run浏览器中输入 http://127.0.0.1:8090/swagger-ui.html , 通过 swagger 查看我们的成果。可以看到如下当然,可以使用 swagger 进行简单测试。3.3. NewsInfo 建模在 NewsCategory 的建模过程中,我们的主要精力放在了 NewsCategory 对象上,其他部分基本都是框架帮我们生成的。既然框架为我们做了那么多工作,为什么还需要我们新建 NewsCategoryApplication 和 NewsCategoryController呢?答案,需要为复杂逻辑预留扩展点。3.3.1. NewsInfo 建模整个过程,和 NewsCategory 基本一致,在此不在重复,只选择差异点进行说明。NewsInfo 最终代码如下:@EnableGenForAggregate@Index(“categoryId”)@Data@Entity@Table(name = “tb_news_info”)public class NewsInfo extends JpaAggregate { @Column(name = “category_id”, updatable = false) private Long categoryId; @Setter(AccessLevel.PRIVATE) @Convert(converter = CodeBasedNewsInfoStatusConverter.class) private NewsInfoStatus status; private String title; private String content; private NewsInfo(){ } /* * GenApplicationIgnore 创建 BaseNewsInfoApplication 时,忽略该方法,因为 Optional<NewsCategory> category 需要通过 逻辑进行获取 * @param category * @param creator * @return / @GenApplicationIgnore public static NewsInfo create(Optional<NewsCategory> category, NewsInfoCreator creator){ // 对 NewsCategory 的存在性和状态进行验证 if (!category.isPresent() || category.get().getStatus() != NewsCategoryStatus.ENABLE){ throw new IllegalArgumentException(); } NewsInfo newsInfo = new NewsInfo(); creator.accept(newsInfo); newsInfo.init(); return newsInfo; } public void update(NewsInfoUpdater updater){ updater.accept(this); } public void enable(){ setStatus(NewsInfoStatus.ENABLE); } public void disable(){ setStatus(NewsInfoStatus.DISABLE); } private void init() { setStatus(NewsInfoStatus.ENABLE); }}3.3.1.1. NewsInfo 创建逻辑建模NewsInfo 的创建逻辑中,需要对 NewsCategory 的存在性和状态进行检查,只有存在并且状态为 ENABLE 才能添加 NewsInfo。具体实现如下:/* * GenApplicationIgnore 创建 BaseNewsInfoApplication 时,忽略该方法,因为 Optional<NewsCategory> category 需要通过 逻辑进行获取 * @param category * @param creator * @return */@GenApplicationIgnorepublic static NewsInfo create(Optional<NewsCategory> category, NewsInfoCreator creator){ // 对 NewsCategory 的存在性和状态进行验证 if (!category.isPresent() || category.get().getStatus() != NewsCategoryStatus.ENABLE){ throw new IllegalArgumentException(); } NewsInfo newsInfo = new NewsInfo(); creator.accept(newsInfo); newsInfo.init(); return newsInfo;}该方法比较复杂,需要我们手工处理。在 NewsInfoApplication 中手工添加创建方法:@GenController(“com.geekhalo.ddd.lite.demo.controller.BaseNewsInfoController”)public interface NewsInfoApplication extends BaseNewsInfoApplication{ // 手工维护方法 NewsInfo create(Long categoryId, NewsInfoCreator creator);}在 NewsInfoApplicationImpl 添加实现:@Autowiredprivate NewsCategoryRepository newsCategoryRepository;@Overridepublic NewsInfo create(Long categoryId, NewsInfoCreator creator) { return creatorFor(getNewsInfoRepository()) .publishBy(getDomainEventBus()) .instance(()-> NewsInfo.create(this.newsCategoryRepository.getById(categoryId), creator)) .call();}其他部分不需要调整。3.3.2. NewsInfo 查找逻辑建模查找逻辑设计两个部分:根据 categoryId 进行分页查找;禁用的 NewsInfo 在查找中不可见。3.3.2.1. Index 注解在 NewsInfo 类上多了一个 @Index(“categoryId”) 注解,该注解会在 BaseNewsInfoRepository 中添加以 categoryId 为维度的查询。interface BaseNewsInfoRepository extends SpringDataRepositoryAdapter<Long, NewsInfo>, Repository<NewsInfo, Long>, QuerydslPredicateExecutor<NewsInfo> { Long countByCategoryId(Long categoryId); default Long countByCategoryId(Long categoryId, Predicate predicate) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QNewsInfo.newsInfo.categoryId.eq(categoryId));; booleanBuilder.and(predicate); return this.count(booleanBuilder.getValue()); } List<NewsInfo> getByCategoryId(Long categoryId); List<NewsInfo> getByCategoryId(Long categoryId, Sort sort); default List<NewsInfo> getByCategoryId(Long categoryId, Predicate predicate) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QNewsInfo.newsInfo.categoryId.eq(categoryId));; booleanBuilder.and(predicate); return Lists.newArrayList(findAll(booleanBuilder.getValue())); } default List<NewsInfo> getByCategoryId(Long categoryId, Predicate predicate, Sort sort) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QNewsInfo.newsInfo.categoryId.eq(categoryId));; booleanBuilder.and(predicate); return Lists.newArrayList(findAll(booleanBuilder.getValue(), sort)); } Page<NewsInfo> findByCategoryId(Long categoryId, Pageable pageable); default Page<NewsInfo> findByCategoryId(Long categoryId, Predicate predicate, Pageable pageable) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QNewsInfo.newsInfo.categoryId.eq(categoryId));; booleanBuilder.and(predicate); return findAll(booleanBuilder.getValue(), pageable); }}这样,并解决了第一个问题。3.3.2.2. 默认方法查看 NewsInfoRepository 类,如下:@GenApplicationpublic interface NewsInfoRepository extends BaseNewsInfoRepository{ default Page<NewsInfo> findValidByCategoryId(Long categoryId, Pageable pageable){ // 查找有效状态 Predicate valid = QNewsInfo.newsInfo.status.eq(NewsInfoStatus.ENABLE); return findByCategoryId(categoryId, valid, pageable); }}通过默认方法将业务概念转为为数据过滤。3.3.3. NewsInfo 数据库准备至此,整个结构与 NewsCategory 再无区别。最后,我们添加数据库文件 V1.003__create_news_info.sql :create table tb_news_info( id bigint auto_increment primary key, category_id bigint not null, status tinyint null, title varchar(64) not null, content text null, create_time bigint not null, update_time bigint not null, version tinyint not null);3.3.4. NewsInfo 测试启动项目,进行简单测试。4. 总结你用了多长时间完成整个系统呢?项目地址见:https://gitee.com/litao851025… ...

February 22, 2019 · 8 min · jiezi