5

Evans、Nilsson、McCarthy などを読み、ドメイン駆動設計の背後にある概念と理由を理解しています。ただし、これらすべてを実際のアプリケーションにまとめるのは難しいと感じています。完全な例がないため、頭を悩ませています。多くのフレームワークと簡単な例を見つけましたが、これまでのところ、DDD に従って実際のビジネス アプリケーションを構築する方法を実際に示したものはありません。

一般的な注文管理システムを例に、注文キャンセルの場合を考えてみましょう。私のデザインでは、注文番号と理由をパラメーターとして受け入れる CancelOrder メソッドを持つ OrderCancellationService を見ることができます。次に、次の「手順」を実行する必要があります。

  1. 現在のユーザーが注文をキャンセルするために必要な権限を持っていることを確認します
  2. OrderRepository から、指定された注文番号の Order エンティティを取得します
  3. 注文がキャンセルされる可能性があることを確認します (サービスは注文の状態を調べてルールを評価する必要がありますか? または、注文にルールをカプセル化する CanCancel プロパティが必要ですか?)
  4. Order.Cancel(reason) を呼び出して、Order エンティティの状態を更新します。
  5. 更新された注文をデータ ストアに保持する
  6. CreditCardService に連絡して、処理済みのクレジット カード請求を元に戻す
  7. 操作の監査エントリを追加します

もちろん、これはすべてトランザクション内で発生する必要があり、どの操作も独立して発生することは許可されません。つまり、注文をキャンセルした場合、クレジット カード取引を元に戻す必要があります。キャンセルすることはできず、この手順を実行しません。これは、imo、より良いカプセル化を示唆していますが、ドメイン オブジェクト (Order) の CreditCardService に依存したくないので、これはドメイン サービスの責任のようです。

これをどのように「組み立てる」ことができるか/すべきかのコード例を見せてくれる人を探しています。コードの背後にある思考プロセスは、すべての点を自分で結び付けるのに役立ちます。どうも!

4

2 に答える 2

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);
    }
}
于 2012-05-14T15:41:56.437 に答える
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);
        }
    }
}

ノート:

  • 一般に、ドメインサービスの必要性は、コマンド/ユース ケースに複数の集約の状態変更が含まれる場合にのみ見られます。たとえば、Customer 集計と Order 集計でメソッドを呼び出す必要がある場合は、両方の集計でメソッドを呼び出すドメイン サービス OrderCancellationService を作成します。
  • アプリケーション層は、インフラストラクチャ (支払いゲートウェイ) とドメインの間で調整を行います。ドメイン オブジェクトと同様に、ドメイン サービスはドメイン ロジックのみに関係する必要があり、支払いゲートウェイなどのインフラストラクチャは無視する必要があります。独自のアダプターを使用して抽象化した場合でも。
  • アクセス許可に関しては、アスペクト指向プログラミングを使用して、ロジック自体からこれを抽出します。この例でわかるように、CancelOrder メソッドに属性を追加しました。そのメソッドでインターセプターを使用して、現在のユーザー ( Thread.CurrentPrincipalで設定する) がその権限を持っているかどうかを確認できます。
  • 監査に関しては、単純に「オペレーションの監査」とおっしゃいました。一般的な監査 (つまり、すべてのアプリ サービス呼び出し) を意味するだけの場合は、メソッドでインターセプターを使用し、ユーザー、どのメソッドが呼び出されたか、どのパラメーターを使用してログに記録します。ただし、注文/支払いのキャンセル専用の監査を意味する場合は、Dmitry の例と同様のことを行います。
于 2012-05-17T09:34:29.970 に答える