领域驱动在代码层面的落地感悟
领域驱动在代码层面的落地感悟
笔者杨涛12年互联网从业经验,8年技术管理经验。先后从事搜索、社交、在线教育、电商等行业相关工作,对高并发和复杂业务场景解决方案均有较深入的经验积累。
背景
小米有品随业务发展,推出了会员系统。包含满5单返会费,开卡礼包、每月优惠券、会员价、优先购等等权益和福利等业务场景。在初期的需求调研过程中,会员系统业务复杂性已经得以显现。按计划,未来业务上还会有横向和纵向上的扩展(如横向上权益增加、多会员身份,纵向上的等级制度等等)。
业务特点与挑战
-
影响范围广,会员权益几乎覆盖所有主要业务场景,例如产品站、购物车、结算页、会场页和会员频道等场景
-
逻辑复杂,权益类型多,状态多(例如有15个左右的退款状态,未来还会增加)
-
关联服务较多,如会员卡购买与购物车、订单、履约等系统相关,权益与卡券、优惠、商城相关,省钱计算器与订单、卡券相关
-
需要为业务扩展和升级留下充足空间,代码上模块间要尽可能的减少耦合(数据库表设计上,我们本次几个主要表都放弃加唯一键约束(非主键),使用程序来控制唯一性更有利于横向扩展)
为什么选择领域驱动
- 会员中心定位是中台服务,团队之前也对领域驱动有了一些积累,加之要准确快速实现如此复杂的业务,还要满足后续紧接着的各种扩展需求,领域驱动此时成了最佳选择。
- 领域驱动设计简称DDD(Domain-Driven Design),是一种开发思想体系,旨在设计和管理复杂问题域编写的软件,由Eric Evans于2003年提出。由于种种原因,它在国内的应用并不广泛,不过其中的一些概念早已经出现在我们的日常工作中了,例如我们在代码中看到的XxxEntity,XxxRepository ,XxxService,XxxFactory。
- 它本身理论的复杂度高,学习成本高,在国内一直没有得到广泛的应用,直到后来微服务和中台的出现。领域驱动中领域和限界上下文等概念,如天然为服务拆分和治理所准备。不过毕竟是18年前提出的理念,当初的DDD并不能完全适用现在,我们需要搭配事件风暴、四色建模等分析和建模方法。
- 微服务经过这么多年发展,相关架构理论越来越成熟,加上事件风暴、敏捷迭代等工程方法得到广泛运用,再去看领域驱动已经不再那么难了,甚至可以根据自己项目特色,设计合适自己的落地方案。
企业应用开发范式比较:数据驱动、特性驱动与领域驱动:
领域驱动应该如何落地
领域驱动分战略设计和战术设计两个阶段,其中又有很多的概念,如:通用语言、领域/子域、限界上下文、领域模型、值对象、实体、聚合/聚合根、领域服务、领域事件、资源库、工厂等。
这些概念不一定能全部用到,可以根据自己的项目特色做出选择,即便同一个项目也不是必须将所有业务划分领域,例如某管理系统中的操作日志,则可以直接在应用层调用基础设施层中的资源库进行存库操作。
战略设计过程方法有事件风暴和四色建模(可根据项目团队习惯选择),参与者需要包括产品经理、领域专家、研发等,项目流程上需要做出相应改变,可以参考下图:
架构上可以选择(或组合)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;亦不建议跳过学习,直接用简单的项目来练手
当前的各类互联网业务,不再是一味追求流量了(高并发也不再是难题),产品会越做越“好玩”,系统业务势必也越来复杂,领域驱动也将得到越来越多的关注。