领域驱动架构设计

前言

一个好的应用架构,都应该遵循一些共同模式,不管是六边形架构、洋葱圈架构、整洁架构,都提倡以业务为核心,解耦外部依赖,分离业务复杂度和技术复杂度等。随着业务复杂度提高,传统的MVC模式极易造成换乱的大泥球。领域驱动设计是一种以业务领域为依据自上而下的设计思路,对于降低服务之间的耦合和微服务的划分提供了良好的理论和实战参考。

目标

领域驱动设计的目标是通过关注领域模型(而不是技术)来创建更好的软件。达到松耦合,高内聚,减少软件的复杂度。

实现步骤

DDD(领域驱动设计)是一种软件开发方法论,它关注的是软件系统的业务领域,以此为中心进行设计和开发。要落地DDD,可以遵循以下步骤:
1.研究业务领域:了解业务领域的特点和需求,与业务专家进行沟通,获取业务知识和需求。
2.定义领域模型:根据业务领域的特点和需求,定义领域模型,包括实体、值对象、聚合和领域服务等。
3.实现领域模型:根据定义好的领域模型,使用编程语言实现领域模型。
4.设计领域驱动的架构:根据领域模型,设计领域驱动的架构,包括应用服务、领域服务、聚合根等。
5.实现领域驱动的架构:根据领域驱动的架构,实现软件系统。
6.使用测试来验证:使用单元测试和集成测试等方法验证软件系统是否符合业务需求。
7.持续优化:持续地对软件系统进行优化,包括对领域模型和领域驱动的架构进行优化,以及对代码进行重构等。
需要注意的是,DDD并不是一种简单的技术,它需要开发人员具备较高的业务领域知识和设计能力。同时,DDD的落地需要团队的共同努力和领导层的支持。

核心概念

基本

实体:实体的方法应该与实体的职责相关,表达实体的行为和状态。
实体工厂:创建领域对象。将复杂的创建过程封装,领域对象的创建变得简单易用。使用工厂方法,抽象工厂或者builder构建器创建。
值对象:没有唯一标识符的一些相关属性的集合,具有不可变性,不具备行为能力。比如日期,地址,金额等。
聚合根:包含相关联的实体和值对象。一个聚合根通常具有唯一的标识符(ID),对聚合根的修改都通过聚合根修改,保证整个聚合根的一致性和完整性。
领域服务:通常与一个或多个实体或值对象交互,以完成一些复杂的业务操作,领域服务一般不与外界进行直接交互。
应用服务:可能涉及到多个聚合根或实体,主要用来协调领域模型和外部世界之间的交互。
上下文:在一个特定场景或用例中,一组相关联的概念,规则和实体的集合。一个上下文包含领域的一部分,是个狭义的概念,通常是指某个具体的业务场景。
防腐层:与外部系统(如第三方库,其他服务,外部数据源等)与内部领域模型进行隔离的一层。适配器,数据转换,反射等实现方式。
贫血模型:一个类中只有属性或者成员变量。贫血模式会带来贫血失忆症。
充血模型:一个类中除了属性和成员变量,还有方法。

高级配合玩法

领域事件:
基于领域模型设计,业务的关键步骤,实现事件驱动和复用,领域事件的处理一般是异步的,通过发布订阅模式或者观察者模式实现。命名规则:领域名称+动词的一般过去式+Event。例如:LocationUpdatedEvent.java
CQRS:
命令查询分离的架构模式(DDD理论的一种实践)。当代码要并行执行时:由于没有副作用,查询可以并行化而没有任何问题,但命令不能,查询和增删改操作分离,避免加锁,避免读取数据的时候需要判断是否线程安全,很好的映射支持数据库读写分离。CQRS设计最大优势是与DDD 和事件溯源配合,可以将技术代码和业务代码分离。
事件溯源:
记录每个事件的状态,保证最终一致性,统一的事件流(事件日志)能够很自然实现事务机制,无需额外ACID机制或2PC之类同步强硬方式。

支付功能的DDD实战

传统MVC实现支付功能设计

PaymentController.java

public class PaymentController
  @Autowired
  private PayService payService;
  public Result pay(String account ,BigDecimal amout){
      Long userId = (Long)session.getAttribute("userId");
      return payService.pay(userId,account,amount);
  }
}

PayServiceImpl.java

public class PayServiceImpl implements PayService{
    @Autowired
    private AccountDao accountDao;//数据库操作
    @Autowired
    private KafkaTemplate<String,String> kafkaTemplate;//kafka操作
    @Autowired
    private RiskCheckService riskCheckService;//风控服务接口
    private final String TOPIC_AUDIT_LOG = "audit_log";
    public Result pay(Long userId, String account,BigDecimal amount){
     //1.从数据库中读取账户数据
     AccountDO clientDO = accountDao.selectByUserId(userId);
     AccountDO merchantDO = accountDao.selectByAccountNumber(account);
     //2.业务参数校验
     if(amount>clientDO.getAvailable()){
       throw new NoMoneyException();
     }
     //3.调用风控微服务
     RiskCode riskCode = riskCheckService.checkPayment(....);
     //4.检查交易合法性
     if("0000"!=riskCode){
       throw new InvalideOperException();
     }
     //5.计算新值,并且更新字段
     BigDecimal newSource = clientDO.getAvailable.subtract(amount);
     BigDecimal newTarget = merchantDO.getAvailable.add(amount);
     clientDO.setAvailable(newSource);
     merchantDO.setAvailable(newTarget);
     //6.更新到数据库
     accountDao.update(clientDO);
     accountDao.update(merchantDO);
     //7.发送审计信息
     String message = sourceId+","+account+","+account;
     kafkaTemplate.send(TOPIC_AUDIT_LOG,message);
     return Result.SUCCESS;
    }
}

