[DDD Start!] ドメイン(2)-ドメインモデル、エンティティ、および価値のエクスポート


ドメインモデルのエクスポート


ドメインをモデリングする際の最も基本的なタスクは、モデルを構成するキー要素、ルール、機能を理解することです.この過程は要求から始まる.
これは注文ドメインに関する要件です.

注文項目を表すOrderLineは、少なくとも注文する商品、商品の価格、購入個数、購入項目の購入価格を提供しなければならない.
data class OrderLine(
    val product: Product,
    val price: Int,
    val quantity: Int,
    val amounts: Int
)
このように作成したモデルは,他の開発者と議論する過程でも共有され,需要を精錬する.モデルを共有する場合は、ホワイトボードやウィキペディアなどのツールを使って、誰でも簡単にアクセスできるようにしたほうがいいです.

ドキュメント化


ドキュメント化を行う主な理由は、知識を共有するためです.
機能リストまたはモジュール構造全体、構築中にコードを表示および直接理解するよりも、このレベルで整理されたドキュメントを参照すると、ソフトウェア全体を迅速に理解できます.コードで構造全体を理解し、より深く理解する必要がある部分を分析すればよい.
コードを表示することでドメインを深く理解できるため、コード自体もドキュメント化されたオブジェクトになります.ドメインの知識をよりよく理解するためにコードを記述しない場合は、コードの動作手順を説明できますが、ドメインの観点からコードを記述する理由を理解するのに役立ちません.コードの可読性が良いだけでなく、ドメインの観点から言えば、コードがドメインをよく表現できるだけで、コードの可読性が向上し、コードはドキュメントとして意味がある.

エンティティと価値


エクスポートされたモデルは、エンティティと価値に大きく分けることができます.

ドメインを正しく設計および実装するには、エンティティと価値を正しく区別する必要があります.

エンティティ


エンティティの最大の特徴は、識別子を持つことです.
エンティティの識別子は一意(ex.受注エンティティの受注番号)であるため、2つのエンティティオブジェクトの識別子が同じである場合、2つのエンティティは同じとみなすことができる.

エンティティの識別子の作成


