学习ddd(一)--领域驱动设计相关概念

Posted by zhangtao on Friday, April 12, 2024

我之前一直对领域驱动设计(DDD)相关的知识有零散的认识,没有系统性地学习过。最近抽空系统地学习了一下,发现这块知识比较抽象,很难读懂。加上我自己的理解,我整理了一些知识,希望能够分享给大家

第一期先讲些了DDD的一些基础概念

充血模型

在我们以往的开发模式中,Model 对象通常只包含属性变量和 get/set 方法,这种模式被称为“贫血模型”。举个例子,比如订单的作废方法,在传统的做法中,我们会在 Service 层编写业务逻辑,如下所示:

@Data
class Order {
  private Long id;
  private Date cancelDate;
  private Integer Status;
}

@Service
class OrderService {
    // 传统写法,service臃肿
    public void cancel(Long id) {
            Order order = new Order(id);
            order.setCancelTime(new Date());
            order.setStatus(StatusEnum.CANCELED.getCode());
            orderDAO.update(order);
        }
}

如果我们使用充血模型进行改造

@Data
class Order {
  private Long id;
  private Date cancelDate;
  private Integer Status;

  cancelOrder() {
     this.setCancelTime(new Date());
     this.setStatus(StatusEnum.CANCELED.getCode());
  }
}

@Service
class OrderService {
    public void cancel(Long id) {
            Order order = new Order(id);
            order.cancelOrder();
            orderDAO.update(order);
        }
}

通过这个例子可以看出,原本在 OrderService 中执行的业务逻辑被移到了 Order 对象中,使得 Order 成为一个充血模型。

这样的设计有什么好处呢?首先,对于简单项目来说,贫血模型也许没什么问题。但一旦项目复杂起来,大量操作都涉及对 Order 进行取消,如果取消逻辑需要修改,那么代码将无法统一维护,容易出现 bug。 其次,将 Service 中的行为移到 Model 中,使对象除了属性外还具备行为。这样一来,Service 的代码会更加简洁,而 Model 中的行为则成为领域对象的核心逻辑,而不是零散的业务代码,更易于理解业务。

实体与值对象

在领域驱动设计中,有两个重要概念:实体(Entities)与值对象(Value Objects)。如何区分实体与值对象呢?一般来说,实体是具有唯一标识符(ID)的对象,对应着业务概念,拥有属性和行为;而值对象则是没有唯一标识符的对象,通常是多个属性的集合,用来描述实体的状态和特征。

值对象的一个重要优点是可复用性,比如 ItemInfo 值对象可以被多个实体所使用。另一个优点是,值对象内部的属性是一个整体,如果将这些字段拆分到实体中,会导致属性的零散分布,而值对象则能更清晰地将相关属性组织在一起。

对于值对象的持久化,我们可以使用数据库的 JSON 字段来存储。 例如

// 实体
class OrderDetail {
    private Long id;
    private Integer number;
    private Long itemId;
    private ItemInfo itemInfo;

}
// 值对象
class ItemInfo {
    private String name;
    private String description;
    private String brand;
    private String price;

}

聚合、聚合根、边界上下文

在 DDD 中,聚合(Aggregates)、聚合根(Aggregate Roots)和边界上下文(Bounded Context)是非常重要的概念。

聚合是一组相关的实体和值对象的集合,它们之间有着紧密的关联,并且具有相同的生命周期,一起创建一起销毁。比如电商系统中的订单、订单地址、订单明细、商品信息等对象构成一个聚合。当订单被删除时,相关的订单地址、订单明细、商品信息等也会一起被删除。

聚合根则是聚合的根节点,比如在上述例子中,订单可以作为聚合根。我们只能通过聚合根来操作聚合内的对象,这样的好处是当聚合内部的逻辑变更时,只需要修改聚合根内部的逻辑即可,无需影响到外部。

// 聚合根
class Order {
  private Long id;
  private List<OrderDetail> details;
  private OrderAddress orderAddress;
}

上下文边界这个概念由两个单词组成。一些紧密联系业务的聚合组成边界,在这个边界里有着相同的语义。以电商供应链 ERP 系统为例,订单在销售上下文中称为销售订单,在仓储上下文中称为销售出库单,在售后上下文中称为售后单。这样的设计使得每个上下文都可以尽可能少地调用外部上下文的交互,形成高内聚、低耦合的特性,为日后的系统拆分提供了便利。

资源库

资源库(Repository)的概念和之前讲到的聚合根的概念紧密相连。当我们需要对数据库进行访问时,往常我们对通过DAO访问,但这种是针对单个实体。而Repository是针对聚合根的访问,例如针对Order聚合根的OrderRepository

@Repository
public class OrderRepository {
    @Autowired
    private OrderDao orderDao;
    @Autowired
    private OrderDetailDao orderDetailDao;
  
    public Order getOrderById(int id) {
        Order order = orderDao.getById(id);
        if(Order == null){
            return order;
        }
        List<OrderDetail> list = orderDetailDao.getByMainId(id);
        order.setDetails(list);
        return order;
    }
  
   

同时,如果是其他非数据库访问的资源,比如缓存的方式,也可以放在资源库中访问。资源库的好处就是实现了业务跟技术的隔离,双方的职责更加清晰

防腐层

防腐层(Anti-Corruption Layer)是用来干什么的?我们项目中常常会碰到调用外部接口的情况,无论是http api还是dubbo调用,我们都需要处理传入参数和返回值,当我们不想在业务代码中耦合这些逻辑时,我们可以引入防腐层,把调用的逻辑挪到这里来做,将我们的业务参数转换成外部接口参数,以及将返回值转换成我们项目中的对象。

举个例子,原先的代码调用的是orderService.update(order);

@Service
class OrderService {
    public void cancel(Order order) {
        order.setStatus(StatusEnum.CANCELED.getCode());
        orderService.update(order);
    }
}

后续需求改动orderService.update(order)方法需要换成orderDubboService.update(orderRequest),我们可以加入防腐层。这样就实现了业务逻辑和外部调用逻辑的隔离,防止外部接口代码调整导致业务代码改动,这种思想跟我们设计模式中的门面模式或者适配器模式相似


@Service
class OrderService {
    public void cancel(Order order) {
        order.setStatus(StatusEnum.CANCELED.getCode());
        // 引入适配器,代码无入侵
        orderFacade.update(order);
    }
}

// 防腐层
@Service
class OrderFacade {
    public void update(Order order) {
      OrderRequest request = new OrderRequest();
      request.setId(order.getId());
      request.setStatus(order.getStatus());
      orderDubboService.update(orderRequest);
  }
}

领域事件

领域事件指的是业务中某些事情发生后会影响到下一步业务流程的事件。以电商系统为例,订单审核通过后会触发 WMS 生成销售出库单,订单作废时对应的销售出库单也会作废。在开发中,除了直接接口调用外,通常还会使用消息队列(MQ)进行异步调用,尤其是在涉及到领域事件时,使用 MQ 更为合适。MQ 的发布订阅模型天生适配领域事件,而且相比同步调用,异步调用的耦合度更低,能够切断领域之间的依赖关系。

同时,采用 MQ 发送事件时需要考虑一致性问题。可以通过使用事务消息或本地消息表等方式来保障系统的一致性。

Alt text

希望这些基础概念的分享能够对大家有所帮助!后面会做一篇代码实战给大家