コードで学ぶ集約とリポジトリの関係〜DDD実践のためのきほん〜

はじめに

今回はポイント管理システムをテーマに

DDD×クリーンアーキテクチャでサンプルコードを作ったので、

それを使いながら、

集約とリポジトリの関係

についてまとめました。


集約とリポジトリのサンプルコード

今回テーマにしているポイントシステムでは、

店舗からレシート情報とカード、ポイント利用などを連携され、

付与ポイントの加算、ポイント利用の減算、

クーポンの発行とクーポン利用によるポイント付与率の制御ができる

ポイント管理システムを想定しています。

ポイント管理システムのイメージ図


まずは支払登録ユースケースを実現する際に登場する、

支払集約について考えてみましょう。

支払時には購入品、支払手段単位で付与ポイントを算出。

会員がポイントを利用すると会員の残ポイント減算。

を行う必要がある想定です。

支払集約ルート
class Payment(
    val receiptNumber: ReceiptNumber,
    private val paymentDate: PaymentDate,
    private val paymentAmount: PaymentAmount,
    private val paymentMember: PaymentMember,
    private val shopCode: ShopCode,
    private val couponCode: CouponCode?,
    private val paymentMethod: List<PaymentMethod>,
    private val paymentPurchase: List<PaymentPurchase>
) {
    init {
        if (paymentAmount.value != paymentMethod.sumOf { it.paymentAmount.value }) throw IllegalArgumentException("支払金額が不正です")
        if (paymentAmount.value != paymentPurchase.sumOf { it.goodsPrice.value }) throw IllegalArgumentException("支払金額が不正です")
    }
}


支払登録ユースケースは支払は必ずPaymentの単位で取り扱います。

それによって、

支払手段の合計金額と最終的な支払金額の整合性が取れない、

支払手段が外から自由に更新できて最終的な支払金額の整合性が取れない。

などが防げます。


次に支払をデータベースへ永続化する

リポジトリのサンプルコードを見てみましょう。

支払リポジトリ
interface IPaymentRepository {
    fun save(payment: Payment)
}

    @Transactional
    override fun save(payment: Payment) {
        val dataModel = payment.getDataModel()
        dslContext.insertInto(PAYMENT, PAYMENT.RECEIPT_NUMBER, PAYMENT.MEMBER_CODE, PAYMENT.AFFILIATE_COMPANY_CODE, PAYMENT.STORE_CODE, PAYMENT.USED_POINT)
            .values(dataModel.receiptNumber, dataModel.paymentMember.memberCode, dataModel.companyCode, dataModel.shopCode, dataModel.paymentMember.usePoints)

        dataModel.paymentMethod.forEach {
            dslContext.insertInto(PAYMENT_METHOD_DETAIL, PAYMENT_METHOD_DETAIL.RECEIPT_NUMBER, PAYMENT_METHOD_DETAIL.PAYMENT_METHOD_CODE, PAYMENT_METHOD_DETAIL.PAYMENT_AMOUNT, PAYMENT_METHOD_DETAIL.GIVEN_POINT)
                .values(dataModel.receiptNumber, it.paymentMethodCode, it.paymentMethodAmount, it.grantPoint)
        }

        dataModel.paymentPurchase.forEach {
            dslContext.insertInto(PURCHASE_PRODUCT_DETAIL, PURCHASE_PRODUCT_DETAIL.RECEIPT_NUMBER, PURCHASE_PRODUCT_DETAIL.AFFILIATE_COMPANY_CODE, PURCHASE_PRODUCT_DETAIL.PURCHASE_COUNT, PURCHASE_PRODUCT_DETAIL.PRODUCT_UNIT_PRICE, PURCHASE_PRODUCT_DETAIL.GIVEN_POINT)
                .values(dataModel.receiptNumber, dataModel.companyCode, it.purchaseQuantity, it.goodsPrice, it.grantPoint)
        }

        dslContext.update(MEMBER).set(MEMBER.POINT_BALANCE, dataModel.paymentMember.remainingPoints).where(MEMBER.MEMBER_CODE.eq(dataModel.paymentMember.memberCode))
    }


支払を登録する際は、

購入品と支払方法、

支払時の付与ポイント、利用ポイント

をセットで登録し、

会員の残ポイントの更新も行う必要があります。


これらどれか1つだけ登録や更新に失敗した状態で永続化すると、

支払登録の整合性が崩れることになるのです。


集約とリポジトリの関係

ここからは集約とリポジトリの関係について見ていきます。

まず、リポジトリはデータの保管庫です。


先ほどのサンプルコードでは、

データベースにデータを保管しました。

リポジトリは保管庫へデータを登録したり、

取り出したりするのが役目です。


次に集約ですが

複数のドメインオブジェクトに跨って整合性を取る場合、

複数のドメインオブジェクトを集約単位でセットで取り扱います。

もし、状態を更新したい場合は集約ルートを必ず経由して

整合性を保ちながら状態を更新できるようにし、

リポジトリを使って永続化する場合も集約単位で丸ごと永続化します。


今回の例だと、

支払登録ユースケースにおいて、

集約ルートが支払で、

支払手段、購入品、利用ポイント、付与ポイント

を整合性を保ちながらセットで取り扱わせるために集約を使い、

(集約ルートによる状態の更新はありませんが)

集約単位で永続化しました。


今回のテーマについては以上です。

DDD実践のためのきほんシリーズのまとめ記事を作っているので

是非ご覧ください。

初めてDDDを実践する方に向けて〜DDD実践のためのきほん〜


参考文献

第3章 DDD固有のモデリング手法

ドメイン駆動設計 モデリング/実装ガイド


Chapter 5 データにまつわる処理を分離する「リポジトリ」

ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本

Chapter12 ドメインのルールを守る「集約」

ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください