DDD场景下仅靠ID跨域获取数据的实现正确性与优化问询
DDD订单域订单项数据组装:当前实现问题与优化方案
当前实现的问题
你的这种写法存在几个明显的问题,完全不符合DDD和干净代码的原则:
- DTO职责越界:OrderItemOutputDto的本职工作就是当个"数据容器",现在在构造函数里直接调用商品域的Facade查数据,把数据查询和DTO的职责混在一起了,违背了单一职责原则。
- 性能隐患:如果处理一个包含多个订单项的订单,每个OrderItemOutputDto都单独发起一次商品查询,直接会出现N+1数据库查询问题,数据量大的时候性能会崩盘。
- 测试难度高:构造函数里硬写了
CatalogProductFacade::getById(),单元测试时没法mock这个依赖,只能依赖真实的商品服务,测试成本极高。 - 领域边界模糊:订单域的DTO直接调用商品域的服务,两个域的耦合度被拉高,不符合DDD中领域边界隔离的核心思想。
所以结论是:当前实现不正确,必须调整。
更优的设计方案
1. 用组装层(Assembler/Factory)抽离查询逻辑
专门创建一个负责组装DTO的类,比如OrderDtoFactory,把查询商品数据、拼接订单项DTO的逻辑都放在这里,让OrderItemOutputDto只专注于存储数据。
示例代码:
class OrderDtoFactory { public function __construct(private readonly CatalogProductFacade $productFacade) {} public function createFromOrder(Order $order): OrderOutputDto { // 先收集所有订单项的商品ID,批量查询——解决N+1问题 $productIds = array_map(fn(OrderItem $item) => $item->productId, $order->items); $products = $this->productFacade->getByIds($productIds); // 把商品转成以ID为键的数组,方便快速查找 $productMap = array_column($products, null, 'id'); $itemDtos = []; foreach ($order->items as $item) { $itemDto = new OrderItemOutputDto( $item->id, $item->productId, $item->price ); // 从批量查询结果中匹配对应商品 if (isset($productMap[$item->productId])) { $itemDto->productDto = $productMap[$item->productId]; } $itemDtos[] = $itemDto; } return new OrderOutputDto($order->id, $itemDtos); } }
然后把OrderItemOutputDto改回纯数据容器,去掉构造函数里的查询逻辑:
class OrderItemOutputDto { public int $id; public int $productId; public ?ProductOutputDto $productDto = null; public float $price; public function __construct(int $id, int $productId, float $price) { $this->id = $id; $this->productId = $productId; $this->price = $price; } // 给组装层提供设置商品DTO的方法 public function setProductDto(?ProductOutputDto $productDto): void { $this->productDto = $productDto; } }
2. 用领域查询服务封装跨域逻辑(贴合DDD实践)
如果想更贴合DDD的规范,可以在订单域里创建一个OrderQueryService,专门负责跨域查询订单相关的扩展数据,内部调用商品域的Facade,再由组装层基于这个服务返回的数据构建DTO:
class OrderQueryService { public function __construct( private readonly OrderRepository $orderRepository, private readonly CatalogProductFacade $productFacade ) {} public function getOrderWithProductDetails(int $orderId): OrderWithProductDetails { // 从订单仓储获取订单实体 $order = $this->orderRepository->findById($orderId); // 批量获取关联商品 $productIds = array_map(fn(OrderItem $item) => $item->productId, $order->items); $products = $this->productFacade->getByIds($productIds); // 返回包含商品详情的只读模型(方便后续组装DTO) return new OrderWithProductDetails($order, $products); } }
3. 替换静态调用,使用依赖注入
不管用哪种方案,都别再直接调用CatalogProductFacade::getById()这种静态方法了,通过构造函数注入Facade实例。这样单元测试时可以轻松替换成Mock对象,也符合依赖倒置原则。
总结
当前实现的核心问题是职责混乱、性能隐患和领域耦合,完全不符合规范。最优的做法是用专门的组装层/工厂类处理跨域数据的查询和DTO拼接,同时通过批量查询解决N+1问题,用依赖注入替代静态调用,保证代码的可测试性和领域边界的清晰。
内容的提问来源于stack exchange,提问作者Lucas André
相关产品推荐
相关产品推荐

