DDD Deom分析
这个Demo不是我写的,下面我将围绕这个Demo,来分析作者是如何落地DDD的。
工程结构
整个项目的工厂结构如下,其中核心是baiyan-ddd-base和baiyan-ddd-core:
baiyan-ddd-base:
baiyan-ddd-core:
表结构
- 用户表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); } }