web-dev-qa-db-ja.com

TransportationモジュールのDDDドメインモデリング

私は、ERPタイプシステムの輸送モジュールをC#とEF Coreを使用してモデル化しようとしています。このサービスは、顧客集荷と会社所有のトラック配送の管理を担当します。顧客集荷には注文とトラックのリストにはストップのリストが含まれており、それらのストップにはオーダーのリストが含まれています。

集荷およびトラックを管理するための主要なインターフェースは、RESTベースのAPIを介したものです。注文作成/更新/キャンセルイベントは、サービスバスキューを介して注文モジュールから受信されます。

ビジネスルール

  • 注文の入力時に、注文には属性が割り当てられ、顧客の集荷か、トラックを介した配送かを指定します。ただし、これらの注文を特定の集荷またはトラック/ストップのインスタンスに関連付けるのは、この輸送モジュール内のユーザー次第です。
  • 注文は、一度に1つの集荷またはトラックの停車地にのみ関連付けることができます。
  • 注文には、ステータスと配送指標(寸法、重量)を含むプロパティがあります。
  • トラックでの注文は、特定の量または総重量を超えることはできません。
  • 注文の更新は、配送の割り当てが行われた後でも、いつでも受け取ることができます。これらの更新により、出荷タイプ(ユーザーが注文を受け取るか、出荷するかを決定する)や、出荷メトリックを変更する注文ビルドを変更できます。配送タイプが変更された場合、注文は現在の集荷またはトラックストップに関連付けられていないはずです。
  • トラックのステータスは、開いている/輸送中/閉じている、トラックの各ストップは開いている/配達済みのステータスです。
  • ユーザーは、すべての注文のステータスが製造済みである場合にのみ、トラックを輸送中としてマークできます。
  • トラックが輸送中の場合、配達が完了すると、ユーザーは各ストップに配達済みのマークを付けます。すべてのストップが配達済みとしてマークされた後にのみ、トラックのステータスをクローズに更新できます。
  • 注文がキャンセルされた場合、関連付けられている可能性のある集荷またはトラックから自動的に削除する必要があります。

個々のエンティティを格納するために、リレーショナルデータベース(SQL Server)を使用しています。私の質問は、これらのさまざまな集約ルート/エンティティ/値オブジェクト/ドメインサービス/ドメインイベント、およびそれらをサポートするデータベーステーブルをモデル化する方法を中心に考えています。

最初の考え

  • Pickup、Truck、Orderのルートを集約します。
  • ピックアップには、リンクされた注文IDを含む値オブジェクトのリストがあります。
  • トラックには、ストップエンティティのリストと、リンクされた注文ID、注文ステータス、注文配送メトリックスを含む値オブジェクトのリストがあります。
  • その他のオプションの検討-ヌル可能外部キーを直接追加して、ピックアップまたはトラックストップを参照するように注文します。追加のオプションは、注文を集荷またはトラックストップにマップするOrderAssociationsテーブルを用意することです。

トラックにビジネスルールを適用するために、ここで少し面白くなります。注文をトラックに追加する場合、他のすべての注文の総重量と体積を考慮に入れる必要があり、注文によってトラックが規定のしきい値を超える場合はエラーが返されます。すでにトラックに関連付けられている注文の注文更新を受信し、その注文によりトラックが許容しきい値を超える場合、アラートを発する必要があり、問題が解決するまでトラックを輸送中としてマークできません。

質問

  • 注文の更新が受信されると、それが集荷またはトラックストップに関連付けられているかどうかを知る必要があります。関連付けられている場合は、そのオブジェクトに通知する必要があります。 PickupOrdersとTruckStopOrdersに別々のテーブルがある場合、両方のテーブルに対してクエリを実行する必要があるため、関連付けを決定することは正確には効率的ではありません。さらに、トラックの集約で更新注文メソッドを呼び出すには、ストップ/注文のトラック全体をロードする必要があります。この注文の更新をどのように処理することをお勧めしますか?これは、注文エンティティ自体とトラックを更新するアプリケーションレベルのイベントハンドラですか?注文エンティティは更新され、トラックに何らかの形で通知されるドメインイベントが発生しますか?このロジックがドメインレイヤーにあるのか、アプリケーションレイヤーにあるのかを知りたいです。
  • トラックは値オブジェクトまたはエンティティを停止しますか?それらはトラックのコンテキスト内にのみ存在します。ただし、これらには関連付けられたステータス(オープン/配信済み)と関連付けられた注文のリストがあります。
  • 注文が関連付けられているピックアップまたはトラックの停車場への参照を維持することは、注文にとって素晴らしいことです。ただし、これを行うと、トラックのストップ/集荷と注文が連動するため、注文の更新では同じトランザクションで両方を更新する必要があります。これがドメインサービスまたはアプリケーション層を介して管理されるかどうか不明ですか?
  • 個別のトラックストップ注文テーブルを保持している場合、このテーブルの注文ステータス/メトリックを注文テーブルと同期させる最良の方法は何ですか?

