领域驱动在代码层面的落地感悟

领域驱动在代码层面的落地感悟

2021年12月14日 10:47 ·  阅读 8981
领域驱动在代码层面的落地感悟

笔者杨涛12年互联网从业经验,8年技术管理经验。先后从事搜索、社交、在线教育、电商等行业相关工作,对高并发和复杂业务场景解决方案均有较深入的经验积累。

背景

小米有品随业务发展,推出了会员系统。包含满5单返会费,开卡礼包、每月优惠券、会员价、优先购等等权益和福利等业务场景。在初期的需求调研过程中,会员系统业务复杂性已经得以显现。按计划,未来业务上还会有横向和纵向上的扩展(如横向上权益增加、多会员身份,纵向上的等级制度等等)。

业务特点与挑战

  • 影响范围广,会员权益几乎覆盖所有主要业务场景,例如产品站、购物车、结算页、会场页和会员频道等场景

  • 逻辑复杂,权益类型多,状态多(例如有15个左右的退款状态,未来还会增加)

  • 关联服务较多,如会员卡购买与购物车、订单、履约等系统相关,权益与卡券、优惠、商城相关,省钱计算器与订单、卡券相关

  • 需要为业务扩展和升级留下充足空间,代码上模块间要尽可能的减少耦合(数据库表设计上,我们本次几个主要表都放弃加唯一键约束(非主键),使用程序来控制唯一性更有利于横向扩展)

为什么选择领域驱动

  1. 会员中心定位是中台服务,团队之前也对领域驱动有了一些积累,加之要准确快速实现如此复杂的业务,还要满足后续紧接着的各种扩展需求,领域驱动此时成了最佳选择。
  2. 领域驱动设计简称DDD(Domain-Driven Design),是一种开发思想体系,旨在设计和管理复杂问题域编写的软件,由Eric Evans于2003年提出。由于种种原因,它在国内的应用并不广泛,不过其中的一些概念早已经出现在我们的日常工作中了,例如我们在代码中看到的XxxEntity,XxxRepository ,XxxService,XxxFactory。
  3. 它本身理论的复杂度高,学习成本高,在国内一直没有得到广泛的应用,直到后来微服务和中台的出现。领域驱动中领域和限界上下文等概念,如天然为服务拆分和治理所准备。不过毕竟是18年前提出的理念,当初的DDD并不能完全适用现在,我们需要搭配事件风暴、四色建模等分析和建模方法。
  4. 微服务经过这么多年发展,相关架构理论越来越成熟,加上事件风暴、敏捷迭代等工程方法得到广泛运用,再去看领域驱动已经不再那么难了,甚至可以根据自己项目特色,设计合适自己的落地方案。

企业应用开发范式比较:数据驱动、特性驱动与领域驱动:

图片

引自:www.yyang.io/2016/06/01/…

领域驱动应该如何落地

领域驱动分战略设计和战术设计两个阶段,其中又有很多的概念,如:通用语言、领域/子域、限界上下文、领域模型、值对象、实体、聚合/聚合根、领域服务、领域事件、资源库、工厂等。

这些概念不一定能全部用到,可以根据自己的项目特色做出选择,即便同一个项目也不是必须将所有业务划分领域,例如某管理系统中的操作日志,则可以直接在应用层调用基础设施层中的资源库进行存库操作。

战略设计过程方法有事件风暴和四色建模(可根据项目团队习惯选择),参与者需要包括产品经理、领域专家、研发等,项目流程上需要做出相应改变,可以参考下图:

图片 引自:zhangyi.xyz/overview-of…

架构上可以选择(或组合)DDD分层架构、六边形架构、整洁架构等,具体怎么选择要看具体的业务场景或全局的架构风格。

从例子看落实到代码层面

领域驱动重心应该放在战略设计阶段,不过为了快速带大家过一遍,看看与当前主要的数据驱动设计有何不同,我们就从代码层面了解领域驱动落地后的样子。

我所在团队成员,之前基本是基于数据驱动进行设计和研发,或者应用过阿里的领域模型规约(DO、DTO、BO、AO、VO、Query),因此我们选择先以DDD分层架构落地,后续视情况升级。

 1. DDD分层架构图

图片

2.落地上,图里两个地方需要注意:

① 业务逻辑只能存在于领域层和应用层,且应用层只能是跨多子域调用,主要业务逻辑都按领域驱动规范在领域层实现。如违反此原则,会让大量的业务逻辑被“挤”到应用层

② 上层是可以调用下方所有层的。例如有一些数据,是没有(或没有必要)使用子域(领域模型)进行相关业务管理的,是可以直接从应用层访问基础设施层的资源库的。(这里不建议用户接口层直接访问资源库,用户接口层还是只做参数校验、幂等、结果封装等就好)

3.各层说明(自下而上)

