前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >DDD代码整洁之道

DDD代码整洁之道

作者头像
吴就业
发布2021-05-11 14:32:19
6000
发布2021-05-11 14:32:19
举报
文章被收录于专栏:Java艺术Java艺术

​在实现DDD的过程中,我们需要严格遵守代码规范才能保持代码的整洁,否则随着需求的迭代,项目很容易就失去DDD该有的模样,变得即不DDD也不MVC。

应用服务、领域服务、聚合根、资源库的职责

资源库(Repository)的职责是提供聚合根或者持久化聚合根,除此之外应尽可能的没有其它行为,否则聚合根就会严重退化成DAO。

代码语言:javascript
复制
public interface Repository<DO, KEY> {
    void save(DO obj);
    DO findById(KEY id);
    void deleteById(KEY id);
}

聚合根则封装业务操作,聚合根下实体的业务操作也应该通过聚合根完成,即应用服务(ApplicationService)与领域服务(DomainService)都不可绕过聚合根调用实体的业务方法,必须通过聚合根去调用。

应用服务是业务逻辑的封装,不处理业务逻辑。虽然领域服务不是必须的,但对于不能直接通过聚合根完成的业务操作就需要通过领域服务。

如修改用户信息,可在应用服务通过资源库获取用户聚合根,再调用用户聚合根的修改用户信息方法,最后通过资源库持久化用户聚合根。

代码语言:javascript
复制
public class UserApplicationService{

    /**
     * 更新用户基本信息
     *
     * @param command
     * @param token
     */
    @Transactional(rollbackFor = Throwable.class, isolation = Isolation.READ_COMMITTED)
    public void updateUserInfo(ModifyUserInfoCommand command, String token) {
        // 获取聚合根
        Account account = findByAccountId(getUser(token).getId());
        // 调用业务方法
        account.modifyAccountInfo(AccountInfoValobj.builder()
                .nickname(command.getNickname())
                .avatarUrl(command.getAvatarUrl())
                .country(command.getCountry())
                .province(command.getProvince())
                .city(command.getCity())
                .gender(Sex.valueBy(command.getGender()))
                .build());
        // 通过资源库持久化
        repository.save(account);
        // 更新缓存
        accountCache.cache(token, getUserById(account.getId()));
    }

}

这里用户聚合根能到看到自己的信息,用户自己修改自己的信息可直接通过聚合根完成,因此这种场景下我们不需要领域服务。

复杂场景如用户绑定手机号码就不能直接在领域服务中完成。

绑定手机号码一般流程为:获取短信验证码、校验短信验证码、校验手机号码是否已经绑定了别的账号。

其中获取短信验证码与校验短信验证码应放在应用服务完成,而校验手机号码是否已经绑定了别的账号就需要由领域服务完成,因为聚合根无法完成这个判断, 聚合根看不到别的账号,聚合根不能拥有资源库。

聚合根

代码语言:javascript
复制
public class Account extends BaseAggregate<AccountEvent>{
    // .....
    private String phone;

    public void bindMobilePhone(String phoneNumber) {
        if (!StringUtils.isEmpty(this.phone)) {
            throw new AccountParamException("已经绑定过手机号码了,如需更新可走更换手机号码流程");
        }
        this.phone = phoneNumber;
    }

}

领域服务

代码语言:javascript
复制
@Service
public class AccountDomainService {
    
    private AccountRepository repository;

    public AccountDomainService(AccountRepository repository) {
        this.repository = repository;
    }

    public void bindMobilePhone(Long userId, String phone) {
        Account account = repository.findById(userId);
        if (account == null) {
            throw new AccountNotFoundException(userId);
        }
        // 号码被其它账户绑定了
        boolean exist = repository.findByPhone(phone) != null;
        if (exist) {
            throw new AccountBindPhoneException(phone);
        }
        account.bindMobilePhone(phone);
        repository.save(account);
    }

}

应用服务

代码语言:javascript
复制
@Service
public class UserApplicationService {
    
       /**
         * 绑定手机号码-发送验证码
         *
         * @param command
         * @param token
         */
        public void bindMobilePhoneSendVerifyCode(VerifyCodeSendCommand command, String token) {
            // verify login
            getUser(token);
            String key = String.format(CacheKeyConstants.BIND_PHONE_VERIFY_CODE, command.getPhone());
            // 生成验证码
            String verifyCode = ValidCodeUtils.generateNumberValidCode(4);
            // 过期时间三分钟
            redisTemplate.opsForValue().set(key, verifyCode, 180, TimeUnit.SECONDS);
            // 调用消息服务发送验证码
            messageClientGateway.sendSmsVerifyCode(command.getPhone(), verifyCode);
        }
    
        /**
         * 绑定手机号码-提交绑定
         *
         * @param command
         * @param token
         */
        public void bindMobilePhone(BindPhoneCommand command, String token) {
            // 校验验证码
            String key = String.format(CacheKeyConstants.BIND_PHONE_VERIFY_CODE, command.getPhone());
            String verifyCode = redisTemplate.opsForValue().get(key);
            if (!command.getVerifyCode().equalsIgnoreCase(verifyCode)) {
                throw new VerifyPhoneCodeApplicationException();
            }
            Long userId = getUser(token).getId();
            // 通过领域服务绑定手机号码
            accountDomainService.bindMobilePhone(userId, command.getPhone());
            // 更新缓存
            accountCache.cache(token, getUserById(userId));
        }
}

接口层

代码语言:javascript
复制
@RestController
@RequestMapping("account")
public class UserController {
 
    @Resource
    private UserApplicationService userApplicationService;

