cover_image

使用DDD设计福利中心微服务代码模型

OA基础平台组 之家技术 2022年04月20日 08:00

图片

总篇141篇 2022年第16篇


★ 目录 ★

01

系统架构

02

封装与调用

03

开发规范

3.1命名规范

3.2模块规范

3.3方法规范

3.4代码规范

3.5其他规范

04

构建代码模型

4.1目录结构

4.2代码实现

  4.2.1适配器层

  4.2.2应用层

  4.2.3领域层

  4.2.4基础设置层

05

总结


上文《使用事件风暴构建福利中心领域模型》,接下来我们使用DDD设计模式来构建福利中心的微服务代码模型。

01

系统架构

微服务系统中可以采用多种风格的架构,可以包裹着领域模型,也可以作用于领域模型之内,一般分为分层架构、事件驱动架构、六边形架构等等,而DDD领域驱动设计并没有指定特定的架构,我们采用最熟悉也是最传统的分层架构来事件DDD领域驱动设计。

通过分层架构,可以将系统按不同职责组织成有序层次,这种划分往往比较容易界定业务边界。在分层架构中,我们将领域模型和业务逻辑分离出来,并减少对业务应用层和基础设施层的依赖,将一个复杂的系统分为不同的层,每层都应该具有良好的内聚性,并且只依赖于比其自身更低的层。通过把领域层(domain)单独分离出来,负责展示业务属性和业务逻辑,形成对领域知识的集中并形成业务软件的核心,如下图所示:


图片


02

封装与调用

从技术角度实现分层:分为适配层、应用层、领域层、基础实施层四层架构。遵循四层架构模式,能够在每层关注自己的事情,适配层关注接口暴露和任务调度,应用层关注组装上下文,领域层关注业务的逻辑,基础实施层关注持久化数据和远程调用,可通过消息中间件或事件总线实现调用。

从业务角度实现分层:通过将大的系统划分多个上下文,焦点放在领域层,将业务领域限定在同一上下文中,只关注当前模块的开发,降低模块之间的依赖,将业务与技术隔离,不依赖任何一个技术框架。

微服务当中的服务是从领域层逐级向上封装、组合和暴露的,微服务之间的应用服务可以通过RPC、HTTP等直接访问,也可以通过消息中间件进行异步解耦。由于跨微服务操作,在进行数据新增和修改操作时,需关注分布式事务,保证数据的一致性。


图片


03

开发规范


3.1命名规范

规范

用途

父类或接口

解释

xxxVO

View Object

cn.automis.takin.data.vo.BaseVO

展示对象

xxxBO/xxxCO

Client Object

cn.automis.takin.data.dto.BaseDTO

客户对象,用于传输数据,等同于DTO

xxxCmd

Client Request

cn.automis.takin.data.dto.Command

Cmd代表Command,表示一个写请求

xxxQuery

Client Request

cn.automis.takin.data.dto.Query

Query,表示一个读请求

xxxCmdExe

Command Executor


命令模式,每一个写请求对应一个执行器

xxxQueryExe

Query Executor


命令模式,每一个读请求对应一个执行器

xxxValue

Entity


值对象

xxxEntity

Entity


领域实体

xxxDO

Data Object

cn.automis.takin.data.domain.BaseDO

数据对象,用于持久化

xxxInterceptor

Command Interceptor


拦截器,用于处理切面逻辑

xxxService

API Service


xxxServiceI 不太习惯,就把 I 放在前边吧

xxxDomainService

Domain Service


需要多个领域对象协作时,使用DomainService

xxxValidator

Validator


校验器,用于校验的类

xxxAssembler

Assembler


组装器,DTO <---> Entity,用于Application层

xxxConvertor

Convertor


转化器,Entity <---> DO,用于Infrastructure层

xxxMapper

Mapper

cn.automis.takin.mybatis.mapper.GenericMapper

Mybatis Mapper

ResponseTO

