DDD简洁模型 介绍
DDD是什么?
一个领域驱动设计,面向大型系统架构思想,项目越大,使用DDD收益越大。
为什么要使用DDD架构?
举个例子,以前有很多老系统,用的是老环境,老的开发思想,导致如果需要重构的话,会发现有很多困难。
例如:
- 沟通难
一个项目大了以后,开发人员,产品可能已经换过好几轮了,产品提出一个“小需求”,在产品那边可能觉得这只是个小需求,开发却要做很久,这时候就会有一个问题,这个系统到底是你懂还是我懂?
- 开发难
对开发人员来说,最痛苦不是让我开发一个项目,而是看别人代码,尤其是如果一个类上千行代码,里面一堆if-else,这怎么看?谁能告诉我这段代码有什么用?能不能去掉?也不敢去,因为也不确定这个代码的影响范围有多少。
- 测试难
牵一发而动全身,改了一个小需求,测试需要组织庞大的测试计划,甚至可能需要通宵。
- 创新难
例如我学到很多比较新,比较潮流的技术,可是项目中就没办法用,例如以前用的hibernate,早期的时候ssh用的就是hibernate,但是现在基本上都转成mybatis,因为mybatis更轻巧,但是如果要你把hibernate转成mybatis,就很少有人换的动,业务太多了。系统背负的业务越来越重,已经基本上丧失了对新技术的灵活敏感。
系统变老的问题,是整个行业都在经历的问题,正因为这个问题,现在出了很多软件工程方法论,很多很多框架,各种各样的技术来解决开发中软件膨胀的问题。
随着现在微服务的项目越来越大,随着而来的肯定会有同样的问题。
微服务架构,曾经一度认为微服务架构是可以防止系统越来越老化。
例如:
电商项目如果太大了,就把他拆分, 比如说,用户微服务,下单微服务,还是以mvc架构去构建。
最初的时候,电商的复杂度都是可以慢慢解开,但是随着互联网项目的越来越发展,单说下单这一块,以后可能也会越来越复杂,就例如一些优惠,什么打折之类的。以后也是有可能会导致代码无比庞大,所以这种方式并不是真正能防止系统老化的方式,治标不治本。
经过行业的讨论,慢慢的就认为DDD 是目前防止系统老化最理想的方式。
DDD从业务上分成一个一个的domain,domain就是领域
例如产品下单模块中,里面有产品的价格,性价比,等这些属性,围绕这些属性,会形成自己的一个功能,这就可以叫产品领域。
再例如,运输领域,里面就会有他的仓储地址,仓储大小,宽度,重量等等运输方面的领域。
抽象到DDD概念中,系统就不再是一个以mvc的构建,而是一个一个有自己独立功能的domain来构成,有这种关系后,以后进行微服务拆分,或者模块拆分,最理想的方式就是我可以随意按领域来拆分,自由组合。
例如一个项目中有三个领域,将他拆成两个微服务,一个微服务包含一个大领域,一个微服务有两个小领域,如果能够达成这样的方式,我们的项目就可以自由组合自由变幻,加如微服务体系中,就能更好的体现微服务的能力,系统就可以以领域的方式茁壮成长,所有的功能也就可以想怎么玩怎么玩,这是最理想的一种方式,这种方式虽然很理想,但是是有一定难度的,怎么去达到这样的方式,DDD就提供了一种方法论,这就是DDD的重要性,也就是为什么越来越多的大项目用到DDD。
实际案例:
一个转账功能:
- 业务需求:用户购买商品后,向商家进行支付。
- 产品设计:实现步骤拆解
- 1,从数据库中查出用户信息和商户的账户信息
- 2,调用风控系统的微服务,进行风险评估
- 3,实现转入转出操作,计算双方的金额变化,保存到数据库
- 4,发送交易情况给kafka发给一些其他的外部系统,进行后续审计和风控
传统Mvc的代码结构:
public class PaymentController { private PayService payService; public Result pay(String merchantAccount, BigDecimal amount){ Long userId = (Long) session.getAttribute("userId"); return payService.pay(userId,merchantAccount,amount); } }
public class PayServiceImpl implements PayService { private AccountDao accountDao; //操作数据库 private KafkaTemplate<String,String> kafkaTemplate;//操作Kafka private RiskCheckService riskCheckService;//风控微服务接口 public Result pay(Long userId, String merchantAccount, BigDecimal amount){ //从数据库读取数据 AccountDO clientDO = accountDao.selectByUserId(userId); AccountDO merchantDO = accountDao.selectByAccountNumber(merchantAccount); //业务参数校验 if(amount >(clientDO.getAvailable)){ throw new NoMoneyException(); } //调用风控微服务 RiskCode riskCode = riskCheckService.checkPayment(....); //检查交易合法性 if("0000"!= riskCode){ throw new InvalideOperException(); } //计算薪值,并更新字段 BigDecimal newSource = clientDO.getAvailable().subtract(amount); BigDecimal newTarget = merchantDO.getAvailable().add(amount); clientDO.setAvailable(newSource); merchantDO.setAvailable(newTarget); //更新到数据库 accountDao.update(clientDO); accountDao.update(merchantDO); //发送审计消息 String message = sourceUserId + "," +targetAccountNumber +","+ targetAmount; kafkaTemplate.send(TOPIC_AUDIT_LOG,message); return Result.SUCCESS; } }
业务怎么设计,我们就怎么开发
这样的代码,就很容易造成我们的代码老化
比如说查用户信息,如果说用户表结构改了,例如以前没有会员,现在加上了会员。整个数据库变了,DAO就要改,DAO改了,下面这段代码可能也需要修改。
//从数据库读取数据 AccountDO clientDO = accountDao.selectByUserId(userId); AccountDO merchantDO = accountDao.selectByAccountNumber(merchantAccount);
第二个:调第三方系统,风控
检查码也是风控给出来的响应码
如果有一天风控他改了
以前用的是模块调用,现在用的是微服务方式做服务化改造,或者是响应码改了,那这里也要改
//调用风控微服务 RiskCode riskCode = riskCheckService.checkPayment(....); //检查交易合法性 if("0000"!= riskCode){ throw new InvalideOperException(); }
然后还有,发送审计信息
现在写的是用kafka用来对接,如果有一天不用kafka了,用mq了,也要改
//发送审计消息 String message = sourceUserId + "," +targetAccountNumber +","+ targetAmount; kafkaTemplate.send(TOPIC_AUDIT_LOG,message); return Result.SUCCESS;
所以这段代码就有非常多的风险,在以后的发展过程中,这种非常有可能膨胀成一个接口里面上前行代码。
传统MVC的代码结构是这样的
使用DDD思想进行改造
首先,我们对DAO进行改造
public class AccountRepositoryImpl implements AccountRepository { @Resource private AccountDao accountDao; @Resource private AccountBuilder accountBuilder; @Override public Account find(Long id){ AccountDO accountDO = accountDao.selectById(id); return accountBuilder.toAccount(accountDO); } @Override public Account find(Long accountNumber){ AccountDO accountDO = accountDao.selectByAccountNumber(accountNumber); return accountBuilder.toAccount(accountDO); } @Override public Account save(Account account){ AccountDO accountDO = accountBuilder.fromAccount(account); if (accountDO.getId() == null){ accountDao.insert(accountDO); }else{ accountDao.update(accountDO); } return accountBuilder.toAccount(accountDO); } }
然后我们对Account 做了个封装
public class Account { private Long id; private Long accountNumber; private BigDecimal available; public void withdraxw(BigDecimal money){ //转入操作 available = available.add(money); } public void deposit(BigDecimal money){ //转出操作 if(available.intValue() < money.intValue()){ throw new InsufficientMoneyException(); } available = available.divide(money); } }
这里面把业务方法也放进去了,这也是DDD里面提出来的,把实体和业务方法封装在一起,构成一个“充血模型” ,以往被我们称为POJO的叫”贫血模型“, 简单来说贫血模型就是带属性和get,set,不带任何业务场景。
这里的模型,是根据业务来设计的,在我们平常的设计当中,往往会把所有的属性放在一个大的实体里面,就比如说account里面还会有姓名,密码等等,都放在这一个大实体里面来。然后通过上层的service做不同的操作,去构成一些业务,这样就会造成“贫血失忆症“,什么叫贫血失忆症呢,就是将所有的属性放在一个实体里后,从这个类上我已经完全看不出,它要做什么事了,但是将这两个方法放进去以后,这个实体要干什么,就一目了然了,以后要改转帐,那好,我只要改这个实体就好了,这就从实体上面就能做到很好的隔离,这就是充血模型。
这就是DDD中强调的概念,业务和实体在一起
这里的实体里面加业务方法和DP的概念是有不同的,DP也是DDD中的一种概念,将隐性的概念显性化,DP的目的是做参数校验。
例如:
public class User { private String email; .... }
public class UserDP { private final User user; public User getUser(){ return user; } public User(User user) throws ValidationException { //验证邮箱格式 String regex = "[a-zA-Z0-9_]+@[a-zA-Z0-9_]+(\\.[a-zA-Z0-9]+)+"; if(user.getEmail().matches(regex)){ throw new ValidationException("邮箱格式不正确!"); } this.user = user; } }
这样写的话,如果需要校验参数,直接调UserDP就好,DP内也是可以提供静态方法给外部调用的。
然后在DDD中对实体也进行了分类,实体和值对象
举个例子,我们要做一个订单,订单里面有订单实体 order,Item订单相关的产品,
Order 是具有id,具有唯一属性,而Item是属于Order的,这是一个整体和部分的关系,order是一个整体,Item是一个部分,它们有一个严格的依赖关系,我们设计的时候就可以在order里面加一个OrderId ,将Item的一些关键属性冗余到order里面,那Item就相当于一个值对象,虽然是个对象,但本质只是个值,这就是实体和值对象的关系,DDD里面你要访问值对象,就一定要通过实体来访问。这样的好处在于你知道这个值出问题了,马上就会想到这个订单。
DDD里业务的理解:指造成实体状态变化的过程,去数据库存储并不会改变实体的变化,例如转入转出,就会使余额发生变化,这就是业务。
然后风控的处理
public class BusiSafeServiceImpl implements BusiSafeService { @Resource private RiskChkService riskChkService; public Result checkBusi(Long userId, Long mechantAccount, BigDecimal money){ //参数封装 RiskCode riskCode = riskChkService.checkPayment(...); if("0000".equals(reskCode.getCode())){ return Result.SUCCESS; } return Result.REJECT; } }
通过接口做一层隔离,也就是风控以后,要怎么调,怎么做,都在BusiSafeServiceImpl实现类里面,这就是DDD提出的另一个概念,防腐层,通过防腐层来隔离当前应用和第三方系统的一些交互,让第三方不影响我们的业务,保证业务的稳定性。
然后kafka的处理
public class AuditMessage { private Long userId; private Long clientAccount; private Long merchantAccount; private BigDecimal money; private Date date; //.... }
public class AuditMessageProducerImpl implements AuditMessageProducer { private KafkaTemplate<String,String> kafkaTemplate; public SendResult send(AuditMessage message){ String messageBody = message.getBody(); kafkaTemplate.send("some topic",messageBody); return SendResult.SUCCESS; } }
也就是之前提到过的,有可能我现在用的是kafka,后面需要用Mq,或者别的一些组件,也是做一个隔离。
最后,具体的业务怎么处理,也就是加钱减钱的操作。
我们也会抽象成
public class AccountTransferServiceImpl implements AccountTransferService { public void tranfer(Account sourceAccount, Account targetAccount, BigDecimal money){ sourceAccount.deposit(money); targetAccount.withdraxw(money); } }
以后如果要打折,要收手续费,那就完全可以控制在这个方法里面。上面这种跨实体的操作,就需要将它抽象成一个方法,抽象成一个服务,这就是DDD的另一个概念,领域服务。
所以我们外界,包括controller对领域的访问,都需要通过领域服务来构建。对外部屏蔽了内部实现。
重新编排后的代码:
public class PayServiceImpl implements PayService { @Resource private AccountRepository accountRepository; @Resource private BusiSafeService busiSafeService; @Resource private AccountTransferService accountTransferService; @Resource private AuditMessageProducer auditMessageProducer; public Result pay(Long userId, String merchantAccount, BigDecimal amount){ //参数校验 Money money = new Money(amount); UserId clientId = new UserId(userId); AccountNumber merchantNumber = new AccountNumber(merchantAccount); //读数据 Account clientAccount = accountRepository.find(clientId); Account merAccount = accountRepository.find(merchantNumber); //交易检查 Result preCheck = busiSafeService.checkBusi(clientAccount,merAccount,money); if (preCheck != Result.SUCCESS){ return Result.REJECT; } //业务逻辑 accountTransferService.transfer(clientAccount,merAccount,money); //保存数据 accountRepository.save(clientAccount); accountRepository.save(merAccount); //发送审计消息 AuditMessage message = new AuditMessage(clientAccount,merAccount,money); auditMessageProducer.send(message); return Result.SUCCESS; } }
整个造成的效果,保证了整个这个代码主体的稳定,会造成变动的代码,都隔离出去,这样后续改动都不会影响业务。所有的变化都隔离开以后,保留的就是真正的核心了,这就是DDD引申的一个思想。
可能很多人,不知道DDD这套方法理论的情况下,也有些地方也是这么处理的,但是很多时候我们如果没有一个好的方法论,那这个接口和实现类的方式就很有可能用的不是很恰当。
举个例子:做业务开发,会要求面向接口编程,也会要求每个controller对应一个service接口,然后这个接口再去做一个实现类,但是,在很多很多情况下,controller只是面向一个具体的业务,你把它隔离了一个service,这个service只是针对一个业务的。这个时候你的service只会有一个实现类,这样改的话,有时候意义不大,甚至画蛇添足,如果没有方法论的指导,controller和service实现类就会变得比较多,体现不出效果,所以这也是DDD的魅力所在,在众多这些方法当中,DDD会提供一个很好的思路,对我们整个团队的业务开发实现了统一。
DDD的四层架构
- 用户接口层:响应外部数据,例如controller,ajx,或者别的协议
- 应用层:组织业务逻辑
- 领域层:由实体和值对象组成,每个领域是有自己的业务核心的,领域与领域之间通过领域服务来进行跨领域的调用
- 基础层:所有业务的支撑
DDD模块
- Application做服务介入
- doman是一个领域
- infrastructure是基础设施层
- interfaces接口
每个领域里面有他的entity实体,有它的repository仓库,还有一些领域服务service
DDD架构和MVC架构的区别
Mvc架构设计的重点是数据库,Data Driven Design
DDD架构设计的重点是具体的服务,Domain Driven Design
在我们去做服务的时候,会自然而然的在各个领域之间形成一种逻辑上的区分,这种区分,在DDD 中称为限界上下文。 如果需要转成微服务架构,可以以限界上下文为依据,把它升级为微服务边界。这样做的好处在于所有的领域都可以独立成一个微服务,这时候微服务就相当于可以做一个组合,你怎么放这些领域都可以。
DDD的简洁架构
这梳理的好像很清楚,但对开发来说,还是没有太多作用,开发需要拿出一个具体的线型架构来还是比较麻烦,怎么落地呢?
中间的核心还是领域层,但是在你设计外部适配的时候,将主动的适配器放在上面那部分,被层为北向网关,被动适配器放在下面这一层,南向网关。
主动适配就需要远程服务层,controller接收外部响应,provider对外去提供服务,subscriber去做订阅发布。下面就是本地服务层,本地服务层只处理调度,不处理任何业务,调度领域层,同样,领域层也不会直接去跟外部资源进行交互,中间也会进行隔离,对接端口层,端口层再把消息发给下面的具体适配器层。这样就形成了一个完整的开发模型,所有的业务都可以按这么几层来组织功能。(中间消息锲约层选用)
这样的好处在于,做完后,整个系统的架构就变成了这样的一个架构
从上层来看,一个客户层(响应外部变化,例:applikation),业务价值层和下面的基础层,全是由旁边的一个个菱形的包来做的,类似项目中一个个domain组成。
DDD下单体架构于微服务架构的统一:单体架构与微服务的区别就在于领域之间的沟通机制,这只是一个防腐层的改变,核心业务领域不需要做任何改动。
DDD的问题与不足
- 学习成本高(采用DDD的话,整个团队就必须要理解DDD)
- 收效缓慢
- 技术落地难(到现在DDD都还没有个完整的技术框架,mvc有Spring,Sping cloud),阿里提出了一个cola整个框架,是DDD具体的一个框架
- DDD 也是动态发展的,(DDD只是一种思想,具体能产生什么价值还是依赖于具体的技术体系,微服务配合DDD价值巨大。)
阿里cola架构
这里就不说cola了,咱也还没学习,有需要的同学可以自行学习。
DDD深度了解
殷浩详解DDD系列
- 殷浩详解DDD系列 第一讲 - Domain Primitive(https://open.atatech.org/articles/146177)
- 殷浩详解DDD系列 第二讲 - 应用架构 (https://open.atatech.org/articles/147553)
- 殷浩详解DDD系列 第三讲 - Repository模式 (https://open.atatech.org/articles/170909)
- 殷浩详解DDD系列 第四讲 - 领域层设计规范 (https://open.atatech.org/articles/188827)
- 殷浩详解DDD系列 第五讲 - 聊聊如何避免写流水账代码 (https://open.atatech.org/articles/203671)
其他作者:
- 聊一聊DDD中的值对象 (https://open.atatech.org/articles/194456)
- 聊一聊DDD应用的代码结构 (https://open.atatech.org/articles/188035)
- 领域驱动设计基础篇 (https://open.atatech.org/articles/214094)
- 领域驱动设计架构篇 (https://open.atatech.org/articles/218072)
关于DDD的两本书:
- 领域驱动设计:软件核心复杂性应对之道
- 实现领域驱动设计 (美)弗农著