    @ApiOperation("绑定手机号-获取验证码")
    @GetMapping("/bindMobilePhone/verifyCode")
    public Response<Void> bindMobilePhone(@RequestBody VerifyCodeSendCommand command, 
                HttpServletRequest request) {
        String token = request.getHeader(Constants.AUTHENTICATION_TOKEN);
        userApplicationService.bindMobilePhoneSendVerifyCode(command, token);
        return Response.success();
    }

    @ApiOperation("绑定手机号-提交绑定")
    @PostMapping("/bindMobilePhone/submit")
    public Response<Void> bindMobilePhone(@RequestBody @Validated BindPhoneCommand command,
                 HttpServletRequest request) {
        String token = request.getHeader(Constants.AUTHENTICATION_TOKEN);
        userApplicationService.bindMobilePhone(command, token);
        return Response.success();
    }
    
}

严格遵守CQRS

所有写操作必须走“应用服务-资源库-聚合根-资源库”流程,即应用服务封装一次业务操作,应用服务通过资源库获取聚合根,调用聚合根的业务方法,最后调用资源库持久化聚合根。如果有产生领域事件则最后由应用服务发布事件。

复杂场景下走“应用服务-领域服务-资源库-聚合根-资源库”流程,即应用服务完成应用层的封装,由领域服务封装对聚合根的操作以及一些聚合根无法完成的业务逻辑。

所有读操作都必须走反模式的(Services-->Dao),包括查询单个聚合根的详情、分页查询等场景。

接口层

代码语言:javascript
复制
@RequestMapping("/order")
@RestController
public class OrderController {
    
    @GetMapping("/query")
    public Response<PageInfo<OrderListRepresentation>> queryOrder(OrderQuery query, HttpServletRequest request) {
        return Response.success(orderRepresentationService.queryOrder(query,
                request.getHeader(Constants.AUTHENTICATION_TOKEN)));
    }

}

应用层

代码语言:javascript
复制
@Service
public class OrderRepresentationService implements Cqrs {

    public PageInfo<OrderListRepresentation> queryOrder(OrderQuery query, String token) {
        Long merchantId = merchantApplicationServiceGateway.loginMerchantUser(token).getMerchantId();
        IPage<OrderListRepresentation> orderPage = new Page<>(query.getPage(), query.getPageSize());
        List<OrderListRepresentation> orders = exploreShopOrderMapper.selectOrderBy(merchantId,query,orderPage);
        PageInfo<OrderListRepresentation> pageInfo = new PageInfo<>(query.getPage(), Query.getPageSize());
        pageInfo.setTotalCount((int) orderPage.getTotal());
        if (CollectionUtils.isEmpty(orders)) {
            pageInfo.setList(Collections.emptyList());
        } else {
            orders.parallelStream().forEach(order -> {
                order.setStatusName(OrderStatus.valueOf(order.getStatus()).getName());
                List<Platform> platforms = Arrays.stream(order.getPlatformIds().split(","))
                        .map(Integer::parseInt)
                        .map(Platform::valueOf)
                        .collect(Collectors.toList());
                order.setPlatforms(platforms.stream().map(Platform::getValue).collect(Collectors.toList()));
                order.setPlatformNames(platforms.stream().map(Platform::getName).collect(Collectors.toList()));
            });
            pageInfo.setList(orders);
        }
        return pageInfo;
    }
}

严格遵守CQE

CQE即Command、Query、Event。接收前端创建订单请求使用Command,接收前端分页查询请求使用Query,消费事件(非领域事件)则使用Event。

除Event外,所有写请求都应该使用Command接收参数,而所有查询都应该使用Query接收参数,只在参数只有一个ID的查询情况下,可省略Query。

在查询分离情况下,Query是可直接传递到DAO的(接口层->应用层->DAO)。因此使用Query封装查询条件能够提高方法的复用,当添加查询条件时,无需给方法加多一个参数。

严格遵守层级依赖

上层只能依赖下层,下层不能依赖上层。

以经典四层架构来理解更容易,四层架构指基础设施层、领域层、应用层、接口层。

在一次创建订单的操作中,用于接收前端请求参数的CreateOrderCommand属于应用层的类,虽然我们在Controller(接口层)可直接使用CreateOrderCommand,但这属于上层依赖下层,并且不是领域层,也并未暴露聚合根内部结构,因此是允许的。

如果反过来,直接将CreateOrderCommand对象传递给聚合根,那就构成下层依赖上层了,因此这是不允许的。

CreateOrderCommand必须在应用层拆解为创建订单所需要的值对象,或者实体对象,再调用订单工厂创建订单,然后交给资源库持久化订单聚合根。

建议聚合根ID由代码生成而不是依赖数据库

因为我们需要在创建聚合根时就知道聚合根的ID,而不是等到最后调用资源库持久化后才返回聚合根的ID,并层层返回。

当我们需要在创建订单后发送创建订单事件时,需要给事件带上订单的ID,而事件又需要聚合根生产(在聚合根持久化后在应用服务中发布,因为聚合根没有事件发布器),当聚合根自己都还不知道自己的ID时,如何创建领域事件呢?

如果实在需要依赖数据库生成ID,那么就由聚合根提供一个回写ID的方法,但不能给聚合根类所有字段提供set方法,聚合根的内部结构不可泄漏给应用层

End…

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-04-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java艺术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 严格遵守CQRS
  • 严格遵守CQE
  • 严格遵守层级依赖
  • 建议聚合根ID由代码生成而不是依赖数据库
相关产品与服务
短信
腾讯云短信(Short Message Service,SMS)可为广大企业级用户提供稳定可靠,安全合规的短信触达服务。用户可快速接入,调用 API / SDK 或者通过控制台即可发送,支持发送验证码、通知类短信和营销短信。国内验证短信秒级触达,99%到达率;国际/港澳台短信覆盖全球200+国家/地区,全球多服务站点,稳定可靠。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档