ResponseTO

cn.automis.takin.exception.to.ResponseTO

返回值包装

xxxEnum

Enum

cn.automis.takin.data.enums.BaseEnum

枚举


  • Application:对外暴露的是DTO,不能暴露 Entity

  • Domain:gateway对外暴露的是Entity,不能暴露 DO

  • 所以这里有两套转换器 xxxAssembler 和 xxxConvertor


图片


3.2模块规范

图片


图片


图片


3.3方法规范

CRUD操作

方法名约定

新增

create

添加

add

删除

remove(App和Domain层),delete(Infrastructure层)

修改

update

查询(单个结果)

get

查询(多个结果)

list

分页查询

page

统计

count


3.4代码规范

遵守alibabaJava开发手册领域模型命名规范并遵守如下约定

1)数据对象:xxxDO,xxx即为数据表名。

2)数据传输对象:xxxDTO,xxx为业务领域相关的名称。

3)展示对象:xxxVO,xxx一般为网页名称。

4)POJO是DO/DTO/BO/VO的统称,禁止命名成xxxPOJO。

5)所有的POJO类属性必须使用包装数据类型。说明:POJO类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何NPE问题,或者入库检查,都由使用者来保证。

6)定义DO/DTO/VO等POJO类时,不要设定任何属性默认值。

7)POJO类必须写toString方法。使用IDE中的工具:source> generate toString时,如果继承了另一个POJO类,注意在前面加一下super.toString。或使用Lombok。

8)禁止在POJO类中,同时存在对应属性xxx的isXxx()和getXxx()方法。

9)POJO类必须实现序列化即实现Serializable接口,或使用脚手架提供的BaseDto,BaseDomain等。


3.5其他规范

要记住,留给公司一个方便维护、整洁优雅的代码库,是我们技术人员最高技术使命,也是我们对公司做出的最大技术贡献。

1)日志记录

  • 统一使用SLF4j接口

2)异常处理

  • 运行时异常:通过参数检查等方式避免或抛出运行时异常,日志记录

  • 检查异常:检查异常需要捕获,处理,日志记录

3)接口定义

【原则】:

  • 接口地址定义表明用意

  • 接口地址定义清晰,简洁,无歧义

  • 同一个服务组件的接口定义具有一致性

【格式】:

  • 控制类的顶层地址格式:/{顶层分类名},例如:/emp人员库相关接口的顶层地址

  • 接口定义使用Swagger的API注解说明

  • 标注完整的请求信息,请求方法,请求地址,参数可选性,接口描述

4)代码注释

  • 类,接口,枚举顶层注释

  • 接口方法注释

  • 静态方法注释

  • 公开方法注释

  • 类的属性字段注释

  • 常量注释

  • 不限于以上


04

构建代码模型


4.1目录结构

