用 DDD 连接点

问题描述 投票:0回答:2

我阅读了 Evans、Nilsson 和 McCarthy 等人的著作,并了解领域驱动设计背后的概念和推理;但是,我发现很难将所有这些组合到现实世界的应用程序中。 缺乏完整的例子让我摸不着头脑。 我找到了很多框架和简单的示例,但到目前为止还没有真正演示如何遵循 DDD 构建真正的业务应用程序。

以典型的订单管理系统为例,以订单取消为例。 在我的设计中,我可以看到带有 CancelOrder 方法的 OrderCancellationService,该方法接受订单 # 和原因作为参数。 然后它必须执行以下“步骤”:

  1. 验证当前用户是否具有取消订单所需的权限
  2. 从 OrderRepository 中检索具有指定订单号的 Order 实体
  3. 验证订单是否可以取消(服务是否应该询问订单的状态以评估规则,或者订单是否应该具有封装规则的 CanCancel 属性?)
  4. 通过调用 Order.Cancel(reason) 更新 Order 实体的状态
  5. 将更新后的订单保存到数据存储中
  6. 联系信用卡服务以恢复任何已处理的信用卡费用
  7. 为操作添加审核条目

当然,所有这些都应该发生在一个事务中,并且任何操作都不允许独立发生。我的意思是,如果我取消订单,我必须恢复信用卡交易,我不能取消订单而不执行此步骤。在我看来,这建议更好的封装,但我不想在我的域对象(订单)中依赖 CreditCardService,所以这似乎是域服务的责任。

我正在寻找有人向我展示代码示例,如何可以/应该“组装”。代码背后的思考过程将有助于我自己将所有点联系起来。 谢谢!

domain-driven-design ddd-repositories ddd-service
2个回答
2
投票

稍微不同的看法:

//UI
public class OrderController
{
    private readonly IApplicationService _applicationService;

    [HttpPost]
    public ActionResult CancelOrder(CancelOrderViewModel viewModel)
    {
        _applicationService.CancelOrder(new CancelOrderCommand
        {
            OrderId = viewModel.OrderId,
            UserChangedTheirMind = viewModel.UserChangedTheirMind,
            UserFoundItemCheaperElsewhere = viewModel.UserFoundItemCheaperElsewhere
        });

        return RedirectToAction("CancelledSucessfully");
    }
}

//App Service
public class ApplicationService : IApplicationService
{
    private readonly IOrderRepository _orderRepository;
    private readonly IPaymentGateway _paymentGateway;

    //provided by DI
    public ApplicationService(IOrderRepository orderRepository, IPaymentGateway paymentGateway)
    {
        _orderRepository = orderRepository;
        _paymentGateway = paymentGateway;
    }

    [RequiredPermission(PermissionNames.CancelOrder)]
    public void CancelOrder(CancelOrderCommand command)
    {
        using (IUnitOfWork unitOfWork = UnitOfWorkFactory.Create())
        {
            Order order = _orderRepository.GetById(command.OrderId);

            if (!order.CanBeCancelled())
                throw new InvalidOperationException("The order cannot be cancelled");

            if (command.UserChangedTheirMind)
                order.Cancel(CancellationReason.UserChangeTheirMind);
            if (command.UserFoundItemCheaperElsewhere)
                order.Cancel(CancellationReason.UserFoundItemCheaperElsewhere);

            _orderRepository.Save(order);

            _paymentGateway.RevertCharges(order.PaymentAuthorisationCode, order.Amount);
        }
    }
}

备注:

  • 一般来说,只有当命令/用例涉及多个聚合的状态更改时,我才认为需要domain服务。例如,如果我需要调用 Customer 聚合和 Order 上的方法,那么我将创建域服务 OrderCancellationService 来调用这两个聚合上的方法。
  • 应用层在基础设施(支付网关)和域之间进行协调。与领域对象一样,领域服务应该只关心领域逻辑,而不关心支付网关等基础设施;即使您使用自己的适配器对其进行了抽象。
  • 关于权限,我会使用面向方面的编程将其从逻辑本身中提取出来。正如您在我的示例中看到的,我已向 CancelOrder 方法添加了一个属性。您可以在该方法上使用拦截器来查看当前用户(我将在 Thread.CurrentPrincipal 上设置)是否具有该权限。
  • 关于审计,您只是简单地说了“对运营进行审计”。如果您只是指一般审计(即针对所有应用程序服务调用),我将再次在方法上使用拦截器,记录用户、调用哪个方法以及使用哪些参数。但是,如果您的意思是专门针对取消订单/付款进行审核,那么请执行类似于德米特里示例的操作。

2
投票

您的域服务可能如下所示。请注意,我们希望在实体中保留尽可能多的逻辑,从而保持域服务的精简。另请注意,不直接依赖信用卡或审核员实施 (DIP)。我们仅依赖于域代码中定义的接口。稍后可以将实现注入到应用程序层中。应用程序层还将负责按编号查找订单,更重要的是,负责在事务中包装“取消”调用(异常时回滚)。

class OrderCancellationService {

   private readonly ICreditCardGateway _creditCardGateway;
   private readonly IAuditor _auditor;

   public OrderCancellationService(
       ICreditCardGateway creditCardGateway, 
       IAuditor auditor) {
       if (creditCardGateway == null) {
           throw new ArgumentNullException("creditCardGateway");
       }
       if (auditor == null) {
           throw new ArgumentNullException("auditor");
       }
       _creditCardGateway = creditCardGateway;
       _auditor = auditor;
   }

   public void Cancel(Order order) {
       if (order == null) {
           throw new ArgumentNullException("order");
       }
       // get current user through Ambient Context:
       // http://blogs.msdn.com/b/ploeh/archive/2007/07/23/ambientcontext.aspx
       if (!CurrentUser.CanCancelOrders()) {
           throw new InvalidOperationException(
           "Not enough permissions to cancel order. Use 'CanCancelOrders' to check.");
       }
       // try to keep as much domain logic in entities as possible
       if(!order.CanBeCancelled()) {
            throw new ArgumentException(
            "Order can not be cancelled. Use 'CanBeCancelled' to check.");
        }
        order.Cancel();

        // this can throw GatewayException that would be caught by the 
        // 'Cancel' caller and rollback the transaction
        _creditCardGateway.RevertChargesFor(order);
            
        _auditor.AuditCancellationFor(order);
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.