💡

CQRS/ES 俺的ベストプラクティス①

2025/02/15に公開

Eventはできるだけシンプルにすること

イベント内部でも値オブジェクトを使ったほうが直感的によさそうな気もしますが、イベントの形式はプリミティブな値だけで構成しましょう。

理由1 - イベントはそもそも検証しなくていいもの

イベントはコマンドの段階で完璧に検証され、システムに不整合な値が含まれていないという前提のもとpublish(イベントストアに発行)されるものです。

なので、イベントストアから取り出す際に、わざわざ値オブジェクトに詰め替えなおしてもう一度検証する意味はほぼないのです。

理由2 - 値オブジェクトのリファクタリングができなくなる

これはイベントとは原則イミュータブルな物として考えなくてはならないからです。
一度イベントストアにシリアライズを経て保存されたイベントは基本的に後から変更してはいけません。

もしイベント内に値オブジェクトがふんだんに使用されている状態で、値オブジェクトのリファクタリングが発生しイベントのスキーマが変更されてしまうとデシリアライズできなくなってしまい、イベントが再生できなくなってしまうのです。

ダメな例

data class ProductCreatedEvent(
    val id: ProductId,
    val brandId: BrandId,
    val name: ProductName,
    val description: ProductDescription,
    val imageUrl: ProductImageURL?,
    val price: ProductPrice,
)

もしも、上のイベントがJson形式にシリアライズされイベントストアに保存された場合以下のようになります。

※ 値オブジェクトの中身は全部 **value**というプロパティだけを持っていると仮定します。

{
  "id": {
    "value": "example-id"
  },
  "brandId": {
    "value": "example-brand-id"
  },
  "name": {
    "value": "example-name"
  },
  "description": {
    "value": "example-description"
  },
  "imageUrl": {
    "value": "example-image-url"
  },
  "price": {
    "value": "example-price"
  }
}

無理やり感がありますが、ここで以下のProductPriceという値オブジェクトをリファクタリングとしてプロパティ名valuevalueeeeeeeに変えたとします。

data class ProductPrice(val value: Int) {
    companion object {
        private const val MIN = 1
        private const val MAX = 1_000_000_000
    }

    init {
        if (value !in MIN..MAX) {
            throw ValueObjectException("商品価格は[$MIN~$MAX]の間です。value:[$value]")
        }
    }
}

↓↓↓↓↓↓↓

data class ProductPrice(val valueeeeeee: Int) {
    // 中身は同上
}

こうしてしまうと、イベントストアに保存されたイベントのスキーマと、現在のイベントクラスのスキーマが異なっているので再生ができなくなってしまうのです。

OKな例

data class ProductCreatedEvent(
    val id: String,
    val brandId: String,
    val name: String,
    val description: String,
    val imageUrl: String?,
    val price: Int,
)

イベントはプリミティブに保とう!

Discussion