Search code examples
domain-driven-designddd-repositories

what is the best practice to recover domain object from storage in DDD


In the process of DDD practice, i found that there was a mismatch between domain objects and persistent objects. I understand that DDD recommends that domain objects only expose the key fields and domain methods, and changes in state are accomplished through the constructor or domain methods.

But for the implementation of the repository, what we retrieved from the database is the persistent object PO. When I convert this PO into a domain object DO, I find that I have no way to set the private fields in the DO. Perhaps calling the domain method to set it is feasible, but what if the domain method has other operations, such as sending an event? I can't trigger the sending of an event when I recover, what is the best practice here?

Our implementation of the storage is based on mybatis, in order to reduce complexity, I will not introduce event sourcing temporarily, and the team cannot control it currently.

// simple domain object
@Getter
public class Order extends AggregateRoot<Order> {

    private Long id;
    private String orderSn;
    private Long userId;
    private BigDecimal totalAmount;
    private BigDecimal paidAmount;
    private OrderStatus status = OrderStatus.CREATED;

    private String address;
    private Date orderTime;

    private List<OrderLine> lines = new ArrayList<>();

    public void payOrder() {
        this.status = OrderStatus.PAID;
        publish(OrderPaidEvent)
    }

    //... other domain methods
}

// and repository impl
public Order findById(long id) {
    DemoOrderPo orderPo = demoOrderMapper.selectById(id);
    Wrapper<DemoOrderLinePo> query = Wrappers.<DemoOrderLinePo>lambdaQuery()
                .eq(DemoOrderLinePo::getOrderId, id);
    List<DemoOrderLinePo> orderLinePoList = demoOrderLineMapper.selectList(query);
    Order order = new Order();
    // can not set private fields, what can i do in best practise?
}

Solution

  • The usual answer is you would normally use an "initializer" - which is to say that you would have a constructor that accepts as arguments the information that you need, and the body of the constructor would assign the private members.

    Order(/* your args here */) {
      this.id = // ...
      this.orderSn = // ...
      // and so on
    }
    

    Essentially, the initializer is a factory method with the special privilege to assign data members that would otherwise be inaccessible.

    From there, you will normally use some flavor of factory method, builder, or parser to convert the general purpose data structures that you receiving from your persistent store to create the arguments you need to pass to the initializer.

    In your example, where you already have methods producing DTOs, you only need to bridge the gap between the DTOs and the initializer arguments

    public Order findById(long id) {
        DemoOrderPo orderPo = demoOrderMapper.selectById(id);
        Wrapper<DemoOrderLinePo> query = Wrappers.<DemoOrderLinePo>lambdaQuery()
                    .eq(DemoOrderLinePo::getOrderId, id);
        List<DemoOrderLinePo> orderLinePoList = demoOrderLineMapper.selectList(query);
        
        return makeMeAnOrder(id, orderPo, orderLinePoList)
    }
    
    Order makeMeAnOrder(long id, DemoOrderPo orderPo, List<DemoOrderLinePo> orderLinePoList) {
         // ....
    
         return new Order(
           id,
           orderSn,
           userId,
           // ....
         )
    }
    

    // now I might do it this way, but i feel pain in this way。

    Yup, that's pretty gross. It will sometimes happen that we are locked into bad design decisions made elsewhere, and that suffering some compromises in our new designs is a preferable alternative to the avalanche of rework that would be required to address the real problem.

    So we do it. But we don't have to be happy about it.