必要に応じて追加の説明を提供させていただきます。ご意見をお待ちしております。

5
jwyoung

一般的な経験則として(特にトラック全体をロードする効率が気になるため)、アグリゲートを小さく保ち、1つのトランザクションで1つのアグリゲートのみを更新する必要があります。これは、複数のアグリゲートにまたがるビジネスルールが最終的にのみ整合性を持つことを意味しますが、これは通常、実際的な問題を引き起こしません。

これに基づいて、現在の設計を裏返しにすることを検討する場合があります。注文のリストを含む集荷およびトラックストップの代わりに、モデルをモデル化するファーストクラスの集約(たとえばPickupOrderおよびTruckStopOrder)を作成します。集荷またはトラックストップへの注文の関連付け。

注文コンテキストハンドルUpdateOrderCommand

  • Order集合体を更新します。
  • OrderUpdatedイベントを発行します。

トラックコンテキストがOrderUpdatedイベントを処理します:

  • TruckStopOrder集合体を更新します。
  • TruckStopOrderUpdatedイベントを発行します。

トラックコンテキストがTruckStopOrderUpdatedイベントを処理します:

  • Truck集合体の総体積/総重量を再計算します。
  • 制約に違反した場合にアラートを発行します。

トラックに注文を追加するためのUIは、Truck集計からキャッシュされた情報を使用して、トラックがすでに満車の場合に警告を表示できます。並行システムでは、他のユーザーが同時にトラックを更新している可能性があるため、これが一貫していることは決して保証できません。

1
casablanca

enter image description here

これをドメインモデル化する方法を次に示します。通常、ビジネス要件をコアオペレーションに分解します。これらのコアオペレーションを特定したら、それらのオペレーションをホストするサブドメインを特定します。

これらの操作を成功させるために必要なタイプを特定する方が簡単なので、最初に操作を特定することをお勧めします。

注:

OOP C#やJavaなどの言語が汎用であるからといって、それらがビジネスドメインのモデル化に理想的な言語であるとは限りません。したがって、静的に型付けされるFP言語は、構文が少なく、情報密度が高いため、ビジネスドメインのモデリングに自然に適合します。

データの永続性に関しては、リレーショナルデータベースは理想的ではないと思います。イベントストア(つまり、不変データベース)を使用して、データを上書きまたは削除できないようにします。結局のところ、このドメインは、更新または削除してはならない(追加のみの)履歴ドメインイベントを操作することです。

ドメインの説明から、次のモデルを提供しました。

CustomerOrder.Operations

namespace CustomerOrder

open Shared
open Language

module Operations =

    type PlaceOrder        = PlaceOrderSubmission        -> AsyncResult<unit,Error>
    type ChangeAcquisition = ChangeAcquisitionSubmission -> AsyncResult<unit,Error>

CustomerOrder.Language

module Language

open Shared

type AuthenticatedCustomer = TODO

type AcquisitionType = string // Ex: CustomerPickup | TruckDelivery

type PlaceOrderSubmission = {

    AuthenticatedCustomer : AuthenticatedCustomer
    Order                 : Order
    OrderRequestType      : AcquisitionType
}

type ChangeAcquisitionSubmission = {

    OrderSubmission       : PlaceOrderSubmission
    NewAcquisitionRequest : AcquisitionType
}

OrderDispatcher.Operations

namespace OrderDispatcher

open Shared
open Language

module Operations =

    type AvailableTruckers      = AvailableTruckersRequest         -> AsyncResult<TruckersOpen,Error>
    type DispatchTrucker        = DispatchTruckerSubmission        -> AsyncResult<unit,Error>
    type CancelledOrder         = OrderCancellationSubmission      -> AsyncResult<OrderCancellationReceipt,Error>
    type ChangeOrderAcquisition = OrderAcquisitionChangeSubmission -> AsyncResult<UnstartedOrder,Error>

OrderDispatcher.Language

module Language

open Shared

type TruckerId = string

type Trucker = {
    TruckerId : TruckerId
}

type DispatchTruckerSubmission = {
    Trucker : Trucker
    Order   : Order
}

type AvailableTruckersRequest = {
    Order   : Order
}

Trucker.Operations

namespace Trucker

open Shared
open Language

module Common =

    type QueryUnstartedOrders = AuthenticatedTrucker -> AsyncResult<UnstartedOrders,Error>

module NewOrderPending =

    type AcceptOrderRequest  = OrderResponseSubmission -> AsyncResult<unit,Error>
    type DeclineOrderRequest = OrderResponseSubmission -> AsyncResult<unit,Error>
    type ForfeitOrderRequest = OrderResponseSubmission -> AsyncResult<unit,Error>

