肝了一个月的 DDD,一文带你掌握!(二)

简介: 大家好,我是楼仔!去年倒腾了一个半月,写过一篇 DDD 的文章,当时没有推广,完全自嗨,为了不让这篇好文被埋没,现重新整理,突出重点,可读性更强!

4. DDD实战


4.1 项目介绍

  • 主要是围绕用户、角色和两者的关系,构建权限分配领域模型。
  • 采用 DDD 4 层架构,包括用户接口层、应用层、领域层和基础服务层。
  • 数据通过 VO、DTO、DO、PO 转换,进行分层隔离。
  • 采用 SpringBoot + MyBatis Plus 框架,存储用 MySQL。


4.2 工程目录

项目划分为用户接口层、应用层、领域层和基础服务层,每一层的代码结构都非常清晰,包括每一层 VO、DTO、DO、PO 的数据定义,对于每一层的公共代码,比如常量、接口等,都抽离到 ddd-common 中。

./ddd-application  // 应用层
├── pom.xml
└── src
    └── main
        └── java
            └── com
                └── ddd
                    └── applicaiton
                        ├── converter
                        │   └── UserApplicationConverter.java // 类型转换器
                        └── impl
                            └── AuthrizeApplicationServiceImpl.java // 业务逻辑
./ddd-common
├── ddd-common // 通用类库
│   ├── pom.xml
│   └── src
│       └── main
│           └── java
│               └── com
│                   └── ddd
│                       └── common
│                           ├── exception // 异常
│                           │   ├── ServiceException.java
│                           │   └── ValidationException.java
│                           ├── result // 返回结果集
│                           │   ├── BaseResult.javar
│                           │   ├── Page.java
│                           │   ├── PageResult.java
│                           │   └── Result.java
│                           └── util // 通用工具
│                               ├── GsonUtil.java
│                               └── ValidationUtil.java
├── ddd-common-application // 业务层通用模块
│   ├── pom.xml
│   └── src
│       └── main
│           └── java
│               └── com
│                   └── ddd
│                       └── applicaiton
│                           ├── dto // DTO
│                           │   ├── RoleInfoDTO.java
│                           │   └── UserRoleDTO.java
│                           └── servic // 业务接口
│                               └── AuthrizeApplicationService.java
├── ddd-common-domain
│   ├── pom.xml
│   └── src
│       └── main
│           └── java
│               └── com
│                   └── ddd
│                       └── domain
│                           ├── event // 领域事件
│                           │   ├── BaseDomainEvent.java
│                           │   └── DomainEventPublisher.java
│                           └── service // 领域接口
│                               └── AuthorizeDomainService.java
└── ddd-common-infra
    ├── pom.xml
    └── src
        └── main
            └── java
                └── com
                    └── ddd
                        └── infra
                            ├── domain // DO
                            │   └── AuthorizeDO.java
                            ├── dto 
                            │   ├── AddressDTO.java
                            │   ├── RoleDTO.java
                            │   ├── UnitDTO.java
                            │   └── UserRoleDTO.java
                            └── repository
                                ├── UserRepository.java // 领域仓库
                                └── mybatis
                                    └── entity // PO
                                        ├── BaseUuidEntity.java
                                        ├── RolePO.java
                                        ├── UserPO.java
                                        └── UserRolePO.java
./ddd-domian  // 领域层
├── pom.xml
└── src
    └── main
        └── java
            └── com
                └── ddd
                    └── domain
                        ├── event // 领域事件
                        │   ├── DomainEventPublisherImpl.java
                        │   ├── UserCreateEvent.java
                        │   ├── UserDeleteEvent.java
                        │   └── UserUpdateEvent.java
                        └── impl // 领域逻辑
                            └── AuthorizeDomainServiceImpl.java
./ddd-infra  // 基础服务层
├── pom.xml
└── src
    └── main
        └── java
            └── com
                └── ddd
                    └── infra
                        ├── config
                        │   └── InfraCoreConfig.java  // 扫描Mapper文件
                        └── repository
                            ├── converter
                            │   └── UserConverter.java // 类型转换器
                            ├── impl
                            │   └── UserRepositoryImpl.java
                            └── mapper
                                ├── RoleMapper.java
                                ├── UserMapper.java
                                └── UserRoleMapper.java