未遵循的设计原则

1.单一职责原则:一个类只有一个引起它变化的原因。负责支付业务这个实现类应该只有支付业务发生变化的时候才会改动。
2.开闭原则:对扩展开放,对修改关闭。
3.依赖反转原则:程序之间要依赖于抽象的接口,而不是具体实现。即面向接口编程。

DDD改造过程

1 使用充血模型的实体对象,描述核心业务能力,实体所承载的业务回归到实体中,系统能做什么,一目了然。

Account.java

//账户实体类
@Entity
public class Account{
    @Id
    private Long id;
    private Long accountNumber;//账户号
    private BigDecimal available;//账户余额
    //转入操作
    public void transferIn(BigDecimal money){
       available = available + money;
    }
    //转出操作
    public void transferOut(BigDeciamal money){
          if(money>available){
             throw new InvalideOperException();
          }
          available = available - money;
    }
}

2 数据库是实现细节,使用仓库,封装实体化操作,摆脱数据库的限制。

上层依赖AccountRepository接口,不管具体的数据库操作实现,类比现实场景:我们去4S店买车,不关心车从哪里制造生产还是从其他店调配过来,客户最终拿到一个完整车就行。业务上不关心关联表,使用工厂可以封装实体的复杂创建和组装过程。

AccountRepository.java

public interface AccountRepository{
    .....
}
public Class AccountRepositoryImpl implements AccountRepository{
    @Autowired
    private AccountDao accountDao;
    @Autowired
    private AccountBuilder accountBuilder;//账户实体工厂
    @Override
    public Account find(Long id){
        AccountDO accountDO = accountDao.selectById(id);
        return accountBuilder.toAccount(accountDO);
    }
    @Override
    public Account find(Long accountNum){
        AccountDO accountDO = accountDao.selectByAccountNumber(accountNum);
        return accountBuilder.toAccount(accountDO);
    }
    @Override
    public Account save(Account account){
        AccountDO accountDO = accountBuilder.findAccount(account);
        if(accountDO == null){
            accountDao.insert(accountDO);
        }else{
            accountDao.update(accountDO);
        }
        return accountBuilder.toAccount(accountDO);
    }
}

3.构建防腐层,隔离外部服务。摆脱技术框架的限制,提供无限的可能。

BusiSafeGetwayImpl.java

public interface BusiSafeGateway{
    ....
}
public class BusiSafeGatewayImpl implements BusiSafeGateway{
    @Autowired
    private RiskCheckService riskCheckService;
    public Result checkBusi(Long userId,Long account,BigDecimal money){
        RiskCode riskCode = riskCheckService.checkPayment(...);
        if("0000".equals(riskCode.getCode())){
            return Result.SUCCESS;
        }
        return Result.REJECT;
    }
}

AuditMessageProducerImpl.java

//审计消息值对象
public class AuditMessage{
    private Long userId;
    private Long sourceAccount;
    private Long targetAccount;
    private BigDecimal amount;
    private Date date;
    ....
}
public Interface AuditMessageProducer{
    ....
}
public AuditMessageProducerImpl implements AuditMessageProducer{
    private kafkaTemplate<String,String> kafkaTemplate;
    public Result send(AuditMessage auditMessage){
        String messageBody = message.getBody();
        kafkaTemplate.send("topic name",messageBody);
        return Result.SUCCESS;
    }
}

4.领域服务体现的是业务的能力,是实体(提供个体能力)的组合。将业务动作和个人动作隔离开。

AccountTransferServiceImpl.java

public interface AccountTransferService{
    void transfer(Account sourceAccount,Account targetAccout,BigDecimal money);
}
public class AccounTransferServiceImpl implements AccountTransferService{
    public void transfer(Account sourceAccount,Account targetAccount,BigDecimal money){
        sourceAccount.transferOut(money);
        targetAccount.transferIn(money);
    }
}

5.应用服务转账服务,最终主体的业务代码,转账流程清晰,转账本身核心业务变动,此类才会修改,达到了高内聚和低耦合,对扩展开放,对修改关闭。

即使不懂代码的业务人员看到代码也能清晰的了解到转账业务的整体步骤:1.加载数据。2.交易检查。3.转账业务。4.保存数据。5.发送审计消息。屏蔽了实现细节,面向接口的编程操作。如果变更实现方式不会修改转账流程业务逻辑代码。

payServiceImpl.java

public Class PayServiceImpl implements payService{
    @Autowired
    private AccountRepository accountRepository;
    @Autowired
    private BusiSafeGateway busiSafeGateway;
    @Autowired
    private AccountTransferService accountTransferService;
    @Autowired
    private AuditMessageProducer auditMessageProducer;
    public Result pay(Account client,Account target,Money money){
        //1.加载数据
        Account clientAccount =  accountRepository.find(client.getId());
        Account targetAccount =  accountRepository.find(target.getId());
        //2.交易检查
        Result preCheck = busiSafeGateway.checkBusi(client.getId(),target.getId(),money);
        if(preCheck != Result.SUCCESS){
            return Result.REJECT;
        }
        //3.转账业务
        accountTransferService.transfer(clientAccount,targetAccount,money);
        //4.保存数据
        accountRepository.save(clientAccount);
        accountRepository.save(targetAccount);
        //5.发送审计消息
        AuditMessage auditMessage = new AuditMessage(clientAccount,targetAccount,money);
        auditMessageProducer.send(auditMessage);
        return Result.SUCCESS;
    }
}

总结

DDD提供的是思想指导,不局限怎么实现。所有的设计都是刚刚好解决目前的业务能力,不做过多的提前设计,未来的方式由未来的新实现解决。

posted @ 2023-03-20 15:34  曹化金  阅读(62)  评论(0编辑  收藏  举报