TL;DR

  • 外から送られてくる値は信用できないので、入力値の妥当性チェックは、アプリケーション層の責務である。
  • チェックは入力モデル(DTO)のコンストラクタで行い、入力モデルをイミュータブルにして、一度チェックを通れば有効な状態が保たれるようにする。

内容

アプリケーション層の責務

アプリケーション層の外から送られてきた値は信用できず、不正な値が持ち込まれる可能性があるので、著者は、入力値の妥当性チェックをアプリケーション層の責務と考えている。

チェックは入力モデル(DTO)で行う。入力モデルをイミュータブルなオブジェクトにすれば、一度チェックを通った値は有効な状態のまま保たれる。似た考え方にドメインプリミティブがある。入力モデルは、ドメイン駆動設計における腐敗防止層のような役割を果たす。例は、ユースケースの概要で出てきた SendMoneyCommand である。

Go での実装

以下は、本書のサンプルを Go で書き直したものである。構造体名に Command と付いているが、Command パターンとは関係がない。由来は、DDD で状態を変える操作を表すコマンドである。

// buckpal/application/port/in/send_money_command.go
type SendMoneyCommand struct {
	SourceAccountID model.AccountID
	TargetAccountID model.AccountID
	Money           model.Money `validate:"positiveMoney"`
}

妥当性チェックには github.com/go-playground/validator/v10 を使う。インスタンスはイミュータブルにしたいが、Go ではイミュータブルなインスタンスを作れない。こういう場合は値レシーバと非公開フィールド(先頭が小文字)を使うのが普通だが、validator のチェックが非公開フィールドには走らないので、フィールドを公開している。

金額には「0 より大きい」というビジネスルールを適用したい。しかし model.Money は独自型なので、カスタムバリデーションを登録する。

// buckpal/common/validator.go
func positiveMoney(fl factValidator.FieldLevel) bool {
	money, ok := fl.Field().Interface().(model.Money)
	if !ok {
		return false
	}
	moneyInt := (*big.Int)(&money)
 
	return moneyInt.Sign() >= 0
}
 
validator.RegisterValidation("positiveMoney", positiveMoney)

入力値のチェックはコンストラクタで行う。Go にはコンストラクタがないので、代わりに NewSendMoneyCommand 関数の中で行う。

// buckpal/application/port/in/send_money_command.go
func NewSendMoneyCommand(
	sourceAccountID model.AccountID,
	targetAccountID model.AccountID,
	money model.Money,
) (*SendMoneyCommand, error) {
	command := &SendMoneyCommand{
		Money:           money,
		SourceAccountID: sourceAccountID,
		TargetAccountID: targetAccountID,
	}
 
	if err := common.Validate(command); err != nil {
		return nil, err
	}
 
	return command, nil
}

validator への依存をできるだけ小さくするため、common パッケージで定義した関数を通して間接的に使っている。ただし、構造体には validate タグが残るので、依存を完全には剥がせない。わずかな依存を加えるだけで時間や労力を大きく節約できるなら、その依存を使っても問題はない。

キーワード

自分の理解

validator のチェックを走らせつつ、フィールドを非公開にしてイミュータブルに近づける方法があれば知りたい。関連する issue として go-playground/validator#417 がある。