代码拉取完成,页面将自动刷新
一、表结构设计
1:用户表
用户ID,account,姓名,用户余额(balance )
2:商户表
商户ID,account,名称,余额
3:商品表
商品ID,名称,单价
4:库存表
ID,商户ID,商品ID,数量
5:购物车表(需不需要像订单一样,做成两张表)
ID,用户ID,商品ID,数量,价格
用户ID,商品ID,数量,价格
...
6:订单表
订单No,用户ID,订单状态,下单时间,总价
7:订单详情表
ID,订单ID,商品ID,数量,单价
ID,订单ID,商品ID,数量,单价
...
二、用户下单操作流程
2.1:用户选择商品,加入购物车
需要查库存:(不扣减库存),在购物车表新增一条记录,可以增加多条记录,(用户服务)
用户下单:根据购物车的商品列表,去查询库存表,并扣除相应库存,然后生成订单号,计算订单总额度,写入订单表和订单详情表;
用户下单分布式事务,分为 库存服务、订单服务(生成单号,写入订单表,订单详情表)
用户付款:查询订单表,扣除用户表的额度,查询订单详情表,计算每个商户的额度,并增加每个商户的额度;
用户付款分布式事务:用户服务(扣款),商户服务(根据订单详情,分别加款),在调用订单服务(更改订单状态)
三.服务(按照动作行为)
1:购物车服务
包括商品放入购物车,商品移除购物车,清空购物车
2:库存服务
新增库存,扣减库存,查询库存
3:下单服务
生成订单,清空购物车;扣减库存
4:用户服务
扣款,加款
5:商户服务
扣款,加款
二、项目结构及模块设计
2.1:respository 里面有dao,进行数据库的查询,实体对象和值对象的封装,实体对象可以有业务,但是值对象不能有业务,值对象一旦构建好后,就是只读的,
在订单领域,address是值对象,在用户领域,address是一个实体,他们可以共用一张表,但是在代码里,值对象没有业务,这里需要注意下
这块用dubbo来做,(我是想把这块单独一个项目做,但是值对象和实体都有他构建,就会把实体暴露给其他模块...,视频里面讲的是仓库直接返回的实体对象,我这里改下,仓库融合在领域模块,仓库通过dubbo调用数据库,查询po,将po组装成业务对象)
其实 respository 写一份就好了,到时候启动时用jenkins做多个部署
2.2:用户选择商品,
添加购物车:查看库存,
下单,付款
撤销订单,
退款
2.3:商品服务,新增商品,删除商品,商品扣减,商品增加数量
2.4:用户下单流程
后期增加流程:用户下单,增加积分,发送短信,购物时扣减积分抵扣额度
三.bean和值对象的构建
约定:仓库层返回po,在领域层构建对象.领域层通过dubbo调用仓库,将仓库返回的po封装成do
参数传递:
1:在controller中,要构建值对象,用于接收前台传来的参数,这些对象方到对应的controller项目中 ?????????
2:所有的领域服务service都用 fegin 来处理,传的参数放到 fegin 发布的api模块中,返回参数也放到 fegin 的api中
2.1: controller调用 领域服务的 fegin ,fegin 是一个领域service,service 调用领域对象,领域对象和值对象的构建,有工厂来完成,修改由仓库调用,工厂和仓库通过dubbo调用数据层(返回po)
3:repository 通过dubbo调用 查数据库的代码,查询出po,在 repository 中组装成 业务对象(eg:其实这步比较多余.repository 不用通过dubbo去查数据库,我是为了使用dubbo才这么干的)
参数返回:
1:controller返回的参数,放到controller中 (页面访问controller的入参和出参)
2:controller调用 应用服务(里面注入的有多个领域服务), 入参出参有应用服务提供,应用服务采用fegin 来提供, ??? 这块可能不合适,待会研究下 fegin
3:fegin 是应用服务 ,应用服务里面注入多个领域服务,应用服务没有什么业务核心.只是对领域服务按照业务进行的简要逻辑封装.并调用领域服务
4:领域服务(每一个都是独立自治的领域,互不侵犯),里面的实体和值对象,由仓储层提供,仓储层通过dubbo 去调用 数据库,将查询出的po封装成领域对象 do
4.1:领域:领域下面的业务实体,都是独立自治的,如果实体需要另一个领域的实体数据,那么另一个领域的实体数据,作为第一个领域的值对象(只能进行读取,不能进行修改)
4.2:领域服务:进行该领域下的业务编排,逻辑控制;
4.3:应用服务层:可跨领域调用,注入多个领域服务对象;
5:实体和值对象的构建:仓储层负责构建实体和值对象. repository 通过dubbo调用 查数据库的代码,查询出po,在 repository 中组装成 业务对象(eg:其实这步比较多余.repository 不用通过dubbo去查数据库,我是为了使用dubbo才这么干的)
思考:这样怎么做ddd?领域呢?
电商系统,核心领域至少有【商品】,【订单】,【用户】,【物流】等;
大家可以回头看看自己所开发过的项目。 但凡是有状态字段的类,很大可能都是整个项目的核心领域之一。
其实很好理解,因为它有流程,因为它需要被各类操作来变更它的状态,所以,他很可能贯穿了这个项目中某一个关键商业逻辑
再思考一个常见的例子:账户转账 应该是这样 account.transfer(otherAccount)吗? 这里,会有点争议,但我会提倡用AccountTransferService来做,因为虽然这个操作从宏观上来说是属于一个领域聚合,但是,这个操作,却是完全不同的对象! 是同时修改了两个对象的数据,而且自然要求完整的事务性。 所以,这种场景,也可以理解为,Service,是处理跨聚合对象。
在进行业务开发时,Set能调用的地方只有1个,那是就在service中!
一些解释及文章:
领域,聚合,聚合根
聚合之间相互引用:引用id即可,如果不行可以持有一个多ID的对象; see:https://blog.csdn.net/weixin_33885676/article/details/90615107?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-5&spm=1001.2101.3001.4242
因为聚合根意味着是某个聚合的根,而聚合又代表着某个上下文边界,而一个上下文边界又代表着某个独立的业务场景,这个业务场景操作的唯一对象总是该上下文边界内的聚合根
https://www.cnblogs.com/laozhang-is-phi/p/9916785.html
比如创建一个订单,必然会生成订单详情,订单详情肯定会有商品信息,我们在修改商品信息的时候,肯定就不能影响到这个订单详情中的商品信息。再比如:用户在下单的时候,会选择一个地址作为邮寄地址,如果该用户立刻下另一个订单,
并对自己个人中心的地址进行修改,肯定就不能影响刚刚下单的邮寄地址信息。这个时候,聚合就有很强的作用,通过值对象保证了对象之间的一致性。我们平时在开发的时候,
虽然没有用到DDD,肯定也是经常用到聚合,就比如上边的问题,撇开DDD不谈,就平时来说,你肯定不会把商品 id 直接绑定到订单详情表中,为外键的,不然会死得很惨。这个时候其实我们就有一些聚合的概念了,
因为什么呢,下单的时候,我们关注订单领域模型,修改商品的时候,我们关注商品领域模型,这些就是我们说到的聚合,当然一个上下文会有很多聚合,而且聚合要尽可能的细分,那如何正确的区分聚合,以及以什么为基准,请往下看
https://javaedge.blog.csdn.net/article/details/108922408
实体(Entity)和值对象(ValueObject)组成聚合(Aggregate),再根据业务将多个聚合划定到同一限界上下文(Bounded Context),并在限界上下文内完成领域建模
https://blog.csdn.net/C_envelope/article/details/87920146
cqrs
https://www.jdon.com/44815
简单理解:聚合是一堆业务对象的相互持有,聚合根是外部访问的唯一入口,聚合中的业务对象要有领域方法,在写一个service,整合这些聚合根,来完成业务.
实体里面有该实体相对应的核心业务逻辑,service只是简单整合这些实体
基于领域分析设计的架构规范-领域分析基础 (系列文章,记得多搜下)
1:https://blog.csdn.net/qianshangding0708/article/details/102550269
2:https://blog.csdn.net/qianshangding0708/article/details/102645392
all:上面的系列文章在这
https://my.oschina.net/u/4006523/blog/3071568
就要从另外一个角度来说了 如果一个命令操作,并且要求是一个完整的事务,修改了多个聚合的数据,那么,需要为这个行为建立一个 Service 而这个Service,不会是一个{领域名称}+Service,而是一个{具体动作}+Service,比如OrderPayService,订单支付
订单领域,库存领域,商品领域,重点看下面的链接,service 的创建,要是一个领域的入口,或者是夸领域,或者是创建,其他的删改查,都在实体里操作
https://my.oschina.net/u/4006523/blog/3071571
puppeteer
spring could alibaba https://gitee.com/siqinbsq/cloud2020/
spring MainApp启动类路径与其子包路径,还有项目自定义依赖的路径关系
es 文档:
https://www.jianshu.com/p/c377477df7fc
https://www.cnblogs.com/ljhdo/p/4486978.html
https://www.jianshu.com/p/4646d2b26b49
https://blog.csdn.net/qdboi/article/details/109374297?
https://segmentfault.com/a/1190000015220491
redis:https://www.cnblogs.com/zhonglongbo/p/13128955.html
https://www.jianshu.com/p/2ec2dfe7119b
秒杀之发现热点数据:https://blog.csdn.net/cxy_19891109/article/details/87480515
rocketmq集群模式:https://www.jianshu.com/p/819c9719f608
mysql集群:
es集群:
redis集群:
============开干==========
电商系统,核心领域至少有【商品】,【订单】,【用户】
repository 操作数据库的用 dubbo;
商品服务,订单服务,用户服务: 用nacos,相当于 spring could alibaba
face:主管页面,写controller,调用service,也可以调用 聚合根
resoprity 用dubbo模块代替
vo 用于展示
dto: 展示层和servce的通讯载体,注:这个传输通常指的前后端之间的传输,DTO本身的一个隐含的意义是要能够完整的表达一个业务模块的输出。如果服务和服务之间相对独立,那就可以叫DTO;如果服务和服务之间不独立,
每个都不是一个完整的业务模块,拆开可能仅仅是因为计算复杂度或者性能的问题,那这就不能够叫做DTO,只能是BO。
do: 领域对象,服务层根据dto创建do,调用 DO 的业务方法完成具体业务。 ①阿里巴巴的开发手册中的定义,DO( Data Object)这个等同于上面的PO
po: 服务层把do转换成po,进行持久化
https://blog.csdn.net/org_hjh/article/details/114500487
https://zhuanlan.zhihu.com/p/264675395
ddd实战课规模:https://my.oschina.net/u/4101481/blog/3163308
https://blog.csdn.net/wwd0501/article/details/95062535
****
先把po放到dubbo的api里,在service层,或者domain层进行po和do的转换
git ddd code https://gitee.com/jhll/leave-sample/tree/master/src/main/java/ddd/leave
代码结构:
application/service
LeaveApplicationService:里面引用的有领域service(leaveDomainService)
domain:(po与领域对象的操作)
leave(领域):
entity:该包下有值对象,聚合根
event:有事件对象
service:领域服务,操作该领域下的聚合根和值对象(引入leaveRepositoryInterface 和 事件对象,聚合根的创建对象);LeaveFactory(领域对象与po互转)
repository:
facade :LeaveRepositoryInterface(接口)
mapper:dao,查询数据库
persistence:实现 facade,注入mapper,进行增删改查
po:数据库映射对象
infrastructure
client:
Feign.java Feign的客户端
common/api
response.java 公用的返回数据对象
event
领域事件和EventPublisher 发送mq
util:
一些工具类
interfaces
assembler:
dto与do互转
facade:
对外提供服务的api,里面有 对应聚合根的service(leaveApplicationService),该包下的代码有controller
dto:
dto对象
//普通的update: <controller>PersonApi.update(dto) -> PersonApplicationService.update(dto 转 do) -> personDomainService.update(do) -> PersonRepositoryImpl.update(do 转 po) -> PersonDao.save(po) 入库
//删除person: <controller>PersonApi.delete(dto 或者id ) -> personApplicationService.deleteById(id) -> personDomainService.deleteById(id) {personRepository根据id查找po,personFactory根据po创建实体}; -> entry.distable(){实体方法,禁用} -> personRepository.update(do 转po) -> personDao.save(po) 入库
controller 接收dto,在调用applicationService时转换成do,然后调用domin,调用reopsity,在调用mapper时转换成po
美团ddd https://tech.meituan.com/2017/12/22/ddd-in-practice.html
值对象:在实践中,需要保证值对象创建后就不能被修改,即不允许外部再修改其属性。在不同上下文集成时,会出现模型概念的公用,
如商品模型会存在于电商的各个上下文中。在订单上下文中如果你只关注下单时商品信息快照,那么将商品对象视为值对象是很好的选择。
注意上面,商品一定是个值对象,不能是领域对象,要注意设计
设计小聚合:大部分的聚合都可以只包含根实体,而无需包含其他实体。即使一定要包含,可以考虑将其创建为值对象
通过唯一标识来引用其他聚合或实体:当存在对象之间的关联时,建议引用其唯一标识而非引用其整体对象。如果是外部上下文中的实体,引用其唯一标识或将需要的属性构造值对象。
如果聚合创建复杂,推荐使用工厂方法来屏蔽内部复杂的创建逻辑。
边界内的内容具有一致性:在一个事务中只修改一个聚合实例。如果你发现边界内很难接受强一致,不管是出于性能或产品需求的考虑,应该考虑剥离出独立的聚合,采用最终一致的方式.
聚合内部多个组成对象的关系可以用来指导数据库创建,但不可避免存在一定的抗阻。如聚合中存在List<值对象>,那么在数据库中建立1:N的关联需要将值对象单独建表,此时是有id的,建议不要将该id暴露到资源库外部,对外隐蔽。
领域服务:一些重要的领域行为或操作,可以归类为领域服务。它既不是实体,也不是值对象的范畴。当我们采用了微服务架构风格,一切领域逻辑的对外暴露均需要通过领域服务来进行。如原本由聚合根暴露的业务逻辑也需要依托于领域服务。
我们将领域行为封装到领域对象中,将资源管理行为封装到资源库中,将外部上下文的交互行为封装到防腐层中。此时,我们再回过头来看领域服务时,能够发现领域服务本身所承载的职责也就更加清晰了,即就是通过串联领域对象、资源库和防腐层等一系列领域内的对象的行为,对其他上下文提供交互的接口。
模块(Module)是DDD中明确提到的一种控制限界上下文的手段,在我们的工程中,一般尽量用一个模块来表示一个领域的限界上下文; 上下文不是单独叫的(叫领域界限上下文)
一般的工程中包的组织方式为{com.公司名.组织架构.业务.上下文.*},这样的组织结构能够明确的将一个上下文限定在包的内部。
对于模块内的组织结构,一般情况下我们是按照领域对象、领域服务、领域资源库、防腐层等组织方式定义的。
https://my.oschina.net/yunqi/blog/4983080
Cache友好代码
一段Cache友好代码往往运行速度较快。但我们需要注意以下两点:
尽可能多的重复使用一个数据(时间局限性)【如果我们需要在某个任务多次使用一个数据时,应该尽可能的一次性使用完,利用了数据的局部性特点】
尽可能跨距为1的访问数据(空间局部性)【在访问一个数据时,应该依次的访问数组元素,不要跳着访问,利用了数据的空间局限性】
有的业务需要多张表组合成对象,这时候,最好在service层用代码进行赋值(不要单纯理解为set,要有具体的动作意义),最好不要在repository里连表关联
repository 只做基础的单表查询
dto to do
**********注:本来ddd是一个领域服务,作为单独的一个项目部署的,这里为了使用dubbo来构建仓储层,所以将领域的仓储层全部抽出来,作为仓储层
2:明天开搞,边搞边看,从订单入手
ddd电商:https://www.cnblogs.com/Zachary-Fan/p/5991674.html
①合作关系(Partnership):如果2个限界上下文的团队要么一起成功,要么一起失败,此时就是这种关系。应该为相互关联的软件功能制定好计划表,这样可以确保这些功能在同一个发布中完成。
②共享内核(Shared Kernel):对模型和代码的共享将产生一种紧密的依赖性,对于设计来说,这种依赖性可好可坏。我们需要为共享的部分模型指定一个显式边界,并保持共享内核的小型化。共享内核具有特殊的状态,
在没有与另一个团队协商的情况下,这种状态是不能改变的。我们应该引入一种持续集成过程来保证共享内核与通用语言的一致性。【简单的说就是数据库共享】
③客户方——供应方(Customer-Supplier Development):当2个团队处于一种上游——下游关系时,上游团队可能独立于下游团队完成开发,此时下游团队的开发可能会受到很大的影响。
因此,在上游团队的计划中,我们应该顾及到下游团队的需求。
④遵奉者(Conformist):在存在上游——下游关系的2个团队中,如果上游团队已经没有动力提供下游团队之需,下游团队便孤军无助了。处于利他主义,上游团队可能向下游团队做出种种承诺,但是有很大的可能是:这些承诺是无法实现的。
下游团队只能盲目地使用上游团队模型。
⑤防腐层(Anticorruption Layer):在集成2个设计良好的限界上下文时,翻译层可能很简单,甚至可以很优雅的实现。但是,当共享内核,合作关系或客户方——供应方关系无法顺利实现时,此时的翻译将变得复杂。
对于下游客户来说,你需要根据自己的领域模型创建一个单独的层,该层作为上游系统的委派向你的系统提供功能。防腐层通过已有的接口与其他系统交互,而其他系统只需要做很小的修改,甚至无需修改。在防腐层内部,它在你自己的模型和他方模型之间进行翻译转换。【为每个防腐层定义相应的领域服务】
⑥开放主机服务(Open Host Service):定义一种协议,让你的子系统通过该协议来访问你的服务。并且需要将协议公开。
⑦发布语言(Published Language):在2个限界上下文之间翻译模型需要一种公用的语言。此时你应该使用一种发布出来的共享语言来完成集成交流。发布语言通常与开放主机服务一起使用。
⑧另谋他路(SeparateWay):在确定需求时,我们应该做到坚持彻底。如果2套功能没有显著的关系,那么它们是可以被完全解耦的。集成总是昂贵的,有时带给你的好处也不大。
声明2个限界上下文之间不存在任何关系,这样使得开发者去另外寻找简单的、专门的方法来解决问题。
⑨大泥球(Big Ball of Mud):当我们检查已有系统时,经常会发现系统中存在混杂在一起的模型,它们之间的边界是非常模糊的。此时你应该为整个系统绘制一个边界,然后将其归纳在大泥球范围之列。
在这个边界内,不要试图使用复杂的建模手段来化解问题。同时,这样的系统有可能会向其他系统蔓延,应该对此保持警觉。;
代码臃肿的例子流程,及付款的优惠券等的经验
https://blog.csdn.net/weixin_48182198/article/details/109963659
https://developer.aliyun.com/article/716908
https://blog.csdn.net/hz_940611/article/details/103020752?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.essearch_pc_relevant&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.essearch_pc_relevant
https://blog.csdn.net/chuhe0321/article/details/100614404?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-5.essearch_pc_relevant&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-5.essearch_pc_relevant
https://blog.csdn.net/maoyeqiu/article/details/112788122?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_title~default-1.control&spm=1001.2101.3001.4242
注意应用层和领域层该放哪些东西:比如购物车付款,这是一个应用行为,放到应用层;而这个行为又包括 金额计算,支付,生成支付订单,发放短信,积分计算等,这些环节可以理解为领域行为(是不是支撑域),金额计算,支付,生产订单,发短信,积分计算,为什么不作为应用层呢,
在这里,业务核心是付款;如果业务核心是金额计算(可能包含各种优惠等),那么金额计算也会被当成另外一个应用层;
从技术上讲,Repository和DAO所扮演的角色相似,不过DAO的设计初衷只是对数据库的一层很薄的封装,而Repository是更偏向于领域模型
在所有的领域对象中,只有聚合根才“配得上”拥有Repository,而DAO没有这种约束。DDD 中的仓储主要有两个核心功能:1. 持久化实体: 将实体持久化到底层的数据库中;2. 重建实体: 实体持久化后当我们需要再次使用的时候需要从数据库的数据重建实体。
Repository在DDD中用于解决实体模型和持久化逻辑之间的差异性,为业务逻辑屏蔽底层存储的具体细节,这时候业务模型是不感知底层数据表的,只关心自己的业务
https://blog.csdn.net/roykingw/category_10999884.html ddd系列文章(已付款)
聚合与聚合之间,也需要以聚合根的ID来互相关联,接收外部任务和请求。也就是说如果需要访问其他聚合的实体,就要先访问聚合根,再导航到聚合内部实体。外部对象不能直接访问聚合内实体。例如我们上面例举了订单和用户管理两个聚合,订单聚合中的聚合根就是订单实体,而用户管理聚合中的聚合根就是用户实体。如果需要在用户管理中获取用户所有交易过的商户,就必须要通过访问订单实体来获取,
而不能越过订单实体,直接查询对应的商户信息。这样带来的好处是显而易见的,当聚合内部的业务逻辑反生变更时,只与聚合内部有关,只需要对聚合内部进行更新,与外部程序无关,这样就降低了变更时的维护成本,提高了系统的设计质量。
在设计聚合和聚合根时,要注意的是,跨聚合的服务调用需要通过应用层的领域服务来实现,而不能在领域模型中直接完成。
这样有助于减少聚合之间的耦合,便于未来以聚合为单位进行微服务拆分。而且,要尽量减少跨聚合的领域服务调用,如果一个事务需要修改多个聚合的状态,
那应该采用事件驱动的方式异步进行修改,这样有助于实现聚合之间的解耦。最后,这些聚合和聚合根的限制都只是指导性的,并不是强制性的,所以如果考虑到项目的具体情况,
聚合之间的这些限制也不是不可以突破的,总之一切都要以解决实际问题为出发点。
ddd只适合增删改操作,至于查询,用其他办法,但是有疑问:比如订单实体的构建,需要地址实体,商品实体等,这两个实体或者值对象,怎么查询?通过仓储层查询赋值,repository 调用 各个dao
ddd架构与演变:https://blog.csdn.net/qq_32828253/article/details/11067320
识别限界上下文:https://zhuanlan.zhihu.com/p/141611842
通用子域,协作子域,是怎么供核心子域调用的?
从壹开始ddd :https://www.cnblogs.com/laozhang-is-phi/p/9832684.html
arthas jvm监控
spring could 全家桶
http://c.biancheng.net/view/5310.html
Sentinel
https://www.cnblogs.com/crazymakercircle/p/14285001.html
https://www.jianshu.com/p/c47dfd25eeee
ddd
Domain Primitive
https://blog.csdn.net/weixin_42596778/article/details/118938934
https://segmentfault.com/a/1190000020270851?utm_source=tag-newest
https://developer.aliyun.com/article/716908
https://developer.aliyun.com/article/713097
让我们重新来定义一下 Domain Primitive :Domain Primitive 是一个在特定领域里,拥有精准定义的、可自我验证的、拥有行为的 Value Object 。
可枚举的 int :比如 Status(一般不用Enum因为反序列化问题)
无状态的业务逻辑: 比如手机号11位的校验,或者说取手机号的前三位;
订单,支付,商城,物流,用户;
该项目中增加平台监管,现在想到的功能是:用户付款,先转账至平台,确认收货后由平台转账至商户;
DP的第一个原则:将隐性的概念显性化
DP的第一个原则:将 隐性 的 上下文 显性化
DP的第一个原则:封装 多对象 行为
Domain Primitive的定义:
Domain Primitive是一个在特定领域里,拥有精准定义的、可自我验证的、拥有行为的Value Object。
DP是一个传统意义上的Value Object,拥有Immutable的特性
DP是一个完整的概念整体,拥有精准定义
DP使用业务域中的原生语言
DP可以是业务域的最小组成部分、也可以构建复杂组合
Domain Primitive是Value Object的进阶版,在原始VO的基础上要求每个DP拥有概念的整体,而不仅仅是值对象。在VO的Immutable基础上增加了Validity和行为。当然同样的要求无副作用(side-effect free)
Domain Primitive和Data Transfer Object (DTO)的区别:
DTO DP
功能 数据传输属于技术细节 代表业务域中的概念
数据的关联 只是一堆数据放在一起,不一定有关联度 数据之间的高相关性
行为 无行为 丰富的行为和业务逻辑
什么情况下应该用Domain Primitive
常见的DP的使用场景包括:
有格式限制的String:比如Name,PhoneNumber,OrderNumber,ZipCode,Address等
有限制的Integer:比如OrderId(>0),Percentage(0-100%),Quantity(>=0)等
可枚举的int:比如Status(一般不用Enum因为反序列化问题)
ouble或BigDecimal:一般用到的Double或BigDecimal都是有业务含义的,比如Temperature、Money、Amount、ExchangeRate、Rating等
复杂的数据结构:比如Map<String, List<Integer>>等,尽量能把Map的所有操作包装掉,仅暴露必要行为;
殷浩详解DDD系列 第二讲 应用架构 (一个转账服务为案例)
https://developer.aliyun.com/article/715802;
https://developer.aliyun.com/article/758292
reporsitory只能通过聚合根去操作数据库,更改聚合根下的某个实体,这样写,会有很多无用的写操作,可以通过Change-Tracking 变更追踪 避免,基于Snapshot的方案
通过快照在内存中比较来避免掉,但是会消耗过多的内存;第二种是加动态代理,但是逻辑比较复杂,如果有list,则更难处理,目前业界都是用快照内存比较的方式进行的;
Snapshot方案的好处是比较简单,成本在于每次保存时全量Diff的操作(一般用Reflection),以及保存Snapshot的内存消耗。
Proxy方案的好处是性能很高,几乎没有增加的成本,但是坏处是实现起来比较困难,且当有嵌套关系存在时不容易发现嵌套对象的变化(比如子List的增加和删除等),有可能导致bug。
由于Proxy方案的复杂度,业界主流(包括EF Core)都在使用Snapshot方案。这里面还有另一个好处就是通过Diff可以发现哪些字段有变更,然后只更新变更过的字段,再一次降低UPDATE的成本。
在这里我简单贴一下我们自己Snapshot的实现,代码并不复杂,每个团队自己实现起来也很简单,部分代码仅供;
https://mp.weixin.qq.com/s/NoRTUSovcO2Yz237k0ceIw#at
阿里讲ddd系列
https://zhuanlan.zhihu.com/p/366395817
ECS的一些核心性能优化包括将同类型组件放在同一个Array中,然后Entity仅保留到各自组件的pointer,
这样能更好的利用CPU的缓存,减少数据的加载成本,以及SIMD的优化等。
我们可以利用enum的PlayerClass、MonsterClass来代替继承关系,后续也可以利用Type Object设计模式来做到数据驱动。;
【建议】在有些简单场景里,有时候确实可以比较随意的设置一个值而不会导致不一致性,也建议将方法名重新写为比较“行为化”的命名,会增强其语意。比如setPosition(x, y)可以叫做moveTo(x, y),setAddress可以叫做assignAddress等。
代码快速失败,是抛异常还是返回null?
IllegalStateException
通过聚合根保证主子实体的一致性
在稍微复杂一点的领域里,通常主实体会包含子实体,这时候主实体就需要起到聚合根的作用,即:
子实体不能单独存在,只能通过聚合根的方法获取到。任何外部的对象都不能直接保留子实体的引用
子实体没有独立的Repository,不可以单独保存和取出,必须要通过聚合根的Repository实例化
子实体可以单独修改自身状态,但是多个子实体之间的状态一致性需要聚合根来保障;
不可以强依赖其他聚合根实体或领域服务
一个实体的原则是高内聚、低耦合,即一个实体类不能直接在内部直接依赖一个外部的实体或服务。这个原则和绝大多数ORM框架都有比较严重的冲突,所以是一个在开发过程中需要特别注意的。这个原则的必要原因包括:对外部对象的依赖性会直接导致实体无法被单测;以及一个实体无法保证外部实体变更后不会影响本实体的一致性和正确性。
所以,正确的对外部依赖的方法有两种:
只保存外部实体的ID:这里我再次强烈建议使用强类型的ID对象,而不是Long型ID。强类型的ID对象不单单能自我包含验证代码,保证ID值的正确性,同时还能确保各种入参不会因为参数顺序变化而出bug。具体可以参考我的Domain Primitive文章。
针对于“无副作用”的外部依赖,通过方法入参的方式传入。比如上文中的equip(Weapon,EquipmentService)方法;
ddd第四讲
仔细品读,理解:https://mp.weixin.qq.com/s?__biz=MzAxNDEwNjk5OQ==&mid=2650414919&idx=1&sn=0ad1df1a1b0e2488f7faa21008fdbdd0&chksm=8396d75fb4e15e49341b07022780dcb8dca66a0efb7f129d4de86a5ef5d8a890f6e0d2fd6432&scene=178&cur_album_id=1452661944472977409#rd
领域事件是在DDD里,比较推荐使用的跨实体“副作用”传播机制
ddd第五讲:下单链路
https://zhuanlan.zhihu.com/p/366395817
CQE vs DTO
Result vs Exception:
最后,上文曾经提及在Interface层应该返回Result,在Application层应该返回DTO,在这里再次重复提出规范:
Application层只返回DTO,可以直接抛异常,不用统一处理。所有调用到的服务也都可以直接抛异常,除非需要特殊处理,否则不需要刻意捕捉异常;
异常的好处是能明确的知道错误的来源,堆栈等,在Interface层统一捕捉异常是为了避免异常堆栈信息泄漏到API之外,但是在Application层,异常机制仍然是信息量最大,代码结构最清晰的方法,避免了Result的一些常见且繁杂的Result.isSuccess判断。所以在Application层、Domain层,以及Infrastructure层,遇到错误直接抛异常是最合理的方法。;
Change-Tracking 变更追踪
系统常见的常识性代码:
1:参数校验
2:返回值统一
3:统一异常处理
4:鉴权
5:日志
6:缓存,缓存可单独处理
1:Result vs Exception
规范:Interface层的HTTP和RPC接口,返回值为Result,捕捉所有异常规范:Application层的所有接口返回值为DTO,不负责处理异常;
可在interface中利用aop进行拦截,统一处理异常,而在application中,可以尽情的抛异常,但是异常一定是意料之外的
cqrs中,事件驱动发消息不太合适,不利于代码的阅读;
Interface接口层:
鉴权、Session、限流、缓存、日志,返回值和异常处理规范,Result vs Exception
Application层:
ApplicationService应用服务:最核心的类,负责业务流程的编排,但本身不负责任何业务逻辑
DTO Assembler:负责将内部领域模型转化为可对外的DTO
Command、Query、Event对象:作为ApplicationService的入参,最好是一个use case一个Command,独立起来,Command 是指写操作,更新操作,一般不会有太对的参数,不会涉及到其他领域的查询,如果涉及到,那么其他领域的实体,在这里就是值对象
返回的DTO:作为ApplicationService的出参
ApplicationService的入参是CQE对象,但是出参却是一个DTO
规范:CQE对象的校验应该前置,避免在ApplicationService里做参数的校验。可以通过JSR303/380和Spring Validation来实现
防腐层:
防腐层根据第三方api,封装自有的业务对象
https://zhuanlan.zhihu.com/p/115685384,这篇文章上说.command是命令,q是查询,需要返回dto,那么命令中的查询就用repository去查,cqe命令是针对application层的
目前来看:命令中,只要不涉及领域外的实体或聚合根,都可以在仓储层进行查询封装;例子:address,在物流领域,那是一个简单的值对象,则在仓储层根据需要直接查询就行,在用户领域,他就是
实体,需要通过用户领域,进行增删改; 但是,在物流领域,初次拉取address 时,创建物流时,肯定要传入用户的地址,物流地址是物流领域的实体或者值对象,那么,在创建物流时,要通过application
传入物流需要的东西;而用户或者物流领域,之间的调用,要有一个三方领域来融合,此时,是选用command命令驱动还是Event驱动呢,要看调用方是否依赖被调用方的返回值,此时明显不依赖,则采用事件驱动
https://zhuanlan.zhihu.com/p/343388831
有一些非业务状态的变更,可以封装成Domain Primitive和Domain Service,无状态的逻辑Domain Primitive
https://zhuanlan.zhihu.com/p/366395817 各个层的规范
DTO(传输对象):主要作为Application层的入参和出参,比如CQRS里的Command、Query、Event,以及Request、Response等都属于DTO的范畴。
DTO的价值在于适配不同的业务场景的入参和出参,避免让业务对象变成一个万能大对象。
模型对象之间的关系:
1:在实际开发中DO、Entity和DTO不一定是1:1:1的关系。一些常见的非1:1关系如下:复杂的Entity拆分多张数据库表:常见的原因在于字段过多,导致查询性能降低,需要将非检索、大字段等单独存为一张表,提升基础信息表的检索效率。常见的案例如商品模型,将商品详细描述等大字段单独保存,提升查询性能
2:多个关联的Entity合并一张数据库表:这种情况通常出现在拥有复杂的Aggregate Root - Entity关系的情况下,且需要分库分表,为了避免多次查询和分库分表带来的不一致性,牺牲了单表的简洁性,提升查询和插入性能。常见的案例如主子订单模型
3:从复杂Entity里抽取部分信息形成多个DTO:这种情况通常在Entity复杂,但是调用方只需要部分核心信息的情况下,通过一个小的DTO降低信息传输成本。同样拿商品模型举例,基础DTO可能出现在商品列表里,这个时候不需要复杂详情
4:合并多个Entity为一个DTO:这种情况通常为了降低网络传输成本,降低服务端请求次数,将多个Entity、DP等对象合并序列化,并且让DTO可以嵌套其他DTO。同样常见的案例是在订单详情里需要展示商品信息
模型所在模块和转化器:
DTO Assembler:在Application层,Entity到DTO的转化器有一个标准的名称叫DTO Assembler(可以多个entity生成一个DTO);
Repository代码规范:
1:接口名称不应该使用底层实现的语法:我们常见的insert、select、update、delete都属于SQL语法,使用这几个词相当于和DB底层实现做了绑定。相反,我们应该把 Repository 当成一个中性的类 似Collection 的接口,使用语法如 find、save、remove。在这里特别需要指出的是区分 insert/add 和 update 本身也是一种和底层强绑定的逻辑,一些储存如缓存实际上不存在insert和update的差异,在这个 case 里,使用中性的 save 接口,然后在具体实现上根据情况调用 DAO 的 insert 或 update 接口。
2:出参入参不应该使用底层数据格式:需要记得的是 Repository 操作的是 Entity 对象(实际上应该是Aggregate Root),而不应该直接操作底层的 DO 。更近一步,Repository 接口实际上应该存在于Domain层,根本看不到 DO 的实现。这个也是为了避免底层实现逻辑渗透到业务代码中的强保障。
3:应该避免所谓的“通用”Repository模式:很多 ORM 框架都提供一个“通用”的Repository接口,然后框架通过注解自动实现接口,比较典型的例子是Spring Data、Entity Framework等,这种框架的好处是在简单场景下很容易通过配置实现,但是坏处是基本上无扩展的可能性(比如加定制缓存逻辑),在未来有可能还是会被推翻重做。当然,这里避免通用不代表不能有基础接口和通用的帮助类;
4:这里需要再次强调的是Repository的接口是在Domain层,但是实现类是在Infrastructure层。
(po转do在RepositoryImpl里面做),在RepositoryImpl操作的是聚合根,那么大的聚合根有很多小对象需要保存,但是真正需要保存的就一个小对象,那其他的咋办?
分布式事务缺点:
调用方增加表或状态记录调用结果,接收方接口保证幂等性【定时校对】。必要时增加监控
引入可靠消息队列(kafka、RocketMQ),生产者确认消息发送成功后记录发送记录,消费者订阅消息手动提交,同时保证消息处理的幂等性,防止重复消息【可靠MQ】
tcc-transaction,缺点就是代码量翻倍,存在一定的侵入性
当然还有最笨也是最常见的处理方式就是减少分库(规避分布式事务),采用公共服务层或者代码拷贝等方式规避服务与服务之间的调用带来的事务问题
https://zhuanlan.zhihu.com/p/183753774
https://blog.csdn.net/ljheee/article/details/99696571
记录seata做分布式事务时出现多线程同时开启分布式事务出现的错误
seata 踩坑:https://blog.csdn.net/Yunwei_Zheng/article/details/104839881,https://shirenchuang.blog.csdn.net/article/details/106739176
sental 与dubbo,RocketMQ 结合使用,与Hystrix 对比
《比AtomicLong还高效的LongAdder 源码解析》
阿里java开发手册
https://developer.aliyun.com/topic/java20?utm_content=g_1000161792
rocketmq系列:https://zhuanlan.zhihu.com/p/59516998
redis:https://www.cnblogs.com/dw-haung/p/11972708.html
io多路复用:https://zhuanlan.zhihu.com/p/115912936
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。