DDD领域设计架构落地
前言
经历了几年的代码生涯,经常会遇到一个问题,一个公司的业务系统,随着迭代的次数越来越多,代码结构越来越混乱,即便是开始之初结构是如何整洁,也难逃这厄运。
究竟是什么缘由导致的,可以大概概括为如下几点:
- 业务迭代快,工期紧,领导催的急,为了将需求快速落地,无条理地的开发,什么方式快就是用什么方式,就连代码注释和文档都懒的写。
- 开发人员编码习惯与风格不一致
- 人员变动,新来的员工在不熟悉框架的情况下就进行开发
- 团队疏于重构,开发人员根本没有时间重构代码,需求源源不断,哪来时间重构,只能任由代码腐烂下去。
那到底有没有方案彻底解决这几个痛点呢?可能局限于个人眼界,目前没有发现有彻底解决的方案。那么我们退而求其次,是否存在让代码腐烂相对比较缓慢的方案呢。
下面介绍的是一个可落地的,结构清晰的代码架构,让每一位开发人员都遵循这套架构规范,在一定程度上让代码腐烂的尽可能慢一点。
该架构以DDD领域驱动为依据,参考借鉴了DDD分层架构、COLA架构及CQRS架构整合而成的架构。
架构
该架构和cola整体架构极其相似,但是架构内的包含的内容确有不同
读写分离:
之所以采用读写分离的方式,是受CQRS架构的启发。CQRS架构首先将请求分成两类,一种是返回信息但不改变内部状态即QUERY,一种是会改变对象的内部状态,但不返回任何内容,要么只返回元数据即COMMAND。采用这种方式,可以将业务和查询分离。业务只做业务相关的事,查询只做查询相关的事。即便后续业务需要将查询抽离出来,单独作为一个查询服务也能更好的拆分。
根据上述架构,查询请求只需要在application定义查询服务,在infrastructure层实现查询服务即可,减少调用链路长度。
网关隔离:
对系统而言,业务是最具有价值的部分,将业务逻辑抽取出来作为业务层,业务层包括application层【应用层】和domain层【领域层】。与业务交互的请求划分为两类,分别是请求和响应并分配给两个网关进行处理,这两个网关分别是响应网关【adapter层】和请求网关【infrastructure层】。响应网关处理需要系统接收客户端的请求,如web端请求,手机端请求、定时任务请求等。请求网关处理系统需要发送和接收外部系统的请求,如数据库、缓存、mq等。
通过响应网关和请求网关的隔离,避免了外部系统对业务核心的侵入,也使得架构更加清晰。
功能介绍
层次 | 包名 | 功能 |
---|---|---|
Adapter 层 | web | 处理页面请求的 Controller |
Adapter 层 | wireless | 处理无线端的适配 |
Adapter 层 | consumer | 处理外部事件 |
Adapter 层 | scheduler | 处理定时任务 |
Application层 | cmdservice | 处理业务指令逻辑 |
Application层 | qryservice | 处理查询逻辑 |
Domain 层 | entity | 实体模型 |
Domain 层 | ability | 领域能力,包括 DomainService |
Domain 层 | repository | 仓储层 |
Infrastructure 层 | repositoryimpl | 仓储实现 |
Infrastructure 层 | qryserviceimpl | 查询服务实现 |
Infrastructure 层 | mapper | ibatis 数据库映射 |
Infrastructure 层 | config | 配置信息 |
client 层 | api | 服务对外透出的 API |
client 层 | cmd | 服务对外透出的指令请求 |
client 层 | qry | 服务对外透出的查询请求 |
功能详情
如下几层的描述摘自【极客时间-欧创新老师】对分层的见解。
Adapter 层【用户接口层】
用户接口层负责向客户端显示信息和解释客户端指令。这里的客户可能是:用户、程序、自动化测试、定时任务、事件消息和批处理脚本等等。
Application层【应用层】
应用层是很薄的一层,理论上不应该有业务规则或逻辑,主要面向用例和流程相关的操作。但应用层又位于领域层之上,因为领域层包含多个聚合,所以它可以协调多个聚合的服务和领域对象完成服务编排和组合,协作完成业务操作。
此外,应用层也是微服务之间交互的通道,它可以调用其它微服务的应用服务,完成微服务之间的服务组合和编排。这里我要提醒你一下:在设计和开发时,不要将本该放在领域层的业务逻辑放到应用层中实现。因为庞大的应用层会使领域模型失焦,时间一长你的微服务就会演化为传统的三层架构,业务逻辑会变得混乱。
另外,应用服务是在应用层的,它负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果的拼装,以粗粒度的服务通过 API 网关向前端发布。还有,应用服务还可以进行安全认证、权限校验、事务控制、发送或订阅领域事件等。
Domain 层【领域层】
领域层的作用是实现企业核心业务逻辑,通过各种校验手段保证业务的正确性。领域层主要体现领域模型的业务能力,它用来表达业务概念、业务状态和业务规则。
领域层包含聚合根、实体、值对象、领域服务等领域模型中的领域对象。
这里我要特别解释一下其中几个领域对象的关系,以便你在设计领域层的时候能更加清楚。首先,领域模型的业务逻辑主要是由实体和领域服务来实现的,其中实体会采用充血模型来实现所有与之相关的业务功能。其次,你要知道,实体和领域服务在实现业务逻辑上不是同级的,当领域中的某些功能,单一实体(或者值对象)不能实现时,领域服务就会出马,它可以组合聚合内的多个实体(或者值对象),实现复杂的业务逻辑。
Infrastructure 层【基础层】
基础层是贯穿所有层的,它的作用就是为其它各层提供通用的技术和基础服务,包括第三方工具、驱动、消息中间件、网关、文件、缓存以及数据库等。比较常见的功能还是提供数据库持久化。
基础层包含基础服务,它采用依赖倒置设计,封装基础资源服务,实现应用层、领域层与基础层的解耦,降低外部资源变化对应用的影响。
比如说,在传统架构设计中,由于上层应用对数据库的强耦合,很多公司在架构演进中最担忧的可能就是换数据库了,因为一旦更换数据库,就可能需要重写大部分的代码,这对应用来说是致命的。那采用依赖倒置的设计以后,应用层就可以通过解耦来保持独立的核心业务逻辑。当数据库变更时,我们只需要更换数据库基础服务就可以了,这样就将资源变更对应用的影响降到了最低。
client 层【跨服务接口层】
定义该服务对其它服务的接口访问。其它服务只需映入该层的jar包,采用远程调用接口的方式请求接口,实现服务间的交互。
示例
接下来介绍一个简单的房地产的招商业务,并以领域驱动设计的方式来抽象该业务,取其中一个小的功能来介绍使用该架构是如何实现的。
业务介绍
大致业务:
招商团队成员针对没有入驻的商铺,寻找适合入驻该商铺的客户【品牌方】,建立招商任务,进行有效的沟通后,确认该客户有意向入驻后,签署合同。
划分业务领域:
业务领域 | 事件风暴【细化业务】 | |
---|---|---|
商铺 | 商场管理人员,事先将商场的所有商铺录入系统,当商铺信息变更时,会生成变更记录。 | |
招商团队 | 招商负责人创建招商团队,并指定团队负责人及其成员,只有招商团队成员才能开展招商任务 | |
客户 | 招商团队成员寻找到合适的客户【品牌方】后,录入系统,以便后续跟进 | |
招商任务 | 招商团队成员选择待入驻的商铺与有意向入驻该商铺的客户后,创建招商任务,完成招商任务 | |
合同 | 招商任务顺利完成后,需要公司【招商所在单位】与客户【品牌方】签署合同。 |
领域模型:
上述描述的都是关于招商业务,故此领域定为招商领域,将采用商铺业务来讲解架构
聚合 | 聚合根 | 实体 | 值对象 | 事件 | 命令 | 查询 |
---|---|---|---|---|---|---|
商铺 | 商铺 | 商铺、地址、变更记录 | 商铺变更事件 | 新增商铺、编辑商铺、删除商铺 | 查询商铺列表、查看商铺详情、查询变更记录 | |
团队 | 团队 | 团队、负责人、成员 | 新增团队、编辑团队 | 查询团队列表、查看团队详情 | ||
客户 | 客户 | 客户、品牌 | 新增客户、编辑客户 | 查询客户列表、查询客户详情 | ||
任务 | 任务 | 任务、商铺、客户、跟进记录、跟进人 | 新增任务、编辑任务、跟进任务、记录跟进信息 | 查询任务列表、查询跟进记录 |
商铺业务示例
整体模块
父模块包含如下子模块
<modules><module>pango-demo-client</module><module>pango-demo-biz</module><module>pango-demo-adapter</module><module>pango-demo-infrastructure</module><module>pango-demo-start</module></modules>
client层
对其它服务提供访问该服务的接口,并将接口分类为cmd【指令】接口和qry【查询】接口,该层只有接口定义和传输类定义,不包含任何的业务逻辑处理。
包名 | 说明 |
---|---|
cmd | 指令 |
cmd.api | 指令请求接口定义 |
cmd.req | 指令请求传输类 |
cmd.res | 指令结果返回传输类 |
event | 事件,可以是服务内部事件也可以是队列消息 |
qry | 查询 |
qry.api | 查询请求接口定义 |
qry.req | 查询请求传输类 |
qry.res | 查询结果返回传输类 |
商铺指令api:
package com.cloud.pango.client.cmd.api;import com.cloud.pango.client.cmd.req.StoreEditCmd;
import com.cloud.pango.client.cmd.req.StoreSaveCmd;
import com.cloud.pango.client.util.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;@FeignClient(name = "pango-demo", contextId = "storeCmdApi", path = "/store")
public interface IStoreCmdApi {@PostMapping("/save")R saveStore(@RequestBody StoreSaveCmd saveCmd);@PostMapping("/edit")R editStore(@RequestBody StoreEditCmd editCmd);
}
商铺新增传输类:
package com.cloud.pango.client.cmd.req;
import lombok.Data;
import java.math.BigDecimal;@Data
public class StoreSaveCmd {/*** 名称*/private String name;/*** 描述*/private String description;/*** 占用面积*/private BigDecimal area;/*** 楼栋*/private String building;/*** 楼层*/private String floor;/*** 详细地址*/private String address;
}
商铺编辑传输类:
package com.cloud.pango.client.cmd.req;
import lombok.Data;
import java.math.BigDecimal;@Data
public class StoreSaveCmd {/*** 名称*/private String name;/*** 描述*/private String description;/*** 占用面积*/private BigDecimal area;/*** 楼栋*/private String building;/*** 楼层*/private String floor;/*** 详细地址*/private String address;
}
商铺查询API:
package com.cloud.pango.client.qry.api;import com.baomidou.mybatisplus.core.metadata.IPage;
import com.cloud.pango.client.qry.req.StorePageQry;
import com.cloud.pango.client.qry.res.StoreLoadResult;
import com.cloud.pango.client.util.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;@FeignClient(name = "pango-demo", contextId = "storeQryApi", path = "/store")
public interface IStoreQryApi {@GetMapping("/load")R<StoreLoadResult> loadStore(@RequestParam("id") Long id);@GetMapping("/page")R<IPage> pageStore(@RequestBody StorePageQry qry);
}
商铺查询传输类:
package com.cloud.pango.client.qry.req;import lombok.Data;@Data
public class StorePageQry {private Integer pageNo = 1;private Integer pageSize = 10;private String name;private String building;
}
Adapter 层
负责向客户端显示信息和解释客户端指令,实现client层定义的接口,作为响应网关,接收所有外部请求,并对外部请求进行响应,外部请求可以来自于前端、手机端、消息时间、定时任务等。
该层不做任何业务逻辑,可以对接收到的请求进行参数校验和数据组装。
包名 | 说明 |
---|---|
consumer | 接收外部队列消息或事件请求 |
mobile | 接收手机端请求 |
scheduler | 接收定时任务请求 |
web | 接收web端请求,并实现对外暴露的client层的api接口 |
商铺web端控制器:
package com.cloud.pango.web;import com.baomidou.mybatisplus.core.metadata.IPage;
import com.cloud.pango.application.cmd.IStoreCmdService;
import com.cloud.pango.application.qry.IStoreQryService;
import com.cloud.pango.client.cmd.api.IStoreCmdApi;
import com.cloud.pango.client.cmd.req.StoreEditCmd;
import com.cloud.pango.client.cmd.req.StoreSaveCmd;
import com.cloud.pango.client.qry.api.IStoreQryApi;
import com.cloud.pango.client.qry.req.StorePageQry;
import com.cloud.pango.client.qry.res.StoreLoadResult;
import com.cloud.pango.client.qry.res.StorePageResult;
import com.cloud.pango.client.util.R;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;@RequestMapping("/store")
@RestController
public class StoreController implements IStoreQryApi, IStoreCmdApi {@Resourceprivate IStoreQryService storeQryService;@Resourceprivate IStoreCmdService storeCmdService;@Overridepublic R<StoreLoadResult> loadStore(Long id) {StoreLoadResult result = storeQryService.loadStoreById(id);return R.ok(result);}@Overridepublic R<IPage> pageStore(StorePageQry qry) {IPage<StorePageResult> result = storeQryService.pageStore(qry);return R.ok(result);}@Overridepublic R saveStore(StoreSaveCmd saveCmd) {storeCmdService.saveStore(saveCmd);return R.ok();}@Overridepublic R editStore(StoreEditCmd editCmd) {storeCmdService.editStore(editCmd);return R.ok();}
}
biz层
业务逻辑层。专门处理业务逻辑,分为application层及domain层。
application层包含了指令应用服务和查询应用服务,一个聚合往往对应一个指令应用服务和一个查询应用服务,如商铺聚合有商铺、落位和变更记录多个实体,但只会有商铺指令应用服务和商铺查询应用服务,所有的与商铺相关的请求下达到应用层后由商铺指令服务和商铺查询应用服务来接收处理,应用服务层接收到业务请求后调用domain层的领域服务进行业务处理,领域服务再调用实体方法进行业务处理。
如果应用层接收到的是查询请求,则直接调用基础设施层进行数据查询。
领域服务上接应用服务层,下接仓储层,对外暴露领域的服务能力,如商铺的编辑功能、新增功能和保存变更日志功能等,它模拟的是业务领域的能力,往往一个聚合对应各一个领域服务,而该领域服务下的所有方法都需要添加事务管理,以确保聚合的数据一致性。
实体也是有业务操作的,属于充血模型。如商铺的变更数据行为,就是在Store实体中存在一个change的方法用来修改Store的数据。
包名 | 说明 |
---|---|
application | 应用层 |
application.cmd | 指令服务接口定义,一个聚合往往对应一个指令应用服务 |
application.cmd.impl | 指令服务接口实现 |
application.qry | 查询服务接口定义,一个聚合往往对应一个查询应用服务 |
domain | 领域层,定义各个聚合的聚合根、实体、值对象及仓储接口 |
domain.**.ability | 是领域对外暴露的服务能力,一个聚合往往对应一个领域服务,且该领域服务的所有方法都应该加上事务,保证聚合的数据一致性 |
domain.**.entity | 领域实体,一般定义充血的实体 |
domain.**.repository | 定义聚合的仓储接口,一个聚合对应一个仓储接口,以便后续更改实现数据存储方式 |
application层:
将指令业务处理和查询业务分隔开
- 指令业务
商铺指令服务接口
package com.cloud.pango.application.cmd;import com.cloud.pango.client.cmd.req.StoreEditCmd;
import com.cloud.pango.client.cmd.req.StoreSaveCmd;
import com.cloud.pango.client.event.StoreChangeEvent;public interface IStoreCmdService {/*** 保存商铺* @param saveCmd*/void saveStore(StoreSaveCmd saveCmd);/*** 编辑商铺* @param editCmd*/void editStore(StoreEditCmd editCmd);/*** 保存商铺变更日志* @param event*/void saveChangeLog(StoreChangeEvent event);
}
商铺指令服务接口实现类
package com.cloud.pango.application.cmd.impl;import com.cloud.pango.application.cmd.IStoreCmdService;
import com.cloud.pango.client.cmd.req.StoreEditCmd;
import com.cloud.pango.client.cmd.req.StoreSaveCmd;
import com.cloud.pango.client.event.StoreChangeEvent;
import com.cloud.pango.domain.store.ability.StoreDomainService;
import org.springframework.stereotype.Service;import javax.annotation.Resource;@Service
public class StoreCmdServiceImpl implements IStoreCmdService {@Resourceprivate StoreDomainService storeDomainService;@Overridepublic void saveStore(StoreSaveCmd saveCmd) {storeDomainService.createStore(saveCmd.getName(),saveCmd.getDescription(),saveCmd.getArea(),saveCmd.getBuilding(),saveCmd.getFloor(),saveCmd.getAddress());}@Overridepublic void editStore(StoreEditCmd editCmd) {storeDomainService.editStore(editCmd.getId(),editCmd.getName(),editCmd.getDescription(),editCmd.getArea(),editCmd.getBuilding(),editCmd.getFloor(),editCmd.getAddress());}@Overridepublic void saveChangeLog(StoreChangeEvent event) {storeDomainService.createChangeLog(event);}
}
- 查询业务
商铺查询接口
package com.cloud.pango.application.qry;import com.baomidou.mybatisplus.core.metadata.IPage;
import com.cloud.pango.client.qry.req.StorePageQry;
import com.cloud.pango.client.qry.res.StoreLoadResult;
import com.cloud.pango.client.qry.res.StorePageResult;public interface IStoreQryService {/*** 加载商铺详情* @param id* @return*/StoreLoadResult loadStoreById(Long id);/*** 分页查询商铺* @param qry* @return*/IPage<StorePageResult> pageStore(StorePageQry qry);
}
domain层:
商铺实体
package com.cloud.pango.domain.store.entity;import cn.hutool.core.lang.Assert;
import com.baomidou.mybatisplus.annotation.TableName;
import com.cloud.pango.shared.IdGenrator;
import java.math.BigDecimal;
/*** 商铺*/
@TableName("pango_store")
public class Store {/*** ID*/private Long id;/*** 名称*/private String name;/*** 描述*/private String description;/*** 占用面积*/private BigDecimal area;public Store() {}private Store(Long id, String name, String description, BigDecimal area) {this.id = id;this.name = name;this.description = description;this.area = area;}/*** 创建商铺* @param name* @param description* @param area* @return*/public static Store create(String name, String description, BigDecimal area){Assert.notBlank(name,"商铺名称不能为空");Assert.notNull(area,"商铺面积不能为空");Long id = IdGenrator.nextId();return new Store(id,name,description,area);}/*** 修改商铺* @param name* @param description* @param area*/public void change(String name,String description,BigDecimal area){this.name = name;this.description = description;this.area = area;}public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getDescription() {return description;}public void setDescription(String description) {this.description = description;}public BigDecimal getArea() {return area;}public void setArea(BigDecimal area) {this.area = area;}
}
地址实体
package com.cloud.pango.domain.store.entity;import cn.hutool.core.lang.Assert;
import com.baomidou.mybatisplus.annotation.TableName;
import com.cloud.pango.shared.IdGenrator;/*** 商铺落位地址*/
@TableName("pango_store_location")
public class Location {private Long id;/*** 商铺Id*/private Long storeId;/*** 楼栋*/private String building;/*** 楼层*/private String floor;/*** 详细地址*/private String address;public Location() {}private Location(Long id, Long storeId, String building, String floor, String address) {this.id = id;this.storeId = storeId;this.building = building;this.floor = floor;this.address = address;}/*** 创建商铺地址* @param store* @param building* @param floor* @param address* @return*/public static Location create(Store store, String building, String floor, String address) {Assert.notNull(store,"商铺不能为空");Assert.notBlank(building,"楼栋不能为空");Assert.notBlank(floor,"楼层不能为空");Long id = IdGenrator.nextId();Long storeId = store.getId();return new Location(id,storeId,building,floor,address);}/*** 修改地址* @param building* @param floor* @param address*/public void change(String building, String floor, String address){this.building = building;this.floor = floor;this.address = address;}public Long getId() {return id;}public void setId(Long id) {this.id = id;}public Long getStoreId() {return storeId;}public void setStoreId(Long storeId) {this.storeId = storeId;}public String getBuilding() {return building;}public void setBuilding(String building) {this.building = building;}public String getFloor() {return floor;}public void setFloor(String floor) {this.floor = floor;}public String getAddress() {return address;}public void setAddress(String address) {this.address = address;}
}
变更记录实体
package com.cloud.pango.domain.store.entity;import cn.hutool.core.lang.Assert;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.annotation.TableName;
import com.cloud.pango.shared.IdGenrator;/*** 变更记录*/
@TableName("pango_store_change_log")
public class ChangeLog {private Long id;/*** 商铺Id*/private Long storeId;/*** 原记录*/private String source;/*** 先记录*/private String dest;public ChangeLog() {}private ChangeLog(Long id, Long storeId, String source, String dest) {this.id = id;this.storeId = storeId;this.source = source;this.dest = dest;}/*** 创建变更日志* @param sourceStore* @param destStore* @return*/public static ChangeLog create(Long storeId,String sourceStore,String destStore){Assert.notBlank(sourceStore,"原商铺不能为空");Assert.notBlank(destStore,"现商铺不能为空");Long id = IdGenrator.nextId();return new ChangeLog(id,storeId,sourceStore,destStore);}public Long getId() {return id;}public void setId(Long id) {this.id = id;}public Long getStoreId() {return storeId;}public void setStoreId(Long storeId) {this.storeId = storeId;}public String getSource() {return source;}public void setSource(String source) {this.source = source;}public String getDest() {return dest;}public void setDest(String dest) {this.dest = dest;}
}
商铺聚合领域服务能力
package com.cloud.pango.domain.store.ability;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.json.JSONUtil;
import com.cloud.pango.client.event.StoreChangeEvent;
import com.cloud.pango.client.event.data.StoreDto;
import com.cloud.pango.domain.store.entity.ChangeLog;
import com.cloud.pango.domain.store.entity.Location;
import com.cloud.pango.domain.store.entity.Store;
import com.cloud.pango.domain.store.repository.StoreRepository;
import com.cloud.pango.shared.DomainEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.Optional;/*** 商铺领域服务*/
@Service
public class StoreDomainService {@Resourceprivate StoreRepository storeRepository;@Resourceprivate DomainEventPublisher publisher;public Store getStore(Long storeId){Optional<Store> storeOpt = storeRepository.findStoreById(storeId);Assert.isTrue(storeOpt.isPresent(),"商铺不存在");return storeOpt.get();}public Location getLocation(Long storeId){Optional<Location> locationOpt = storeRepository.findLocationByStoreId(storeId);Assert.isTrue(locationOpt.isPresent(),"商铺位置不存在");return locationOpt.get();}/*** 创建商铺* @param name* @param description* @param area* @param building* @param floor* @param address*/@Transactional(rollbackFor = Exception.class)public void createStore(String name, String description, BigDecimal area, String building, String floor, String address){Store store = Store.create(name, description, area);Location location = Location.create(store,building, floor, address);storeRepository.insertStore(store);storeRepository.insertLocation(location);}/*** 编辑商铺* @param storeId* @param name* @param description* @param area* @param building* @param floor* @param address*/@Transactional(rollbackFor = Exception.class)public void editStore(Long storeId,String name, String description, BigDecimal area, String building, String floor, String address){Optional<Store> storeOpt = storeRepository.findStoreById(storeId);Store store = storeOpt.get();StoreDto sourceStore = BeanUtil.copyProperties(store, StoreDto.class);store.change(name,description,area);storeRepository.updateStore(store);StoreDto destStore = BeanUtil.copyProperties(store, StoreDto.class);Optional<Location> locationOpt = storeRepository.findLocationByStoreId(storeId);Location location = locationOpt.get();BeanUtil.copyProperties(location,sourceStore );location.change(building,floor,address);storeRepository.updateLocation(location);BeanUtil.copyProperties(location,destStore);//发布商铺变动事件StoreChangeEvent changeEvent = new StoreChangeEvent(sourceStore,destStore);BeanUtil.copyProperties(location,changeEvent);publisher.publish(changeEvent);}/*** 保存变更日志* @param event*/public void createChangeLog(StoreChangeEvent event){ChangeLog changeLog = ChangeLog.create(event.getSource().getStoreId(), JSONUtil.toJsonStr(event.getSource()), JSONUtil.toJsonStr(event.getDest()));storeRepository.insertChangeLog(changeLog);}}
仓储层:
商铺仓储接口定义
package com.cloud.pango.domain.store.repository;import com.cloud.pango.domain.store.entity.ChangeLog;
import com.cloud.pango.domain.store.entity.Location;
import com.cloud.pango.domain.store.entity.Store;import java.util.Optional;public interface StoreRepository {Optional<Store> findStoreById(Long id);void insertStore(Store store);void updateStore(Store store);void deleteStoreById(Long id);Optional<Location> findLocationByStoreId(Long storeId);void insertLocation(Location location);void updateLocation(Location location);void deleteLocationByStoreId(Long storeId);void insertChangeLog(ChangeLog changeLog);}
infrastructure层
为整个服务提供基础设施,其中有仓储层实现、mybatis的mapper及其配置信息、缓存、mq、消息事件等。
该层属于请求网关,就是业务层主动请求外部系统处理的网关。有这层的网关可以是业务层和基础设施的解耦。如当前系统如果使用的是mysql存储数据,随着抵业务的发展需要更改存储方式,改为mongodb,也只是需要改变仓储层实现而已,并不会影响到业务层,确保了系统最有价值的部分【业务】不会因为基础设施的更改而受到太大的牵连。
包名 | 说明 |
---|---|
db | 数据库基础设施 |
db.mapper | mybatis的mapper |
db.config | mybatis的配置文件 |
event | 事件基础设施 |
event.config | 事件配置文件 |
event.publisher | 事件发布 |
qry | 查询服务实现 |
repository | 仓储服务实现 |
与数据库相关的配置【mybatis配置】
package com.cloud.pango.db.config;import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;/*** @author xiongfengju* @date 2022/8/18*/
@EnableTransactionManagement(proxyTargetClass = true)
@Configuration
public class MybatisPlusConfig
{@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor(){MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 分页插件interceptor.addInnerInterceptor(paginationInnerInterceptor());// 乐观锁插件interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor());// 阻断插件interceptor.addInnerInterceptor(blockAttackInnerInterceptor());return interceptor;}/*** 分页插件,自动识别数据库类型 https://baomidou.com/guide/interceptor-pagination.html*/public PaginationInnerInterceptor paginationInnerInterceptor(){PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();// 设置数据库类型为mysqlpaginationInnerInterceptor.setDbType(DbType.MYSQL);// 设置最大单页限制数量,默认 500 条,-1 不受限制paginationInnerInterceptor.setMaxLimit(-1L);return paginationInnerInterceptor;}/*** 乐观锁插件 https://baomidou.com/guide/interceptor-optimistic-locker.html*/public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor(){return new OptimisticLockerInnerInterceptor();}/*** 如果是对全表的删除或更新操作,就会终止该操作 https://baomidou.com/guide/interceptor-block-attack.html*/public BlockAttackInnerInterceptor blockAttackInnerInterceptor(){return new BlockAttackInnerInterceptor();}
}
商铺Mapper
package com.cloud.pango.db.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cloud.pango.client.qry.req.StorePageQry;
import com.cloud.pango.client.qry.res.StorePageResult;
import com.cloud.pango.domain.store.entity.Store;
import org.apache.ibatis.annotations.Param;public interface StoreMapper extends BaseMapper<Store> {IPage<StorePageResult> page(Page<StorePageResult> page,@Param("qry") StorePageQry qry);
}
落位Mapper
package com.cloud.pango.db.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cloud.pango.domain.store.entity.Location;public interface LocationMapper extends BaseMapper<Location> {}
变更日志mapper
package com.cloud.pango.db.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cloud.pango.domain.store.entity.ChangeLog;public interface ChangeLogMapper extends BaseMapper<ChangeLog> {}
事件配置
package com.cloud.pango.event.config;import com.google.common.eventbus.EventBus;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class EventConfig {@Beanpublic EventBus configEvent() {EventBus eventBus = new EventBus();return eventBus;}
}
事件发布器
package com.cloud.pango.event.publisher;import com.cloud.pango.shared.DomainEventPublisher;
import com.google.common.eventbus.EventBus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
public class GuavaDomainEventPublisher implements DomainEventPublisher {@AutowiredEventBus eventBus;public void publish(Object event) {eventBus.post(event);}
}
商铺查询服务实现
package com.cloud.pango.qry;import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cloud.pango.application.qry.IStoreQryService;
import com.cloud.pango.client.qry.req.StorePageQry;
import com.cloud.pango.client.qry.res.LocationLoadResult;
import com.cloud.pango.client.qry.res.StoreLoadResult;
import com.cloud.pango.client.qry.res.StorePageResult;
import com.cloud.pango.db.mapper.LocationMapper;
import com.cloud.pango.db.mapper.StoreMapper;
import com.cloud.pango.domain.store.entity.Location;
import com.cloud.pango.domain.store.entity.Store;
import org.springframework.stereotype.Service;import javax.annotation.Resource;@Service
public class StoreQryServiceImpl implements IStoreQryService {@Resourceprivate StoreMapper storeMapper;@Resourceprivate LocationMapper locationMapper;@Overridepublic StoreLoadResult loadStoreById(Long id) {Store store = storeMapper.selectById(id);StoreLoadResult storeLoadResult = BeanUtil.copyProperties(store, StoreLoadResult.class);LambdaQueryWrapper<Location> locationQry = new LambdaQueryWrapper<>();locationQry.eq(Location::getStoreId,id);Location location = locationMapper.selectOne(locationQry);LocationLoadResult locationLoadResult = BeanUtil.copyProperties(location, LocationLoadResult.class);storeLoadResult.setLocation(locationLoadResult);return storeLoadResult;}@Overridepublic IPage<StorePageResult> pageStore(StorePageQry qry) {Page<StorePageResult> page = Page.of(qry.getPageNo(),qry.getPageSize());return storeMapper.page(page,qry);}
}
商铺仓储实现
package com.cloud.pango.repository;import cn.hutool.core.lang.Assert;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cloud.pango.db.mapper.ChangeLogMapper;
import com.cloud.pango.db.mapper.LocationMapper;
import com.cloud.pango.db.mapper.StoreMapper;
import com.cloud.pango.domain.store.entity.ChangeLog;
import com.cloud.pango.domain.store.entity.Location;
import com.cloud.pango.domain.store.entity.Store;
import com.cloud.pango.domain.store.repository.StoreRepository;
import org.springframework.stereotype.Repository;import javax.annotation.Resource;
import java.util.Objects;
import java.util.Optional;@Repository
public class StoreRepositoryImpl implements StoreRepository {@Resourceprivate StoreMapper storeMapper;@Resourceprivate LocationMapper locationMapper;@Resourceprivate ChangeLogMapper changeLogMapper;@Overridepublic Optional<Store> findStoreById(Long id) {if(Objects.isNull(id)){return Optional.empty();}Store store = storeMapper.selectById(id);return Optional.ofNullable(store);}@Overridepublic void insertStore(Store store) {Assert.notNull(store,"商铺信息不能为空");storeMapper.insert(store);}@Overridepublic void updateStore(Store store) {Assert.notNull(store,"商铺信息不能为空");storeMapper.updateById(store);}@Overridepublic void deleteStoreById(Long id) {Assert.notNull(id,"商铺Id不能为空");storeMapper.deleteById(id);}@Overridepublic Optional<Location> findLocationByStoreId(Long storeId) {if (Objects.isNull(storeId)) {return Optional.empty();}LambdaQueryWrapper<Location> qry = new LambdaQueryWrapper<>();qry.eq(Location::getStoreId,storeId);Location location = locationMapper.selectOne(qry);return Optional.ofNullable(location);}@Overridepublic void insertLocation(Location location) {Assert.notNull(location,"落位信息不能为空");locationMapper.insert(location);}@Overridepublic void updateLocation(Location location) {Assert.notNull(location,"落位信息不能为空");locationMapper.updateById(location);}@Overridepublic void deleteLocationByStoreId(Long storeId) {Assert.notNull(storeId,"商铺Id不能为空");LambdaQueryWrapper<Location> qry = new LambdaQueryWrapper<>();qry.eq(Location::getStoreId,storeId);locationMapper.delete(qry);}@Overridepublic void insertChangeLog(ChangeLog changeLog) {changeLogMapper.insert(changeLog);}
}
start层
整个应用的启动模块,只存放启动项目及全局配置有关的配置项。
DDD领域设计架构落地相关推荐
- 如何设计一个复杂的业务系统?从对领域设计、云原生、微服务、中台的理解开始...
欢迎关注方志朋的博客,回复"666"获面试宝典 01 如何解决复杂业务设计 Aliware 软件架构设计本身就是一个复杂的事情,但其实业界已有一个共识,那就是"通过组件化 ...
- ddd 企业应用架构模式_灵魂拷问:用了DDD分包就是落地了领域驱动设计吗?谈谈DDD本质...
学习DDD的时候,作为开发,我们更关心它在技术层面的东西,尤其体现在DDD的分包方式.编码技巧等方面. 自然的,我们不禁发问,用了DDD的分包,就是实践落地了DDD了么? 不卖关子,直接说答案,并不是 ...
- DDD 领域驱动设计落地实践:六步拆解 DDD
引言 相信通过前面几篇文章的介绍,大家对于 DDD 的相关理论以及实践的套路有了一定的理解,但是理解 DDD 理论和实践手段是一回事,能不能把这些理论知识实际应用到我们实际工作中又是另外一回事,因此本 ...
- 【DDD落地实践系列】DDD 领域驱动设计落地实践:六步拆解 DDD
引言 相信通过前面几篇文章的介绍,大家对于 DDD 的相关理论以及实践的套路有了一定的理解,但是理解 DDD 理论和实践手段是一回事,能不能把这些理论知识实际应用到我们实际工作中又是另外一回事,因此本 ...
- DDD 领域驱动设计落地实践系列:工程结构分层设计
引言 前面几篇文章中,笔者给大家阐述了 DDD 领域驱动设计的三大过程,重点围绕如何通过战略设计与战术设计进行 DDD 落地实践进行了详细的讨论,但是还没有涉及到工程层面的落地.实际上所有的这些架构理 ...
- DDD领域驱动设计落地实践系列:战略设计和战术设计
引言 通过前面的文章介绍,相信大家对于什么是DDD有了初步的了解,知道它是一种微服务的架构设计方法论,为我们解决如何建立领域模型,如何实现微服务划分等提供了方向和指导.但是对于如何具体落地使用DDD, ...
- DDD微服务架构设计第四课 微服务落地实践的技术中台
10 微服务落地的技术实践 如今,做一个优秀的程序员越来越难.激烈的市场竞争.互联网快速的迭代.软件系统规模化发展,无疑都大大增加了软件设计的难度.因此,对于架构师的能力要求也越来越高,就像我的一本书 ...
- 领域驱动DDD在签到场景落地案例之架构模式(二)
承接DDD概念初识第二篇,第一篇传送地址:领域驱动DDD在签到场景落地案例之概念初识(一) 本篇文章介绍微服务设计原则,以此为设计思想,然后列举DDD常见架构模式,不同架构方式对比,在工作中根据业务选 ...
- DDD 领域驱动设计落地实践系列:战略设计和战术设计
引言 通过前面的文章介绍,相信大家对于什么是 DDD 有了初步的了解,知道它是一种微服务的架构设计方法论,为我们解决如何建立领域模型,如何实现微服务划分等问题提供了方向和指导.但是对于如何具体落地使用 ...
最新文章
- python中的变量、Debug和数据类型
- Linux 系统命令 - pwd - 显示当前所在的位置
- LiveVideoStackCon讲师热身分享 ( 十五 ) —— 教育场景下的实时音视频解决方案
- spring期刊状态_无状态Spring安全性第2部分:无状态认证
- java ajax 导入excel_Ajax asp.net 导入Excel
- 在线健身悄然升温,千亿市场潜力正在释放
- php任务奖励体系,phpwind7.5完备的积分体系
- ubuntu使用之-rime
- 智能手机的超性能语音识别技术简介
- efi文件错误服务器崩溃,电脑故障分析:Winload.efi文件丢失导致蓝屏的解决方法...
- TarBase:有实验数据支持的miRNA靶基因数据库
- CheckboxPreference 改造
- signature=cc8d613f503e9b933c233da06afc0fc6,襄阳市公安局交通警察支队违法车辆信息公告20210118...
- 如何批量保存苏宁易购里的商品图片
- 反思 大班 快乐的机器人_幼儿园大班教案《机器人》含反思
- 图片怎么缩小尺寸比例不变?
- SourceTree安装教程
- PPT之幻灯片中的大纲选项卡
- javac编译错误: 程序包 com.sun.xxx 不存在
- android简易计算,android实现简易计算器