エンティティ識別子を作成する時点は、ドメインの特徴と使用するテクノロジーによって異なります.通常、識別子は次のいずれかの方法で生成されます.
  • 特定の規則に従って
  • を生成する.
  • UUID
    val uuid: UUID = UUID.randomUUID()
  • を使用する.
    直接入力
  • のシリアル番号(シリアル番号またはDBの自動増分コラムを使用)
  • を使用します.
    自動増分カラム以外の方法では、識別子を作成してから、エンティティオブジェクトを作成するときに識別子を渡すことができます.ただし、自動増分カラムはDBテーブルにデータを挿入して値を知る必要があるため、テーブルにデータを追加するまで識別子を知ることはできません.

    バリュー・タイプ


    Value Typeは、コンセプト上完全な1つを表現する場合に使用します.
    明確な意味を表すために、価値型を使う場合もあります.
    data class Money(val value: Int)
    
    data class OrderLine(
        val product: Product,
        val price: Money,
        val quantity: Int,
        val amounts: Money
    )
    コードの理解に役立つ「お金」を表すお金のタイプを作成します.お金のタイプによって、価格や金額が金額を意味することがわかりやすい.
    Value Typeを使用するもう一つの利点は、Value Typeに機能を追加できることです.お金のタイプはお金を計算する機能を増やすことができます.
    data class Money(val value: Int) {
        fun add(money: Money): Money {
            return Money(this.value + money.value)
        }
        fun multiply(multiplier: Int): Money {
            return Money(this.value + multiplier)
        }
    }

    価値のあるオブジェクトのデータを変更する場合、既存のデータを変更するよりも、変更されたデータを持つ新しい価値のあるオブジェクトを作成するほうが好きです.
    data class Money(val value: Int) {
        fun add(money: Money): Money {
            return Money(this.value + money.value)
        }
    }
    moneyクラスのadd()メソッドに基づいて,新しいmoneyを生成している.
    お金のようにデータ変更機能を提供しないタイプを不変と呼ぶ.バリュー・タイプが変わらない最大の理由は、不変タイプを使用してより安全なコードを記述できることです.
    お金がsetValue()などの方法で値を変更できる場合、参照の透明性に関する問題が発生する可能性があります.

    したがって、この場合、新しい金銭オブジェクトを作成するためにコードを記述する必要があります.

    お金が変わらない場合は、これらのコードを記述する必要はありません(データをコピーする新しいオブジェクトを作成します).moneyのデータは変更できないため、パラメータが渡すpriceを安全に使用できます.
    不変オブジェクトには、参照の透明性とスレッドの安全性の特徴があります.
    エンティティ・タイプの2つのオブジェクトが同じかどうかを比較するには、通常識別子が使用されます.
    2つの価値のあるオブジェクトを比較する場合は、すべてのプロパティが同じかどうかを比較します.

    エンティティ識別子と価値タイプ


    エンティティ識別子の実際のデータは、通常Stringなどの文字列で構成されます.
    お金は単純な数字ではなく、ドメイン内の「お金」を指すように、この識別子は単純な文字列ではなく、ドメイン内では通常特別な意味を持つため、識別子に提供されるタイプを使用してその意味をよりよく表示することができます.
    たとえば、StringではなくOrder No Valueタイプを使用して受注番号を表す場合、このフィールドはタイプによって受注番号であることがわかります.
    data class Order(
        val id: OrderNo,
        ...
    )

    setメソッドをドメインモデルに入れない


    get/setメソッドを習慣的に追加するのはよくありません.特にsetメソッドは、ドメインのコア概念または意図をコードから消滅させる.
    class Order {
        fun setShippingInfo(newShipping: ShippingInfo) {}
        fun setOrderState(state: OrderState) {}
    }
    setShippingInfo()メソッドは、配送先の値を設定するだけですが、配送情報を変更するかどうかは不明です.
    setOrder State()メソッドでは、ステータス値のみを変更するか、ステータス値とともに他の処理に使用するコードを実装するかは決定されません.
    setメソッドのもう一つの問題は、ドメインオブジェクトを作成するときに完全ではない可能性があることです.

    上のコードは発注者の設定を漏らしています.orderがnullの場合、order.setState()メソッドは、商品準備中の状態に変更できます.
    ドメインオブジェクトが不完全な状態にならないようにするには、作成時に必要な情報を指定する必要があります.
    すなわち、必要なすべてのデータをジェネレータで受信する必要がある.
    val order = Order(orderer, lines, shippingInfo, OrderState.PREPARING)
    コンストラクション関数の作成時に必要なすべてのコンテンツを受信した場合は、次のように呼び出し時に必要なデータが正しいかどうかをすぐに確認できます.
    class Order {
        fun Order(orderer: Orderer, orderLines: List<OrderLine>, shippingInfo: ShippingInfo, state: OrderState) {
            setOrderer(orderer)
            setOrderLines(orderLines)
                ...
        }
        private fun setOrderer(orderer: Orderer) {
            if (orderer == null) throw IllegalArgumentException("no orderer")
            this.orderer = orderer
            calculateTotalAmounts()
        }
        private fun setOrderLines(orderLines: List<OrderLine>) {
            verifyAtLeastOneOrMoreOrderLines(orderLines)
            this.orderLines = orderLines
        }
        private fun verifyAtLeastOneOrMoreOrderLines(orderLines: List<OrderLine>) {
            if (orderLines.isEmpty()) {
                throw IllegalArgumentException("no orderLines")
            }
        }
        private fun calculateTotalAmounts() {
            this.totalAmounts = orderLines.stream().mapToInt{(it.amounts)}.sum()
        }
    }
    ここで、setメソッドのアクセス範囲はprivateであり、クラスでデータを変更するために使用されます.privateなのでsetメソッドを使用して外部でデータを変更することはできません.
    不変の価値タイプを使用すると、価値タイプではsetメソッドは自然に実装されません.
    set法を実施する特別な理由がなければ,不変型の利点を発揮するために,不変型を用いて実現する.

    DTOのget/setメソッド


    以前から使用されていた計画作業では、要求パラメータやDBコラムの値を設定する際にsetメソッドが必要であったため、DTOでget/setメソッドを実装せざるを得なかった.ただし、最近のフレームワークでは、設定方法ではなく、プライベートフィールドに直接値を割り当てる機能が提供されています.したがって、setメソッドを提供する必要がなければ、最大限に実施しないほうがよい.これによりDTOも不変オブジェクトとなり,不変の利点をDTOに拡張できる.