├──pom.xml├──start│  ├─src│  │  └─main│  │      ├─java│  │      │  └─cn│  │      │      └─autohome│  │      │          └─ddd│  │      │              └─Application.java│  │      └─resources│  │          ├─bootstrap.yml│  │          ├─application.yml│  │          └─logback-spring.xml│  └─pom.xml├──adapter│  ├─src│  │  └─main│  │      └─java│  │          └─cn│  │              └─autohome│  │                  └─ddd│  │                      ├─controller│  │                      │  ├─BalanceController.java│  │                      │  └─GiftsController.java│  │                      ├─jobhandler│  │                      │  └─JobHandler.java│  │                      └─mqhandler│  │                         ├─CheckAndSendMQHandler.java│  │                         └─config│  └─pom.xml├──app│  ├─src│  │  └─main│  │      └─java│  │          └─cn│  │              └─autohome│  │                  └─ddd│  │                      ├─command│  │                      │  ├─GiftsAddCmdExe.java│  │                      │  └─query│  │                      │     └─GiftsDictQryExe.java│  │                      ├─event│  │                      │  └─handler│  │                      └─service│  │                         ├─BalanceServiceImpl.java│  │                         └─GiftsServiceImpl.java│  └─pom.xml├──client│  ├─src│  │  └─main│  │      └─java│  │          └─cn│  │              └─autohome│  │                  └─ddd│  │                      ├─api│  │                      │  ├─BalanceServiceI.java│  │                      │  └─GiftsServiceI.java│  │                      ├─constant│  │                      │  └─Constants.java│  │                      ├─dto│  │                      │  ├─clientobject│  │                      │  │  ├─GiftsCO.java│  │                      │  │  └─MessageCO.java│  │                      │  └─domainevent│  │                      │     └─CustomerCreatedEvent.java│  │                      ├─enums│  │                      │  └─CurrencyTypeEnum.java│  │                      └─vo│  │                         └─BalanceVo.java│  └─pom.xml├──domain│  ├─src│  │  └─main│  │      └─java│  │          └─cn│  │              └─autohome│  │                  └─ddd│  │                      └─domain│  │                          ├─balance│  │                          │  ├─Balance.java│  │                          │  └─BalanceGateway.java│  │                          ├─send│  │                          ├─task│  │                          ├─user│  │                          └─...│  └─pom.xml└─infrastructure   ├─src   │  └─main   │      ├─java   │      │  └─cn   │      │      └─autohome   │      │          └─ddd   │      │              ├─common   │      │              │  └─event   │      │              │    └─DomainEventPublisher.java     │      │              ├─config   │      │              │  └─properties   │      │              │    └─Properties.java   │      │              ├─convertor   │      │              ├─enums   │      │              └─gatewayimpl   │      │                  ├─database   │      │                  │  ├─DictMapper.java   │      │                  │  └─dataobject   │      │                  │    └─DictDO.java   │      │                  ├─feign   │      │                  │  ├─BalanceApiClient.java   │      │                  │  └─dataobject   │      │                  │     └─balance   │      │                  │       └─BalanceParam.java   │      │                  └─rpc   │      │                      └─dataobject   │      └─resources   │          └─cn   │              └─autohome   │                  └─ddd   │                      └─gatewayimpl   │                          └─database   │                             └─DictMapper.xml   └─pom.xml


4.2代码实现


4.2.1适配器层

1)Web API:
@RestController@RequestMapping("/balance")@Api(value = "/balance", tags = "余额")public class BalanceController {
@Autowired private BalanceServiceI balanceService;
@GetMapping("/like") @ApiOperation("查询余额") public ResponseTO<BalanceVo, DefaultResponseErrorType> balance(){ return balanceService.getLikeBalance(); }}


2)MQ API:

@Component@Slf4jpublic class CheckAndSendLikeHandler extends MessageHandler<FinishTaskSendLikeAddCmd> {
@Autowired private SendRecordServiceI sendRecordService;
@Override public void onMessage(String messageId, FinishTaskSendLikeAddCmd message, MessageProperties messageProperties) throws Exception { log.info("onMessage [{}][{}][{}]", messageId, message, messageProperties); ResponseTO<Long, DefaultResponseErrorType> responseTO = sendRecordService.addCheckTaskAndSend(message); log.info("处理成功 条数为[{}]", responseTO.getData()); }
@Override public void onError(Throwable t) { log.error(t.getMessage(), t); }}


