web-dev-qa-db-ja.com

コマンドパターンによる大規模/複雑なエンティティの永続化-私はそれを正しく行っていますか?

私は、できれば長く実りある人生を送ることができる、サービスとしての大規模な在庫管理ソフトウェアを設計および構築している最中です。そのため、アプリケーションが保守可能で水平にかたくなるように、事前に多くの努力を払っています。私は本当に楽しんで デカップリングに関するMathias Verraesによるこの講演 は、高度に分離された非常に明示的なコードに役立つため、コマンドパターンを試すことを刺激しました。

テクノロジーに関しては、Laravel 5とDoctrine 2がMySQLデータベースによってサポートされています。

Mathiasは「CRUDの考え方から抜け出す」という主題について語っていますが、ビジネス言語が実際にCRUDを反映している多くの状況が存在することに私たちは皆同意できると思います。たとえば、ロジスティクスマネージャーは「注文書を作成する必要があります。注文書を作成してサプライヤーに送信します」と言います。 「SUBMIT」の部分はコマンドの優れたユースケースとして光りますが、CREATEの部分はそれほどではありません。

誰かが同意/反対し、多分私をより良い方向に向けられることを期待して私の立場を説明しましょう。

Ifeelコマンドパターンは、注文書などの複雑なオブジェクトのCREATEリクエストを処理する行為には特に適していません。私のドメインでは、注文書には次のような情報が含まれています。

  • ユーザー定義の注文番号/識別子(PO1234など)
  • 注文が送信されるサプライヤー
  • 発送先
  • 対処する請求書
  • リクエストされた配送方法と宅配便
  • NET規約
  • 通貨
  • 注文を直送する場合の販売注文と顧客
  • 1つ以上のラインアイテム

私が正しく理解していれば、コマンドオブジェクトは本質的に、比較的単純なDTOであり、アプリケーションと対話するための外部の世界へのコントラクトを提供します。 "新しい注文書を作成する場合は、このオブジェクトにデータを入力してバスに送信します"。開発者として、アプリケーションにCreatePurchaseOrderCommandを発行することは美しく明示的です-私はそれがどのように聞こえるかが好きですが、実行に関しては不器用に感じます。

不器用とは、Requestオブジェクトから上記のデータをすべて外挿し、CreatePurchaseOrderCommand9個以上の引数、そのうちのいくつかは配列、おそらくValueObjectまたは2つです(または結合が導入されますか?)。

次のようなコードをモックアップしました。