./ddd-interface
├── ddd-api  // 用户接口层
│   ├── pom.xml
│   └── src
│       └── main
│           ├── java
│           │   └── com
│           │       └── ddd
│           │           └── api
│           │               ├── DDDFrameworkApiApplication.java // 启动入口
│           │               ├── converter
│           │               │   └── AuthorizeConverter.java // 类型转换器
│           │               ├── model
│           │               │   ├── req // 入参 req
│           │               │   │   ├── AuthorizeCreateReq.java
│           │               │   │   └── AuthorizeUpdateReq.java
│           │               │   └── vo  // 输出 VO
│           │               │       └── UserAuthorizeVO.java
│           │               └── web     // API
│           │                   └── AuthorizeController.java
│           └── resources // 系统配置
│               ├── application.yml
│           └── resources // Sql文件
│               └── init.sql
└── ddd-task
    └── pom.xml
./pom.xml


4.3 数据库

包括 3 张表,分别为用户、角色和用户角色表,一个用户可以拥有多个角色,一个角色可以分配给多个用户。

create table t_user
(
    id           bigint auto_increment comment '主键' primary key,
    user_name    varchar(64)                        null comment '用户名',
    password     varchar(255)                       null comment '密码',
    real_name    varchar(64)                        null comment '真实姓名',
    phone        bigint                             null comment '手机号',
    province     varchar(64)                        null comment '用户名',
    city         varchar(64)                        null comment '用户名',
    county       varchar(64)                        null comment '用户名',
    unit_id      bigint                             null comment '单位id',
    unit_name    varchar(64)                        null comment '单位名称',
    gmt_create   datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    gmt_modified datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '修改时间',
    deleted      bigint   default 0                 not null comment '是否删除,非0为已删除'
)comment '用户表' collate = utf8_bin;
create table t_role
(
    id           bigint auto_increment comment '主键' primary key,
    name         varchar(256)                       not null comment '名称',
    code         varchar(64)                        null comment '角色code',
    gmt_create   datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    gmt_modified datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '修改时间',
    deleted      bigint   default 0                 not null comment '是否已删除'
)comment '角色表' charset = utf8;
create table t_user_role (
    id           bigint auto_increment comment '主键id' primary key,
    user_id      bigint                             not null comment '用户id',
    role_id      bigint                             not null comment '角色id',
    gmt_create   datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    gmt_modified datetime default CURRENT_TIMESTAMP not null comment '修改时间',
    deleted      bigint   default 0                 not null comment '是否已删除'
)comment '用户角色关联表' charset = utf8;


4.4 基础服务层

仓储(资源库)介于领域模型和数据模型之间,主要用于聚合的持久化和检索。它隔离了领域模型和数据模型,以便我们关注于领域模型而不需要考虑如何进行持久化。

比如保存用户,需要将用户和角色一起保存,也就是创建用户的同时,需要新建用户的角色权限,这个可以直接全部放到仓储中:

public AuthorizeDO save(AuthorizeDO user) {
    UserPO userPo = userConverter.toUserPo(user);
    if(Objects.isNull(user.getUserId())){
        userMapper.insert(userPo);
        user.setUserId(userPo.getId());
    } else {
        userMapper.updateById(userPo);
        userRoleMapper.delete(Wrappers.<UserRolePO>lambdaQuery()
                .eq(UserRolePO::getUserId, user.getUserId()));
    }
    List<UserRolePO> userRolePos = userConverter.toUserRolePo(user);
    userRolePos.forEach(userRoleMapper::insert);
    return this.query(user.getUserId());
}

仓储对外暴露的接口如下:

// 用户领域仓储
public interface UserRepository {
    // 删除
    void delete(Long userId);
    // 查询
    AuthorizeDO query(Long userId);
    // 保存
    AuthorizeDO save(AuthorizeDO user);
}

基础服务层不仅仅包括资源库,与第三方的调用,都需要放到该层,Demo 中没有该示例,我们可以看一个小米内部具体的实际项目,他把第三方的调用放到了 remote 目录中:

EZU5[WS[]G89)}(3XW]ZYJT.png


