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 がある。