3)JOB API:
@Component@Slf4jpublic class JobHandler {
@Autowired private SendRecordServiceI sendRecordServiceI;
@Job("conSumeJobHandler") public void conSumeJobHandler() { try { //获取参数 String param = JobHelper.getJobParam(); sendRecordServiceI.send(SendRecordSendCmd.builder().sendRecordId(Long.parseLong(param)).build()); } catch (Exception e) { log.error(e.getMessage(), e); JobHelper.handleFail(e.getMessage()); } JobHelper.handleSuccess(); }}
  • 适配器层面向终端提供服务支持,是各种服务的入口

  • 为Web端提供身份认证和权限验证服务,VO数据转换

  • 为API端提供限流和熔断服务,DTO数据转换


4.2.2应用层

//余额服务@Servicepublic class BalanceServiceImpl implements BalanceServiceI {
@Resource private LikeBalanceQryExe likeBalanceQryExe;
@Override public ResponseTO<BalanceVo, DefaultResponseErrorType> getLikeBalance() { OaTokenInfo user = OaWebContextUtil.getTokenWapper().getOaTokenInfo(); return likeBalanceQryExe.execute(user); }}
//查询余额动作@Component@Slf4jpublic class LikeBalanceQryExe {
public ResponseTO<BalanceVo, DefaultResponseErrorType> execute(OaTokenInfo user) { Account account = welfareConfig.getAccountMap().get(AccountTypeEnum.GOLD.getCode()); BeanUtils.copyProperties(account, userAccount);
UserAccount userAccount = UserAccount.builder().userProfile(UserProfileConvertor.toEntity(user)).build(); Balance balance = userAccount.getAllBalance(); BalanceVo vo = new BalanceVo(); BeanUtils.copyProperties(balance , vo); return ResponseTO.ok(vo); }}

//赠送服务@Servicepublic class GiftsServiceImpl implements GiftsServiceI { @Resource private GiftsAddCmdExe giftsAddCmdExe;
/** * 赠送家家赞 * * @param cmd * @return */ @Override public ResponseTO<Long, DefaultResponseErrorType> addGiftsCmd(GiftsAddCmd cmd) { return giftsAddCmdExe.execute(cmd, OaWebContextUtil.getTokenWapper().getOaTokenInfo()); }}
//赠送动作@Component@Slf4jpublic class GiftsAddCmdExe {
@Resource private GiftsGateway giftsGateway;
@DistributedLock(key = "'GiftsAddCmdExe:execute:'+#args[1].userCode", tryCnt = 5, interval = 100, fallbackMethod = "fallback") public ResponseTO<Long, DefaultResponseErrorType> execute(@Valid GiftsAddCmd cmd, OaTokenInfo oaTokenInfo) {
//写赠送表 Gifts gifts = giftsGateway.addGifts(Gifts.builder() .createUser(currentUser) .giftsUser(sendUser) .currencyType(CurrencyTypeEnum.JIAJIAZAN) .giftsNumber(cmd.getGiftsNum()) .giftsMessage(cmd.getGiftsMessage()) .build()); return ResponseTO.ok(1L); }
public ResponseTO<Long, DefaultResponseErrorType> fallback(@Valid GiftsAddCmd cmd, OaTokenInfo oaTokenInfo) { log.error("获取分布式锁失败 [{}][{}]", cmd, oaTokenInfo); throw new SystemException("请勿频繁点击!"); }}
  • 应用层是很薄的一层,主要用于调用和组合领域服务,切勿包含任何业务逻辑

  • 应用层可包括少量的流程参数判断

  • 应用层接口api、dto、vo的定义在client项目模块下

  • 对外暴露的是DTO、vo,不能暴露领域层实体


4.2.3领域层

1)领域对象:

这里的领域对象包括实体、值对象。

实体:可以用唯一标识(而不是属性)来区分,能单独存在且可变化的对象,这种对象称为实体(Entity)。

值对象:不能单独存在或在逻辑层面单独存在无意义,且不可变化的对象。

聚合:组相关对象的集合,对外是一个整体。

聚合根:聚合中可代表整个业务操作的实体对象,通过它提供对外访问操作,它维护聚合内部的数据一致性,它是聚合中对象的管理者,是这个聚合的根节点。

