qiuxuhui

导航

DDD简洁模型 介绍

DDD是什么?

一个领域驱动设计,面向大型系统架构思想,项目越大,使用DDD收益越大。

 

为什么要使用DDD架构?

举个例子,以前有很多老系统,用的是老环境,老的开发思想,导致如果需要重构的话,会发现有很多困难。

 

例如:

  1. 沟通难

一个项目大了以后,开发人员,产品可能已经换过好几轮了,产品提出一个“小需求”,在产品那边可能觉得这只是个小需求,开发却要做很久,这时候就会有一个问题,这个系统到底是你懂还是我懂?

 

  1. 开发难

对开发人员来说,最痛苦不是让我开发一个项目,而是看别人代码,尤其是如果一个类上千行代码,里面一堆if-else,这怎么看?谁能告诉我这段代码有什么用?能不能去掉?也不敢去,因为也不确定这个代码的影响范围有多少。

 

  1. 测试难

牵一发而动全身,改了一个小需求,测试需要组织庞大的测试计划,甚至可能需要通宵

 

  1. 创新难

例如我学到很多比较新,比较潮流的技术,可是项目中就没办法用,例如以前用的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);
    }

}

 

这样的好处在于,就像之前说的,我的数据库要改技术,从hibernate改为mybatis,通过这一层repository就可以实现数据库的隔离,就能保证这里的orm的技术实现不会影响业务,要换也只是换repository的实现类,整体业务部需要变,这就是DDD里面提出的一个仓库的概念,只管去仓库拿数据,并不关心数据从何而来。
 

然后我们对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的两本书:

  • 领域驱动设计:软件核心复杂性应对之道
  • 实现领域驱动设计 (美)弗农著

 

 

 

posted on 2021-12-06 11:21  qiuxuhui  阅读(1342)  评论(0编辑  收藏  举报