DDD 中的 Assembler(装配器):对象转换的桥梁

在领域驱动设计(DDD)中,Assembler(装配器) 是负责不同层级对象之间转换的关键组件。其核心职责是实现领域对象(如实体、值对象)与数据传输对象(DTO)、数据库实体(DO)等不同类型对象之间的属性映射与转换,同时在转换过程中保持各层架构之间的解耦。

Assembler 的核心价值

在 DDD 分层架构中,不同层次所使用的对象模型承担着不同的职责:

  • 领域层:使用领域对象(实体、值对象),封装核心业务逻辑与规则。
  • 应用层与接口层:使用DTO(Data Transfer Object,数据传输对象),专注于数据传递。
  • 基础设施层:使用数据库实体(DO),专注于数据持久化。

Assembler 的核心价值主要体现在以下三个方面:

  • 隔离模型:隔离不同层的对象模型,避免层级间的紧耦合。
  • 集中管控:集中管理对象转换逻辑,提高代码的复用性与可维护性。
  • 隐藏细节:隐藏对象转换的复杂细节,使各层能够聚焦于自身的核心职责。

Assembler 的主要职责

  1. 领域对象与 DTO 之间的转换

    • 将领域对象(实体/值对象)转换为 DTO,供应用层和接口层使用。
    • 将 DTO 转换为领域对象或命令对象(Command),供领域层处理。
  2. 领域对象与数据库实体(DO)之间的转换

    • 将领域对象转换为数据库实体,用于数据持久化操作。
    • 将数据库实体转换为领域对象,用于后续业务处理。
  3. 处理复杂对象关系

    • 处理聚合对象内部的嵌套转换。
    • 处理集合类型对象的批量转换。
    • 处理不同命名规范和数据类型的映射差异。

Assembler 的实现方式

1. 手动实现的 Assembler

最为常见的方式是创建专门的 Assembler 类,手动编写对象转换逻辑。这种方式控制力最强,适合转换逻辑复杂或需要特殊处理的场景。

// 订单领域对象
public class Order {
    private OrderId id;
    private UserId userId;
    private Money totalAmount;
    private List<OrderItem> items;
    private OrderStatus status;
    // 业务方法...
}

// 订单 DTO
public class OrderDTO {
    private String orderId;
    private String userId;
    private BigDecimal totalAmount;
    private String currency;
    private List<OrderItemDTO> items;
    private String status;
    // getter 和 setter...
}

// 订单 Assembler
public class OrderAssembler {
    
    // 领域对象转 DTO
    public OrderDTO toDTO(Order order) {
        OrderDTO dto = new OrderDTO();
        dto.setOrderId(order.getId().getValue());
        dto.setUserId(order.getUserId().getValue());
        dto.setTotalAmount(order.getTotalAmount().getAmount());
        dto.setCurrency(order.getTotalAmount().getCurrency().getCode());
        dto.setStatus(order.getStatus().getName());
        // 处理集合转换
        dto.setItems(order.getItems().stream()
            .map(this::toOrderItemDTO)
            .collect(Collectors.toList()));
        return dto;
    }
    
    // DTO 转领域对象
    public Order toDomain(OrderDTO dto) {
        Order order = new Order(
            new OrderId(dto.getOrderId()),
            new UserId(dto.getUserId()),
            new Money(dto.getTotalAmount(), Currency.of(dto.getCurrency())),
            OrderStatus.fromName(dto.getStatus())
        );
        
        // 添加订单项
        if (dto.getItems() != null) {
            dto.getItems().forEach(itemDTO -> 
                order.addItem(toOrderItem(itemDTO))
            );
        }
        
        return order;
    }
    
    // 订单项转换(辅助方法)
    private OrderItemDTO toOrderItemDTO(OrderItem item) {
        // 转换逻辑...
        return null; 
    }
    
    private OrderItem toOrderItem(OrderItemDTO itemDTO) {
        // 转换逻辑...
        return null;
    }
}

2. 使用工具类的 Assembler

可以利用对象映射工具(如 MapStruct、ModelMapper)来简化转换逻辑,减少样板代码。以下示例展示了如何使用 MapStruct 注解定义 Assembler。

// 使用 MapStruct 注解的 Assembler
@Mapper(componentModel = "spring")
public interface OrderAssembler {
    //  standalone 模式下使用,Spring 模式下通常由容器注入
    OrderAssembler INSTANCE = Mappers.getMapper(OrderAssembler.class);
    
    @Mapping(source = "id.value", target = "orderId")
    @Mapping(source = "userId.value", target = "userId")
    @Mapping(source = "totalAmount.amount", target = "totalAmount")
    @Mapping(source = "totalAmount.currency.code", target = "currency")
    @Mapping(source = "status.name", target = "status")
    OrderDTO toDTO(Order order);
    
    @Mapping(source = "orderId", target = "id.value")
    @Mapping(source = "userId", target = "userId.value")
    // 复杂对象构建使用 expression
    @Mapping(target = "totalAmount", expression = "java(new Money(dto.getTotalAmount(), Currency.of(dto.getCurrency())))")
    @Mapping(source = "status", target = "status", qualifiedByName = "statusFromString")
    Order toDomain(OrderDTO dto);
    
    // 自定义转换方法
    @Named("statusFromString")
    default OrderStatus statusFromString(String status) {
        return OrderStatus.fromName(status);
    }
}

Assembler 的设计原则

  1. 单一职责原则:一个 Assembler 只负责一类对象的转换(例如 OrderAssembler 仅处理订单相关对象)。
  2. 无业务逻辑原则:Assembler 仅负责属性映射,不应包含任何业务逻辑判断。
  3. 双向转换能力:通常需要实现正向(领域对象 → DTO)和反向(DTO → 领域对象)的转换能力。
  4. 空值安全:充分考虑源对象或属性为空的情况,避免抛出空指针异常(NPE)。
  5. 命名规范:通常采用「对象名 + Assembler」的命名方式(如 OrderAssemblerUserAssembler)。

Assembler 与其他组件的关系

  • 与 Repository:Assembler 常被 Repository 实现类调用,用于领域对象和数据库实体(DO)之间的转换。
  • 与 Application Service:应用服务使用 Assembler 在领域对象和 DTO 之间进行转换,以便跨层传递数据。
  • 与 Controller:控制器可能直接使用 Assembler 将 DTO 转换为命令对象,或将处理结果转换为响应 DTO 返回给前端。

何时需要使用 Assembler

  • 当对象转换逻辑复杂,包含嵌套对象或特殊转换规则时。
  • 当需要在多个地方复用相同的转换逻辑时。
  • 当希望保持领域对象的纯净性,避免领域对象中包含与其他对象的转换方法时。
说明:对于简单的对象转换,有时也会直接在 DTO 中实现 fromDomain()toDomain() 方法。但对于复杂场景,使用专门的 Assembler 是更优的选择。

总之,Assembler 在 DDD 中扮演着对象转换桥梁的角色。通过合理使用 Assembler,可以有效隔离不同层次的对象模型,保持各层架构的独立性与内聚性。