public class Gifts extends Entity {
private static final long serialVersionUID = 575985897599480566L;
/** * 赠送人 */ private UserProfile createUser;
/** * 受赠人 */ private UserProfile giftsUser;
/** * 币种1.家家赞2.家家币 */ private CurrencyTypeEnum currencyType;
/** * 数量 */ private Long giftsNumber;
/** * 赠送寄语 */ private String giftsMessage;}
public class UserProfile extends Entity { private String userId; private String userCode; private String userName; private String deptCode; private String deptName;}


2)基础设置:

基础设施接口放在领域层主要的目的是减少领域层对基础设施层的依赖接口的设计是不可暴露实现的技术细节,如不能将拼装的SQL作为参数 gateway对外暴露的是Entity,不能暴露 DO。

public interface GiftsGateway {
/** * 赠送家家赞 * @param gifts * @return */ Gifts addGifts(Gifts gifts);
}


4.2.4基础设置层

基础设施层是数据的输出向,主要包含数据库、缓存、消息队列、远程访问等的技术实现基础设施层对外隐藏技术实现细节,不能将数据库实体对外暴露,只能提供粗粒度的数据输出服务数据库操作:领域层传递的是领域对象,在这里可以按最终表结构的设计进行拆分实现。

@Component@Slf4jpublic class GiftsGatewayImpl implements GiftsGateway {
@Resource private GiftsMapper giftsMapper;
@Override public Gifts addGifts(Gifts gifts) { GiftsDO giftsDO = GiftsConvertor.toDataObject(gifts); giftsMapper.add(giftsDO); gifts.setGitfsId(giftsDO.getId()); return gifts; }}
@Component@Slf4jpublic class BalanceGatewayImpl implements BalanceGateway {
@Resource private BalanceApiClient balanceApiClient;
@Override public LikeBalance likeBalence(String userCode) { BalanceRetDto balanceRetDto = new BalanceRetDto(); LikeBalance balance = new LikeBalance(); try { balanceRetDto = balanceApiClient.balance(...); } catch (Exception e) { log.error(e.getMessage(), e); } .... return balance; }}
@Component@Slf4jpublic class SendGatewayImpl implements SendGateway { /** * 异步发放MQ * * @param sendRecord * @return */ @Override public void syncSend(SendRecord sendRecord) { try { simpleMessageProducer.sendMessage(MqDestinationEnum.SYNC_LIKE.getDestination(), sendRecord); } catch (Exception e) { log.error(e.getMessage(), e); } }}

05

总结

本篇通过DDD代码实践来讲述软件设计的术与器,本质是为了高内聚低耦合,紧靠本质,读者可按自己的理解和团队实际情况来实践DDD即可。

  • oop原则,领域驱动设计是一种面向对象的设计,先建模再去设计数据库,这样更加符合oop原则。    

  • 对齐业务,应用层代码就是业务文档,看到应用层代码就能理解到业务逻辑,而看到领域层就不仅仅能看到对象中属性还能看到业务的核心方法,这样开发人员和业务人员交流语言得到统一,减少沟通障碍。

  • 技术和业务解耦,领域层除了业务之外不再设计软件架构等底层技术,数据库、缓存、消息队列、远程访问等的技术实现可以放在基础设施层。

  • DDD视角下领域合作,首先判断领域事件发生在微服务之内还是微服务之间,在微服务内部可以采用事件总线的方式实现调用,微服务与微服务之间可以采用消息队列的方式实现服务间的调用。


鉴于作者经验有限,我们对领域驱动的理解难免会有不足之处,欢迎大家共同探讨,共同提高。



参考


https://github.com/alibaba/COLA

张建飞著. 代码精进之路:从码农到工匠[M].北京:人民邮电出版社,2020.1


作者简介

图片

OA基础平台组

OA基础平台组主要负责OA框架的开发、OA基础组件的开发、OA相关业务系统的开发、代码规范的指定、脚手架的推广

图片

阅读更多


▼ 关注「之家技术」,获取更多技术干货 


图片

微信扫一扫
关注该公众号

继续滑动看下一个
之家技术
向上滑动看下一个