ドメインマクロ
Scala 3のopaque type、given / using、コンパイル時derivationは繰り返しパターンの符号化に使う道具であり、欠けているドメインモデリングを隠すためのものではない。同型が3つ以上あり、手書きがdriftしうるときだけ導入を検討する。
不変条件の本体は ドメインモデリング、境界での検証は 境界防御 と整合させる。
型を先に、マクロは次
Section titled “型を先に、マクロは次”Scala 3はopaque type、given / using、コンパイル時derivationを提供する。繰り返しで安定したパターンにだけ使う — 欠けているドメインモデリングを隠すためではない。マクロライブラリや包括的な derives Codec を追加する前に、同型が3つ以上あり、手書きがdriftしうることを確認する。
derivation向き:
- フィールド名とdefaultが文書化された境界DTO codec
- outbox向けに安定した
name/versionメタデータが必要なドメインevent case class - companionで共有するnon-empty / format検証付きID newtype
derivation向きでない:
- 一度きりのビジネスルールやフィールド横断検証
- state machine遷移 — state type上の明示メソッドとして保つ
- 生成コードやマクロ展開内の
throw、.get、panicの隠蔽
内部マクロの前に既存ライブラリを使う
Section titled “内部マクロの前に既存ライブラリを使う”| ニーズ | 推奨 | 備考 |
|---|---|---|
| 検証付きプリミティブ | ライブラリガイド(refined)、iron | 不変条件をソースで可視に保つ |
| 境界での JSON | ライブラリガイド(circe) と明示 codec | ドメイン ID に derives Decoder を避ける |
| 単純ボイラープレート | Scala 3 derives Eq, Show(境界 DTO のみ) | 不変条件を持つ domain state には使わない |
| 繰り返し event メタデータ | domain モジュール内の internal inline given または小さなマクロ | event が同一形状を共有するときのみ |
チームがパターンを所有し、ライブラリがserdeやORMの懸念をドメイン型へ漏らさない形で契約を表現できないとき、myapp.domain.macros のような内部マクロまたはメタプログラミングモジュールを導入する。
ID にはマクロより opaque type
Section titled “ID にはマクロより opaque type”マクロの前に、検証付きcompanionを持つモジュールスコープopaque typeを優先する:
object TaxiRequestDomain: opaque type RequestId = String
object RequestId: def apply(raw: String): Either[DomainError, RequestId] = if raw.trim.isEmpty then Left(DomainError.EmptyId("request_id")) else Right(raw.trim)
extension (id: RequestId) def value: String = idドメインモデリング を参照。検証なしのpublicコンストラクタを生成するマクロは、レビューで明示companionより悪い。
推奨内部パターン
Section titled “推奨内部パターン”event メタデータヘルパー
Section titled “event メタデータヘルパー”outboxとprojectionパイプラインで使うeventレコードを標準化する:
trait DomainEvent: def name: String def version: Int
final case class DriverAssigned( requestId: RequestId, driverId: DriverId, occurredAt: OccurredAt) extends DomainEvent: def name = "taxi.driver_assigned" def version = 1多くのeventが同一形状を共有するとき、小さな inline def eventName[T <: DomainEvent] または内部マクロで name / version を生成してよい — ただしpayloadフィールドは明示的でレビュー可能に保つ。スキーマ進化が文書化されていない限り、event payloadに無制限Circe codecをderiveしない(サービス境界 参照)。
繰り返し match アーム向け宣言ヘルパー
Section titled “繰り返し match アーム向け宣言ヘルパー”フルマクロが重いとき、projection handlerの重複を減らすローカル inline ヘルパー:
inline def dispatchEvent[E <: DomainEvent]( event: StoredEvent, handlers: PartialFunction[String, StoredEvent => Either[ProjectionError, Unit]]): Either[ProjectionError, Unit] = handlers.lift(event.name).toRight(ProjectionError.UnknownEvent(event.name)).flatMap(_(event))ヘルパーはeventを所有するcrate内に留める。サービス境界越しにmacro DSLをexportしない。
Circe / Config derivation ルール
Section titled “Circe / Config derivation ルール”- DTO とwire formatにcodecをderiveし、
Eitherでドメイン型にマップする。 - プロジェクトがleaf検証を文書化していない限り、opaque domain IDやstate structに
derives Decoderしない(境界防御 参照)。 - ドメイン不変条件を持つ型への
Configuration.deriveに注意 — configは境界であり、ドメインfactoryは依然として検証すべき。
生成コードのレビュー期待
Section titled “生成コードのレビュー期待”- 生成またはderiveされたinstanceはpublic mutableフィールド、
nulldefault、不変条件を迂回する黙示coercionを追加しない。 - eventとIDの
toString/Showはログ安全のまま(ロギングとメトリクス 参照)。 - 型にどの挙動がderiveされ、構築時にどの検証が走るかを文書化する — 特にマクロ展開companionについて。
derive やマクロを使わない場合
Section titled “derive やマクロを使わない場合”- フィールド横断検証(amount + currency、日付範囲)
- state machine遷移 —
WaitingRequest、EnRouteRequestなどへの明示メソッド - ORM rowマッピング — インフラで明示mapperを使う(ORM アダプタ 参照)
- JNI / native structマッピング — 境界での明示変換