4.5 领域层

4.5.1 聚合&聚合根

我们有用户和角色两个实体,可以将用户、角色和两者关系进行聚合,然后用户就是聚合根,聚合之后的属性,我们称之为“权限”。

对于地址 Address,目前是作为字段属性存储到 DB 中,如果对地址无需进行检索,可以把地址作为“值对象”进行存储,即把地址序列化为 Json 存,存储到 DB 的一个字段中。

public class AuthorizeDO {
    // 用户ID
    private Long userId;
    // 用户名
    private String userName;
    // 真实姓名
    private String realName;
    // 手机号
    private String phone;
    // 密码
    private String password;
    // 用户单位
    private UnitDTO unit;
    // 用户地址
    private AddressDTO address;
    // 用户角色
    private List<RoleDTO> roles;
}

4.5.2 领域服务

Demo中的领域服务比较薄,通过单位ID后去获取单位名称,构建单位信息:

@Service
public class AuthorizeDomainServiceImpl implements AuthorizeDomainService {
    @Override
    // 设置单位信息
    public void associatedUnit(AuthorizeDO authorizeDO) {
        String unitName = "武汉小米";// TODO: 通过第三方获取
        authorizeDO.getUnit().setUnitName(unitName);
    }
}

我们其实可以把领域服务再进一步抽象,可以抽象出领域能力,通过这些领域能力去构建应用层逻辑,比如账号相关的领域能力可以包括授权领域能力、身份认证领域能力等,这样每个领域能力相对独立,就不会全部揉到一个文件中,下面是实际项目的领域层截图:

image.png


4.5.3 领域事件

领域事件 = 事件发布 + 事件存储 + 事件分发 + 事件处理。

这个 Demo 中,对领域事件的处理非常简单,还是一个应用内部的领域事件,就是每次执行一次具体的操作时,把行为记录下来。Demo 中没有记录事件的库表,事件的分发还是同步的方式,所以 Demo 中的领域事件还不完善,后面我会再继续完善 Demo 中的领域事件,通过 Java 消息机制实现解耦,甚至可以借助消息队列,实现异步。

/**
 * 领域事件基类
 *
 * @author louzai
 * @since 2021/11/22
 */
@Getter
@Setter
@NoArgsConstructor
public abstract class BaseDomainEvent<T> implements Serializable {
    private static final long serialVersionUID = 1465328245048581896L;
    /**
     * 发生时间
     */
    private LocalDateTime occurredOn;
    /**
     * 领域事件数据
     */
    private T data;
    public BaseDomainEvent(T data) {
        this.data = data;
        this.occurredOn = LocalDateTime.now();
    }
}
/**
 * 用户新增领域事件
 *
 * @author louzai
 * @since 2021/11/20
 */
public class UserCreateEvent extends BaseDomainEvent<AuthorizeDO> {
    public UserCreateEvent(AuthorizeDO user) {
        super(user);
    }
}
/**
 * 领域事件发布实现类
 *
 * @author louzai
 * @since 2021/11/20
 */
@Component
@Slf4j
public class DomainEventPublisherImpl implements DomainEventPublisher {
    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;
    @Override
    public void publishEvent(BaseDomainEvent event) {
        log.debug("发布事件,event:{}", GsonUtil.gsonToString(event));
        applicationEventPublisher.publishEvent(event);
    }
}


4.4 应用层

应用层就非常好理解了,只负责简单的逻辑编排,比如创建用户授权:

@Transactional(rollbackFor = Exception.class)
public void createUserAuthorize(UserRoleDTO userRoleDTO){
    // DTO转为DO
    AuthorizeDO authorizeDO = userApplicationConverter.toAuthorizeDo(userRoleDTO);
    // 关联单位单位信息
    authorizeDomainService.associatedUnit(authorizeDO);
    // 存储用户
    AuthorizeDO saveAuthorizeDO = userRepository.save(authorizeDO);
    // 发布用户新建的领域事件
    domainEventPublisher.publishEvent(new UserCreateEvent(saveAuthorizeDO));
}

查询用户授权信息:

@Override
  public UserRoleDTO queryUserAuthorize(Long userId) {
      // 查询用户授权领域数据
      AuthorizeDO authorizeDO = userRepository.query(userId);
      if (Objects.isNull(authorizeDO)) {
          throw ValidationException.of("UserId is not exist.", null);
      }
      // DO转DTO
      return userApplicationConverter.toAuthorizeDTO(authorizeDO);
  }

细心的同学可以发现,我们应用层和领域层,通过 DTO 和 DO 进行数据转换。


4.5 用户接口

最后就是提供 API 接口:

@GetMapping("/query")
public Result<UserAuthorizeVO> query(@RequestParam("userId") Long userId){
    UserRoleDTO userRoleDTO = authrizeApplicationService.queryUserAuthorize(userId);
    Result<UserAuthorizeVO> result = new Result<>();
    result.setData(authorizeConverter.toVO(userRoleDTO));
    result.setCode(BaseResult.CODE_SUCCESS);
    return result;
}
@PostMapping("/save")
public Result<Object> create(@RequestBody AuthorizeCreateReq authorizeCreateReq){
    authrizeApplicationService.createUserAuthorize(authorizeConverter.toDTO(authorizeCreateReq));
    return Result.ok(BaseResult.INSERT_SUCCESS);
}

数据的交互,包括入参、DTO 和 VO,都需要对数据进行转换。


4.6 项目运行

  • 新建库表:通过文件 "ddd-interface/ddd-api/src/main/resources/init.sql" 新建库表。
  • 修改 SQL 配置:修改 "ddd-interface/ddd-api/src/main/resources/application.yml" 的数据库配置。
  • 启动服务:直接启动服务即可。
  • 测试用例:
  • 请求 URL:http://127.0.0.1:8087/api/user/save
  • Post body:{"userName":"louzai","realName":"楼","phone":13123676844,"password":"***","unitId":2,"province":"湖北省","city":"鄂州市","county":"葛店开发区","roles":[{"roleId":2}]}


4.7 项目地址

DDD Demo 代码已经上传到 GitHub 中:

https://github.com/lml200701158/ddd-framework

或者通过下面命令直接获取:

git clone git@github.com:lml200701158/ddd-framework.git


5. 结语


谈谈我对 DDD 的理解,我觉得 DDD 不像一门技术,我理解的技术比如高并发、缓存、消息队列等,DDD 更像是一项软技能,一种方法论,包含了很多设计理念。


这篇文章写于去年,所以当时对 DDD 理解的其实还不够深入,今年做过一些 DDD 的项目,所以现在对 DDD 的理解又加深了几分。


大家不要认为,掌握了一些概念,以及 DDD 的基本思想,就掌握了 DDD,然后做项目时,照葫芦画瓢,这样你会死的很惨!


只掌握 DDD 表面的东西,其实是不够的,我觉得 DDD 最复杂的地方,其实是在它的领域设计部分,项目启动前,你一定要设计各个领域对象,以及它们直接的交互关系。


比如我们之前做过一个项目,因为这块没有做好,大家一边写代码,一边还在思考,这个领域对象该如何构造,严重影响开发效率,最后又不得不回退到 MVC 的模式。

不要为了炫技,啥都要搞个 DDD,两者如何选择:

  • MVC:上来就可以开干,短平快,前期用起来很香,整体开发效率也更高,所以对于紧急,或者不那么重要的项目,我会直接用 MVC 怼,不好的地方就是,后面会越来越复杂,可能最后就是一坨屎山,但是很多时候,比如老板进度催的紧,我哪想到那么多以后呢?
  • DDD:前期需要花大量时间设计好领域模型,对于一些基础组件,或者一些核心服务,如果对象模型非常复杂,建议采用 DDD,前期可能会稍微痛苦一些,但是后期维护起来会非常方便。
相关文章
|
存储 消息中间件 JSON
肝了一个月的 DDD,一文带你掌握!(一)
大家好,我是楼仔! 去年倒腾了一个半月,写过一篇 DDD 的文章,当时没有推广,完全自嗨,为了不让这篇好文被埋没,现重新整理,突出重点,可读性更强!
629 0
肝了一个月的 DDD,一文带你掌握!(一)
|
测试技术
2011年下半年11月份系统架构设计师上午试题答案之二
  2011年下半年11月份系统架构设计师上午试题答案之二 试题一:网络三层 层次化的网络设计在互联网组件的通信中引入了三个关键层的概念,分别是: 核心层、汇聚层和接入层。   核心层是为网络提供了骨干组件或者高速交换组件,在纯粹的分层设计中,核心层只完成数据交换的特殊任务。