基础设施层: 不含业务逻辑。其中包括各种连接池(mysql、redis等)、各种配置(对接配置中心、对接全局唯一、或对接其他集团中间件服务)、项目内部的Util、资源库(数据库、redis、本地缓存等);严格讲,对数据存储的访问(资源库)也属于基础设施层,包括数据库、redis等中间件缓存、jvm本地缓存等,但为了之后服务拆分方便,我们在各个子域包下创建对应的资源库包,存放子域内相关数据的操作类

注意:一些MVC架构下的常量配置类或一些枚举类,在这里属于领域层对应的子域内部(可归属于值对象),不放在基础设施层

领域层: 主要业务逻辑模块,包含1个或多个子域,子域包括核心子域、支撑子域、通用子域(通用子域通常是单独的或第三方的服务提供,如发邮件和短信)

应用层: 跨子域调用(如跨子域的事务)时,可以在应用层调度,也可以通过领域事件触发;其他服务的消息订阅;其他服务(如RPC/Rest接口)的调用代理

用户接口层: 对应dubbo服务API的实现类,以及web(如果web项目使用了领域建模)的controller

4.目录结构一览

图片

5.用一个例子串一下各层调用(自上而下)『伪代码』

用户接口层(接口实现类)有一个方法:

com.xiaomi.youpin.member.service.interfaces.DemoServiceImpl#refund{
    //校验参数
    verifyParams(request); 
    //调用基础设施层做请求幂等性校验,重试1次,间隔50ms 
    IdempotentTools.try(request.getParamsForIdempotent(),() -> {        
        //调用应用层服务,执行具体业务        
        demoXxxxAppService.refund(request.getA(), request.getB());
    },1,50);
}
复制代码

对应的应用层类(统一AppService后缀)的方法:

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
com.xiaomi.youpin.demo.service.application.service.Demodomain1AppService#refund{    
    //顺序调用两个子域的相关方法    
    Long xxId = demodomain1DomainService.invalid(a, b);     
    demodomain2DomainService.refund(a, xxId);
}
复制代码

调用的领域层服务(统一DomainService后缀)有2个,这里以demodomain1DomainService为例:

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
com.xiaomi.youpin.member.service.domains.demodomain1.service.Demodomain1DomainService#invalid{ 
    //从资源库中查询数据对象    .
    ..domains.demodomain1.repository.dao.mysql.bean.DbDemoUser dbDemoUser = demodomain1UserRepository.findById(a);    
    //使用防腐层将数据对象转换为领域实体
    ...domains.demodomain1.aggregate.entity.DemoUser demoUser = DemoUserAdapter.parseToDemoUser(dbDemoUser);    
    //将此领域实体作为聚合根,创建对应聚合    
    ...domains.demodomain1.aggregate.DemoUserAggregate demoUserAggregate = DemoUserAggregate.builder().setAggregateRoot(demoUser).setUserRepository(memberUserRepository).build();    
    //调用聚合类的业务方法,执行具体的业务处理    
    demoUser = demoUserAggregate.invalid(b);    
    //后续流程(可以直接访问Repository)    
    Long vvvId = demodomain1VVVRepository.vvv(a, demoUser.getUid, demoUser.getStatus());    return vvvId;
}
复制代码

以领域服务demodomain1的Demodomain1UserRepository为例

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
com.xiaomi.youpin.demo.service.domains.demodomain1.repository.Demodomain1UserRepository#update{    
    //更新数据    
    demoUserDbDao.update(dbDemoUser);    
    //DbDemoUserSummary类是资源库内部使用的类,在资源库内部转换    ...domains.demodomain1.repository.dao.mysql.bean.DbDemoUserSummary summaryInfo = dbDemoUser.getSummaryInfo();    
    //刷新简要信息缓存    
    refreshSummaryCache(summaryInfo );}private void refreshSummaryCache(long a,int b){    //更新redis缓存    
    demoUserRedisCacheDao.refreshCache(refresh);    
    //这里只是示例,不讨论本地缓存的分布式数据同步问题    demoUserJvmCacheDao.refreshCache(refresh);
}
复制代码

聚合以及实体、值对象相关的,由于涉及业务以及篇幅原因这里就不多做介绍了,大家在实施领域驱动的过程中,战略设计之后这些地方都会非常清晰。

总结和感悟

落地领域驱动并不是目的,使用领域驱动的长处解决我们最需要解决的问题才是目的;一些领域驱动中的设计原则,我们要根据具体情况选择是否遵守,一切以优先解决实际问题为出发点

领域驱动是复杂的、高学习成本的,团队需要做好充足的学习准备,在实践中不断磨合出适合团队的执行方案;它虽然复杂,但确实对复杂的业务场景的治理非常有效。(举个栗子,当面对数据库中几十几百个各种数据表时,或许你需要考虑领域驱动设计了)

领域驱动不是银弹,项目复杂度不高,不建议使用DDD;亦不建议跳过学习,直接用简单的项目来练手

当前的各类互联网业务,不再是一味追求流量了(高并发也不再是难题),产品会越做越“好玩”,系统业务势必也越来复杂,领域驱动也将得到越来越多的关注。

posted on 2022-12-11 18:46  漫思  阅读(19)  评论(0编辑  收藏  举报

导航