一文带你学习DDD,全是干货!(二)

简介: 说一下我为什么突然想学习DDD,这个肯定不是为了装X,也不是为了以后好跳槽,虽然转到人事团队也快3个月,由于之前一直做其它项目,所以现在才开始接触招聘相关的业务。因为招聘业务涉及的系统非常多,想借鉴领域驱动设计的思想,看后续如何对系统进行重构,这个就是我想学习DDD的主要原因。

DDD Deom分析


这个Demo不是我写的,下面我将围绕这个Demo,来分析作者是如何落地DDD的。

github地址:https://github.com/louyanfeng25/ddd-demo

工程结构

整个项目的工厂结构如下,其中核心是baiyan-ddd-base和baiyan-ddd-core:

image.gif4UJ51}XT(B%@}4KM7[%F657.jpg

baiyan-ddd-base:

H0T{~{_~~3IMG~`SRKJOXES.png

baiyan-ddd-core:

OGGAQ6@O5FOI@(TIB}%GG34.pngimage.gif

(Y11{7GUO7HG@8TPP71UZ1H.png


表结构

  • 用户表t_user
  • 角色表t_role
  • 用户角色关联表t_user_role,一个用户会有多个角色
  • 地址表
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;
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;
# 地址表省略...


实体

我们定义了3个实体,分别为角色实体、地址实体、单位实体:

// 角色实体
public class Role implements Entity {
    private Long id;
    private String code; // 角色
    private String name; // 角色名
    private LocalDateTime gmtCreate;
    private LocalDateTime gmtModified;
    @TableLogic(delval = "current_timestamp()")
    private Long deleted;
}
// 地址实体
public class Address implements ValueObject<Address> {
    private String province; // 省
    private String city; // 市
    private String county; // 区
    /**
     * 比较地址相等
     * @param address 地址
     * @return
     */
    @Override
    public boolean sameValueAs(Address address){
        return Objects.equals(this,address);
    }
}
// 单位实体
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Unit implements Entity {
    private Long id;
    private String unitName; // 单位名称
}


聚合&聚合根&值对象

  • 聚合&聚合根:我们把用户、地址、角色和用户单位等聚合在一起,表述一个完整的用户信息含义,里面的聚合根就是用户,它作为信息唯一的载体。比如电商的订单、理赔的保单,这些都可以最为聚合根。
  • 值对象:对于用户数据,其实是通过聚合的方式将所有信息聚合在一起,比如地址信息,如果我们希望DB中能直接存入用户的地址信息,且应用场景中不会通过地址去查询人员信息,仅作为展示使用,可以将地址作为Json值对象,保存在t_user表中。
// 用户聚合根
public class User extends BaseUuidEntity implements AggregateRoot {
    private String userName; // 用户名
    private String realName; // 用户真实名称
    private String phone; // 用户手机号
    private String password; // 用户密码
    private Address address; // 用户地址
    private Unit unit; // 用户单位
    private List<Role> roles; // 角色
    // ...


工厂

下面我们通过工厂模式,创建用户实体:

// 用户聚合创建工厂
public class UserFactory {
    // 新建用户聚合
    public static User createUser(CreateUserCommand command){
        User user = new User(command.getUserName(), command.getRealName(), command.getPhone(), command.getPassword());
        user.bindAddress(command.getProvince(),command.getCity(),command.getCounty());
        user.bindRoleByRoleId(command.getRoles());
        return user;
    }
    // 修改用户聚合
    // ...
}


领域服务

我们需要通过领域服务,对用户单位信息做一些关联:

// 用户领域服务
public class UserDomainServiceImpl implements UserDomainService {
    @Autowired
    UnitAdapter unitAdapter;
    @Override
    public void associatedUnit(Long unitId, User user){
        UnitDTO unitByUnitId = unitAdapter.findUnitByUnitId(unitId);
        user.bindUnit(unitId,unitByUnitId.getUnitName());
    }
}


领域事件

我们插入、更新和删除数据时,希望能做一些后续处理,这个我们就可以通过领域事件来处理,由于是在系统内处理该事件,直接使用Java的Event事件作为示例(如果想做到完全隔离,或者有一些其它的要求,比如时序、频率限制等,也可以借助消息队列)。

发布领域事件:

// 领域事件发布接口
public interface DomainEventPublisher {
    // 发布事件
    void publishEvent(BaseDomainEvent event);
}
// 领域事件基类
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();
    }
}

领域事件:

// 用户新增领域事件
public class UserCreateEvent extends BaseDomainEvent<User> {
    public UserCreateEvent(User user) {
        super(user);
    }
}
// 用户删除领域事件
public class UserDeleteEvent extends BaseDomainEvent<Long> {
    public UserDeleteEvent(Long id) {
        super(id);
    }
}
// 用户修改领域事件
public class UserUpdateEvent extends BaseDomainEvent<User> {
    public UserUpdateEvent(User user) {
        super(user);
    }
}


应用服务

这个就是对业务逻辑进行编排,我们看如何对用户的逻辑进行编排:

  • 通过工厂创建一个用户对象;
  • 通过领域服务关联用户单位信息;
  • 通过仓库存储用户信息
  • 发布用户新建的领域事件
public class UserApplicationServiceImpl implements UserApplicationServic {
    @Autowired
    UserRepository userRepository;
    @Autowired
    DomainEventPublisher domainEventPublisher;
    @Autowired
    UserDomainService userDomainService;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void create(CreateUserCommand command){
        //工厂创建用户
        User user = UserFactory.createUser(command);
        //关联单位单位信息
        userDomainService.associatedUnit(command.getUnitId(),user);
        //存储用户
        User save = userRepository.save(user);
        //发布用户新建的领域事件
        domainEventPublisher.publishEvent(new UserCreateEvent(save));
    }
}


资源库【仓储】

示例中的ORM框架使用MyBatis。

实体

前面其实也有个“实体”,但是那个实体更多是针对业务层面,这个实体主要针对数据库层面,包括用户实体、角色实体和用户角色实体,下面我们只给部分实体示例:

// 基础表结构实体
public class BaseUuidEntity {
    // 主键id 采用默认雪花算法
    @TableId
    private Long id;
    // 创建时间
    private LocalDateTime gmtCreate;
    // 修改时间
    private LocalDateTime gmtModified;
    // 是否删除,0位未删除
    @TableLogic(delval = "current_timestamp()")
    private Long deleted;
}
// 角色实体
@TableName("t_role")
public class RolePO extends BaseUuidEntity {
    /** 角色名称 */
    private String name;
    /** 角色code */
    private String code;
}
// 用户实体
@TableName("t_user")
public class UserPO extends BaseUuidEntity {
  // ...
}
// 用户角色实体
@TableName("t_user_role")
public class UserRolePO extends BaseUuidEntity {
  // ...
}

Mapper映射关系:

// 用户角色关联关系mapper
@Mapper
public interface UserRoleMapper extends BaseMapper<UserRolePO> {
}
// 用户信息mapper
@Mapper
public interface UserMapper extends BaseMapper<UserPO> {
    // 用户信息分页查询
    Page<UserPO> userPage(KeywordQuery query);
}
// 用户角色关联关系mapper
@Mapper
public interface UserRoleMapper extends BaseMapper<UserRolePO> {
}


用户领域仓储

  • 删除用户和用户角色数据;
  • 根据用户ID,获取用户信息和用户角色的详细信息;
  • 保存用户和用户角色信息。
@Repository
public class UserRepositoryImpl implements UserRepository {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleMapper roleMapper;
    @Autowired
    private UserRoleMapper userRoleMapper;
    @Override
    public void delete(Long id){
        userRoleMapper.delete(Wrappers.<UserRolePO>lambdaQuery().eq(UserRolePO::getUserId,id));
        userMapper.deleteById(id);
    }
    @Override
    public User byId(Long id){
        UserPO user = userMapper.selectById(id);
        if(Objects.isNull(user)){
            return null;
        }
        List<UserRolePO> userRoles = userRoleMapper.selectList(Wrappers.<UserRolePO>lambdaQuery()
                .eq(UserRolePO::getUserId, id).select(UserRolePO::getRoleId));
        List<Long> roleIds = CollUtil.isEmpty(userRoles) ? new ArrayList<>() : userRoles.stream()
                .map(UserRolePO::getRoleId)
                .collect(Collectors.toList());
        List<RolePO> roles = roleMapper.selectBatchIds(roleIds);
        return UserConverter.deserialize(user,roles);
    }
    @Override
    public User save(User user){
        UserPO userPo = UserConverter.serializeUser(user);
        if(Objects.isNull(user.getId())){
            userMapper.insert(userPo);
            user.setId(userPo.getId());
        }else {
            userMapper.updateById(userPo);
            userRoleMapper.delete(Wrappers.<UserRolePO>lambdaQuery().eq(UserRolePO::getUserId,user.getId()));
        }
        List<UserRolePO> userRolePos = UserConverter.serializeRole(user);
        userRolePos.forEach(userRoleMapper::insert);
        return this.byId(user.getId());
    }
}


查询仓储

// CQRS模式,用户查询仓储
public interface UserQueryRepository{
    // 用户分页数据查询
    Page<UserPageDTO> userPage(KeywordQuery query);
}
// 用户信息查询仓储
@Repository
public class UserQueryRepositoryImpl implements UserQueryRepository {
    @Autowired
    private UserMapper userMapper;
    @Override
    public Page<UserPageDTO> userPage(KeywordQuery query){
        Page<UserPO> userPos = userMapper.userPage(query);
        return UserConverter.serializeUserPage(userPos);
    }
}


相关文章
|
4月前
|
缓存 前端开发 数据建模
DDD结构学习归纳总结(小傅哥の码场 学习专栏)
DDD结构学习归纳总结(小傅哥の码场 学习专栏)
130 0
|
23天前
|
人机交互 领域建模 数据库
DDD中领域故事的作用
【8月更文挑战第14天】
48 0
|
4月前
|
消息中间件 测试技术 领域建模
DDD - 一文读懂DDD领域驱动设计
DDD - 一文读懂DDD领域驱动设计
6948 1
|
4月前
|
消息中间件 JavaScript Kafka
谈谈代码:DDD从入门到完全入门
之前的DDD文章——谈谈代码:降低复杂度,从放弃三层架构到DDD入门,通篇下来像 是简单的讲了一些概念,然后快速的实战一下——很多同学反馈感觉就是入门了,但没有完全入门,因此我们再加一篇。
236 3
谈谈代码:DDD从入门到完全入门
|
Java 机器人 程序员
大白话DDD(DDD黑话终结者)
大白话DDD(DDD黑话终结者)
326 0
|
Web App开发 机器学习/深度学习 数据可视化
万字长文解析,领域驱动设计(DDD)落地设计
领域驱动设计(简称 ddd)概念来源于2004年著名建模专家Eric Evans 发表的他最具影响力的书籍:《领域驱动设计——软件核心复杂性应对之道》(Domain-Driven Design –Tackling Complexity in the Heart of Software),简称Evans DDD,领域驱动设计思想进入软件开发者的视野。在将近20年的发展中领域模型设计一直占据着非常重要的位置,但其直接面向业务场景的设计思想,更适合在具有特定业务场景的模型构建。
|
敏捷开发 架构师 程序员
有人骂DDD、有人爱DDD,我们讲道理
有人骂DDD、有人爱DDD,我们讲道理
153 0
|
存储 人机交互 微服务
一分钟搞懂 DDD
一分钟搞懂 DDD
194 0
一分钟搞懂 DDD
|
前端开发 JavaScript
#yyds干货盘点# 前端歌谣的刷题之路-第四十二题-去掉字符串两端空格
#yyds干货盘点# 前端歌谣的刷题之路-第四十二题-去掉字符串两端空格
74 0
#yyds干货盘点# 前端歌谣的刷题之路-第四十二题-去掉字符串两端空格