864 0
|
程序员
2011年下半年11月份系统架构设计师上午试题答案之三
  2011年下半年11月份系统架构设计师上午试题答案之三 试题一:著作权 2007年上半年系统分析师上午试题12 ●甲公司的程序员在不影响本职工作的条件下,在乙公司兼职并利用其兼职单位的物质技术条件开发了一套应用程序,该应用程序的著作权属于(12)。
901 0
|
机器人 调度 语音技术
2010年下半年11月份系统架构设计师上午试题以及参考答案之七
2010年下半年11月份系统架构设计师上午试题以及参考答案之七   ●某公司承接了一个开发家用空调自动调温器的任务,调温器测量外部空气温度,根据设定的期望温度控制空调的开关。根据该需求,公司应采用____(50)___架构风格最为合适。
843 0
2010年下半年11月份系统架构设计师上午试题以及参考答案之十
2010年下半年11月份系统架构设计师上午试题以及参考答案之十   ●The software architecture is a set of software components, subsystems, relationships,interactions, the properti...
798 0
|
设计模式 调度
2010年下半年11月份系统架构设计师上午试题以及参考答案之四
2010年下半年11月份系统架构设计师上午试题以及参考答案之四 ●在实际的项目开发中,人们总是希望使用自动工具来执行需求变更控制过程。下列描述中,___(24)___不是这类工具所具有的功能。    (24)A.可以定义变更请求的数据项以及变更请求生存期的状态转换图          B.记录每一种状态变更的数据,确认做出变更的人员          C.可以加强状态转换图使经授权的用户仅能做出所允许的状态变更          D.定义变更控制计划,并指导设计人员按照所制定的计划实施变更 ●需求管理是CMM可重复级中的6个关键过程域之一,其主要目标是___(25)___。
909 0
|
网络协议 数据安全/隐私保护 开发者
2010年下半年11月份系统架构设计师上午试题以及参考答案之八
2010年下半年11月份系统架构设计师上午试题以及参考答案之八   ●某服务器软件系统能够正确运行并得出计算结果,但存在“系统出错后不能在要求的时间内恢复到正常状态”和“对系统进行二次开发时总要超过半年的时间”两个问题,上述问题依次与质量属性中的___(58)___相关。
789 0
|
测试技术
2010年下半年11月份系统架构设计师上午试题以及参考答案之五
2010年下半年11月份系统架构设计师上午试题以及参考答案之五   ●下列关于不同软件开发方法所使用的模型的描述中,正确的是___(32)___。(32)A.在进行结构化分析时,必须使用数据流图和软件结构图这两种模型      B.采用面向对象开发方法时,可以使用状态图和活动图对系统的动态行为进行建模      C.实体联系图(E-R图)是在数据库逻辑结构设计时才开始创建的模型      D. UML的活动图与程序流程图的表达能力等价 参考答案:B   ●某银行系统采用Factory Method方法描述其不同账户之间的关系,设计出的类图如下所示。
1027 0
|
测试技术 项目管理
2009年下半年11月份系统架构设计师上午试题以及参考答案之七
2009年下半年11月份系统架构设计师上午试题以及参考答案之七 ● 系统测试将软件、硬件、网络等其它因素结合,对整个软件进行测试。(43)不是系统测试的内容。(43)A.路径测试B.可靠性测试C安装测试D.安全测试 参考答案:A   ● 软件测试是为了发现错误而执行程序的过程。
916 0
|
存储 文件存储 BI
2009年下半年11月份系统架构设计师上午试题以及参考答案之四
2009年下半年11月份系统架构设计师上午试题以及参考答案之四 ● 某公司欲构建一个网络化的开放式数据存储系统,要求采用专用网络连接并管理存储设备和存储管理子系统。针对这种应用,采用(15)存储方式最为合适。
1205 0