module AcceptedOrder =

    type CancelAcceptance  = CancellableOrder         -> AsyncResult<unit,Error>
    type StartInTransit    = OrderProduced            -> AsyncResult<InTransitToPickupTrucker,Error>
    type InTransitToPickup = InTransitToPickupTrucker -> AsyncResult<IntransitToPickupOrder  ,Error>

    //----------------------------------------------------------------------------------------
    // Handle change of how order is acquired (i.e. pickup or delivery)
    //----------------------------------------------------------------------------------------
    type MyDelegate = delegate of obj * OrderCancelled -> unit

    type IOrderCancelled =
        [<CLIEvent>]
        abstract member OrderCancelled : IEvent<MyDelegate, OrderCancelled>

    type IncomingNotification () =
        let orderCancelled = new Event<MyDelegate, OrderCancelled> ()

        interface IOrderCancelled with
            [<CLIEvent>]
            member x.OrderCancelled = orderCancelled.Publish
    //----------------------------------------------------------------------------------------

module InTransitToDropoff =

    type CancelAcceptance   = InTransitToDropoffTrucker -> AsyncResult<OrderCancellationReceipt,Error>
    type InTransitToDropoff = InTransitToDropoffTrucker -> AsyncResult<IntransitToDropoffOrder ,Error>
    type ClaimDelivered     = InTransitToDropoffTrucker -> AsyncResult<OrderClosed          ,Error>

module OrdersCompleted =

    type CloseTruck = CloseTruckSubmission -> AsyncResult<ClosedTruckReceipt,Error>

Trucker.Language

module rec Language

open Shared

type TruckerStatus =
    | Open      of OpenedTrucker
    | InTransit of InTransitTrucker
    | Completed of CompletedTrucker

type AcceptedOrder = { 
    Trucker : OpenedTrucker
}

type IntransitToPickupOrder = { 
    Trucker : InTransitTrucker
}

type IntransitToDropoffOrder = { 
    Trucker : InTransitTrucker
}

type CompletedOrder = {
    Trucker : CompletedTrucker
}

type OrderResponseSubmission = {
    OpenedTrucker  : OpenedTrucker
    UnstartedOrder : UnstartedOrder
    Response       : Response
}

type InTransitTrucker = {
    Trucker        : AuthenticatedTrucker
    CurrentOrder   : OrderProduced
    OrdersProduced : OrderProduced seq
    OrdersClosed   : OrderProduced seq
}

type InTransitToPickupTrucker = {
    Trucker : AuthenticatedTrucker
    Order   : OrderInTransit
}

type InTransitToDropoffTrucker = {
    Trucker : AuthenticatedTrucker
    Order   : OrderInTransit
}

type CompletedTrucker = {
    Trucker      : AuthenticatedTrucker
    OrdersClosed : OrderProduced seq
}

type ArrivedAtDropoffSubmission = {
    Trucker : InTransitTrucker
}

type CancellableOrder =
    | OpenedTrucker    of OpenedTrucker
    | InTransitTrucker of InTransitTrucker

type CloseTruckSubmission = {
    OrdersClosed : OrderClosed seq
}

type ClosedTruckReceipt = {
    OrdersClosed : OrderClosed seq
}

共有言語

module Shared

type AsyncResult<'T,'error> = Async<Result<'T,'error>>
type Error = string

type OrderId         = string
type TruckerId       = string
type CustomerId      = string
type ItemId          = string
type Name            = string
type Description     = string
type Response        = string
type Address         = string
type Weight          = float
type feet            = float
type AcquisitionType = string
type CancellationId  = string
type OrderStatus     = string
type Dimensions = {
    Length : feet
    Width  : feet
    Height : feet
}

type AuthenticatedTrucker = {
    TruckerId : TruckerId
}

type OpenedTrucker = {
    Trucker : AuthenticatedTrucker
}

type Item = {
    ItemId      : ItemId
    Name        : Name
    Description : Description
    Weight      : Weight
    Dimensions  : Dimensions
}

type ItemQty = {
    Item : Item
    Qty  : int
}

type ItemQtys = ItemQty seq

type Pickup = {
    Address  : Address
    ItemQtys : ItemQtys
}

type Customer = {
    CustomerId : CustomerId
    Address    : Address
}

type Order = {
    OrderId    : OrderId
    Customer   : Customer
    Pickup     : Pickup
    Status     : OrderStatus
}

type OrderProduced = {
    Order : Order
}

type OrderInTransit = {
    OrderProduced : OrderProduced
}

type OrderClosed = {
    OrderInTransit : OrderInTransit
}

type OrderCancelled = {
    Order : Order
}

type OrderCancellationSubmission = {
    Order  : Order
    Reason : string
}

type OrderCancellationReceipt = {
    CancellationId : CancellationId
    Order          : Order
    Reason         : string
}

type OrderAcquisitionChangeSubmission = {
    Order           : OrderCancellationReceipt
    AcquisitionType : AcquisitionType
}

type UnstartedOrder = { Order: Order }

type UnstartedOrders = UnstartedOrder seq
1
Scott Nimrod