値オブジェクトとは
ある値とそれに強く関係する操作をひとまとめにしてオブジェクトとする設計手法です。高い凝集度を実現することができ、最も簡単に始められるDDD(ドメイン駆動設計)のプラクティスの1つだと個人的には思っています。
例えば敵と戦うタイプのRPGでは各キャラクターにHP(ヒットポイント)が割り当てられています。この場合HP(値)とそれに強く関係する操作やロジックを1つのオブジェクトに詰め込みます。Goだとこんな感じに書けます。
package hitPoint
import "fmt"
type HitPoints struct {
value int
}
func NewHitPoints(value int) *HitPoints {
if value < 0 {
panic("hit points must be non-negative")
}
return &HitPoints{value}
}
func (hp *HitPoints) Value() int {
return hp.value
}
func (hp *HitPoints) Subtract(amount int) *HitPoints {
newHP := hp.value - amount
if newHP < 0 {
newHP = 0
}
return NewHitPoints(newHP)
}
func (hp *HitPoints) Add(amount int) *HitPoints {
newHP := hp.value + amount
return NewHitPoints(newHP)
}
func (hp *HitPoints) String() string {
return fmt.Sprintf("%d HP", hp.value)
}
一応以下のような問題点がこのコードにはあるのですが、ある値(HP)とそれに関係するバリデーションロジックや操作を一まとめにするイメージを掴んで頂くために瑣末な問題だとして目を瞑ることにします。
- コンストラクタでpanicではなくerrorを返すべき
- Valueメソッドの戻り値がイミュータブルになってない
そして値の操作では自分自身のオブジェクトを新たに作成して返すことで、常に生成された値がバリデーションを通過していることが保証されるので安心して値を使うことができます。
完全コンストラクタ
完全コンストラクタとは、コンストラクタにバリデーションロジックを実装し、それを通過した値のみ初期化させ、通過しない値は初期化させないようにする設計パターンです。地味ですが、こうすることで存在する値オブジェクトが全てバリデート済みであることが保証されます。不当な値はオブジェクトに初期化すらされないのでアプリ上から存在しなくなります。実は上記のコードの中で既に実践されていました。
func NewHitPoints(value int) *HitPoints {
if value < 0 {
panic("hit points must be non-negative")
}
return &HitPoints{value}
}
こうしておけばバリデーションロジックが増えた時もこのコンストラクタの中だけをいじれば良いので保守もしやすいです。
以上!