[〜#〜] edit [〜#〜]でのCREATEの成功後にJSONエンコードされたオブジェクトを出力する必要性を反映するためにOrderControllerを更新しました私のクライアント側フレームワークのニーズを満たすために。これは良いアプローチと考えられますか?リポジトリを他の読み取りメソッドでほぼ確実に必要とするため、リポジトリをコントローラに挿入することに問題はありません。

OrderController.php

public function __construct(ValidatingCommandBus $commandBus, OrderRepositoryInterface $repository) { .. }
public function create(CreateOrderRequest $request){
     $uuid = UUID::generate();
     $command = new CreatePurchaseOrderCommand($uuid, $request->all());

     try
     {
         $this->commandBus->execute($command);
         $order = $this->repository->findByUuid($uuid);
         return $this->response()->json([
             'success' => true,
             'data'    => $order->jsonSerialize()
         ]);
     }catch(OrderValidationException $e)
     {
         return $this->response()->json([
             'success' => false,
             'message' => $e->getMessage()
         ]);
     }
}

ValidatingCommandBus.php(BaseCommandBusを装飾する)

public function __construct(BaseCommandBus $baseCommandBus, IoC $container, CommandTranslator $translator) { .. }
public function execute(Command $command){
    // string manipulation to CreatePurchaseOrderValidator
    $validator = $this->translator->toValidator($command);

    // build validator class from IOC container
    // validates the command's data, might throw exception
    // does *not* validate set constraints e.g., uniqueness of order number
    // this is answering "does this look like a valid command?"
    $this->container->make($validator)->validate($command) 

    // pass off to the base command bus to execute
    // invokes CreatePurchaseOrderCommandHandler->handle($command)
    $this->baseCommandBus->execute($command)
}

CreatePurchaseOrderCommandHandler.php

public function __construct(PurchaseOrderBuilderService $builder, PurchaseOrderRepositoryInterface $repository){ .. }
public function handle(CreatePurchaseOrderCommand $command){

    // this again? i'm pulling the same data as I pulled from the
    // Request object back in the Controller, now I'm just getting
    // the same data out of the Command object. Seems repetitive...
    $order = $this->builder->build([
       $command->order_number,
       $command->supplier_id,
    ]);

    // now maybe I should handle set constraints?
    // ensure order number is unique, order is not stale... etc.
    $orderNumberIsUnique = new OrderNumberIsUniqueSpecification($this->repository);
    if ( ! $orderNumberIsUnique->isSatisfiedBy($order) ){
        throw new \ValidationException("The Order Number is not unique");
    }

    // ok now I can persist the entity...
    try
    {
        // start a transaction
        $this->repository->persist($order);
    }catch(SomeDbException $e)
    {
        // roll back transaction
        // cant return anything so i'll throw another exception?
        throw new ErrorException('Something went wrong', $e);
    }

    // no return here as that breaks the CommandBus pattern :|
}

コードの観点からは、検証の配置などに関して物事を進めるための論理的な方法のように思えます。しかし、結局のところ、それは適切な解決策のようには感じられません。

また、コマンドバスから値を返すことができないので、永続化する前にUUIDの生成に切り替えるか(現在はMySQLのAUTO INCに依存)、またはother一意の識別子(注文番号)は、すべてのエンティティでは機能しない可能性があります(つまり、すべてのエンティティ/集合体がデータベースIDの他に他の形式の一意の識別子を持つわけではありません)。

コマンドパターンを正しく使用して理解していますか?私の懸念は有効ですか(コード例のコメントを参照)。コマンドパターンについて何か不足していますか?任意の入力が素晴らしいでしょう!!

5
John Hall

DTOをシンプルにする

コンストラクタに9つの引数を持つコマンドがあるのは醜いです。しかし、これらのものをコンストラクタに入れなければならないのですか?フィールドをパブリックにして、空のコンストラクターでコマンドを作成し、フィールドに割り当てるだけです。コンストラクター引数のポイントは、有効なエンティティーが保証されていることです。DTOを使用しているだけで、検証は別のエンティティーによって実行されるため、データを非表示にしたり、複雑なコンストラクターを使用したりすることにはあまり意味がありません。コマンドのパブリックフィールド。

value-DTOに問題はありません

一緒に属する一連のフィールドで構成されるコマンドがある場合、これらをDTOに属する値オブジェクトにマップしてください。これを行うべきではないという厳密な規則はありません。

コマンドは何も返しません

コマンドは、ドメイン内のオブジェクトへの変更です。コマンドバスから何も返す必要はありません。なぜIDを返す必要があるのですか?コマンドで要求されたとおりに注文を作成しました。これで完了です。ただし、考慮すべき例外が1つあります。それは...

検証

Nullではない/空ではない、有効な日付、メールの正規表現などの単純な検証はすべてバリデーターに属します。このバリデーターの呼び出しは、ハンドラーへのコマンドハンドラーの呼び出しが有効なコマンドを受信することが保証される前にチェーンされます。

複雑な検証については、さまざまな意見を検討できます。データベースに触れる必要のある複雑な検証(「ユーザー名は既に取得されています」)も検証ツールに属すべきではないと言う人もいます(リポジトリを必要とする)検証ツールでこの検証をすべて行い、コマンドで検証を実行しないことを好む人もいますなんでも。最後に、コマンドを検証するための失敗を処理するための戦略を決定する必要があります。

設定によっては、コマンドバスからValidationResultsが返され、コマンドバスを呼び出したときにCommandValidationExceptionsをトラップして表示することがあります...

2
JDT

私のアプリでは、ロジックフローは

  • コントローラーがDTOを構築
  • コントローラがコマンドバスにディスパッチします
  • コマンドバス呼び出しイベント
  • イベントハンドラーもコマンドバスにディスパッチします
  • コマンドハンドラーがコントローラーに応答DTOを返します
  • コントローラは応答に基づいてクライアントへの応答を生成します

あなたのアプリケーションは似ているようです。デカップリングも私にとって問題です。ただし、コマンドまたはイベントのチェーン(またはネスト)の問題は問題になりませんでした。可能な場合は、生成されたUUIDと関連するオブジェクトのリストをすべてのコマンドチェーンに保存します。

0
rolfk