Open4

「ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本」をGoでやる

Value Object

プリミティブの値でなく、(オブジェクト指向なら)クラスとかで値クラスを設定する

type VO string 

func newVO(val string) (*Name, error) {
	vo := VO(val)
	return &vo, nil
}

特徴

    • 値は不変である
  • 値は交換可能(a = new VO("aaa") -> a = new VO("bbb)" )
  • 等価性によって比較可能

ふるまいを定義できる

例えば、通貨というValue Objectを定義するとしたら、以下のような定義が可能です。

type Money struct {
	amount int
	currency string
} 

func newMoney(amount int, currency string) (*Money, error) {
	if currency == "" {
		return nil, errors.New("currency is need")
	}
	return &Money{
		amount: amount,
		currency: currency,
	}, nil
}

func (baseMoney *Money) add(addMoney *Money) (*Money, error) {
	if baseMoney.currency != addMoney.currency  {
		return nil, errors.New("currency should be same")
	}
	return &Money{
		amount: baseMoney.amount + addMoney.amount,
		currency: baseMoney.currency,
	}, nil
}

func main() {
	m1, _ := newMoney(100, "USD")
	m2, _ := newMoney(300, "USD") 
	m3, _ := m1.add(m2)
	fmt.Println(m3)  // &{400 USD}
}

ここではaddというメソッドで、通貨を加算するというふるまいを定義しています。
通貨単位が異なる場合にエラーを返すといったドメイン固有のエラーを定義が可能になります
VOを利用せずプリミティブの場合は、通貨の加算だけでなく、これもやらないといけないので考慮点が増えてしまいます。

またふるまいを定義するということは、定義された以外のことはできないということになります。
例えば上のコードは加算しか定義していないため、減算処理なども実行できないです(ドメインで必要ならメソッド定義を行う

値オブジェクトを導入するメリット

プリミティブ型でなく、わざわざVOを定義する理由

  • 表現力がます
  • 不正な値を存在させない
  • 謝った代入を防げる
  • ロジックの散財を防げる

表現力がます

ドメインがたっぷり含まれるような値をプリミティブ型で定義すると、ソースコードだけから情報が読み取れなくなる。

例えば書籍のID的なISBNはプリミティブ型にすると、単純な文字列になってしまうが実際は以下のようにハイフン区切りごとに意味がある。

// 978-4-7981-2196-3
type ISBN struct {
	prefix string // 978
	countryCode string // 4
	publisherCode string // 7981
	bookNameCode string // 2196
	checkDigit string // 3
}

コードで定義することで、コードからドメインの情報が読み取りやすくする

不正な値を代入させない

パスワードを用意するとして、ドメインロジックとして、8文字未満は許容しないといった場合に以下のように、VOにその設定を入れることで弾くことが可能

type Password string

func newPassword(password string) (*Password, error) {
	if len(password) < 8 {
		return nil, errors.New("パスワードは8文字以上を指定してください")
	}
	retPass := Password(password)
	return &retPass, nil
}

謝った代入を防ぐ

VOでクラスとして定義することで、型が使えるのでコンパイルエラーを出しやすくなる
プリミティブ型だと、にた値で間違えることがある

例えばユーザーIDを入力に受け取る関数用意するとして、値としてユーザーID, ユーザー名がある場合
どちらもプリミティブ型で定義していると、間違ってユーザー名渡してしまってもエラーにならない(VOにしておくと型エラーが出るようになる

ロジックの散在を防ぐ

上の不正な値代入などはVOの中に、判定ロジックを入れている。
VOを利用しない場合、この値を新しく生成する箇所、使う箇所至る所にこの判定ロジックを突っ込まないと行けない

エンティティ

VOとの違いは同一性をもつかどうか
ユーザーというオブジェクトは属性があって、その属性を変更してもシステムとしてはユーザーが変わったとは判定しない(ユーザーIDが変わってないみたいな)こういうのがエンティティ

  • 可変である
  • 同じ属性でも区別される
  • 同一性により区別される

何をエンティティにして何をVOにするか

ライフサイクルがあるようなものはエンティティにすべき

ドメインサービス

VOやエンティティに書ききれないふるまいをまとめるもの
例えば重複したユーザーは許容しないというふるまいはエンティティやVOに持たせるべきではない

やろうと思えば、ドメインサービスにふるまい全部を突っ込めるけど、それやるとVOやエンティティがセッターゲッターだけになるので、ほどほどに(ドメイン貧血症)

可能な限りドメインオブジェクト(VO, エンティティ)を利用して、なるべくサービスは利用しないこと

またドメインサービスには、データの保存詳細(SQL処理など)は記載しないこと(それはRepoistoryの役目)

ドメインサービス、エンティティ、VOで物流拠点のユースケースを表現してみる

Entity, VOとして、荷物(baggage)と物流拠点(PhysicalDistributionBase)を表現する

type Baggage struct {
	Name string
	Option []string
} 

func NewBaggage(name string, option ...string) *Baggage {
	return &Baggage{
		Name: name,
		Option: option,
	}
}

type PhysicalDistributionBase struct {
	baggages []*Baggage
}

func NewPhysicalDistributionBase(baggages []*Baggage) *PhysicalDistributionBase {
	return &PhysicalDistributionBase{
		baggages: baggages,
	}
}

func (p *PhysicalDistributionBase) Ship(baggage *Baggage) {
    result := []*Baggage{}
    for _, v := range p.baggages {
        if v != baggage {
            result = append(result, v)
        }
    }
	p.baggages = result
}

func (p *PhysicalDistributionBase) Receive(baggage *Baggage) {
	p.baggages = append(p.baggages, baggage)
}

拠点Aからの出荷と、拠点Bへの入庫は同時に行われないといけない。
これらはエンティティに持たせるべきではないのでサービスを定義する

type Service struct {}

func NewService() *Service {
	return &Service{}
}

func (s Service) Transport(from, to *PhysicalDistributionBase, baggege *Baggage) {
	from.Ship(baggege)
	to.Receive(baggege)
}

これらを実際に使ってみる

func main() {

	baggageA := NewBaggage("AAA", "割れ物")
	baggageB := NewBaggage("BBB", "割れ物", "冷蔵")
	baggageC := NewBaggage("CCC")
	baggageD := NewBaggage("AAA", "電気機器")

	baggages := []*Baggage{
		baggageA,baggageB, baggageC, baggageD,
	}
	BaseA := NewPhysicalDistributionBase(baggages)
	BaseB := NewPhysicalDistributionBase([]*Baggage{})

	service := NewService()
	
	service.Transport(BaseA, BaseB, baggageC)
	service.Transport(BaseA, BaseB, baggageD)

	fmt.Println("BaseA has following baggages")
	for _, v := range BaseA.baggages {
		fmt.Println(v.Name, v.Option)
	}
	fmt.Println("BaseB has following baggages")
	for _, v := range BaseB.baggages {
		fmt.Println(v.Name, v.Option)
	}
	// BaseA has following baggages
	// AAA [割れ物]
	// BBB [割れ物 冷蔵]
	// BaseB has following baggages
	// CCC []
	// AAA [電気機器]
}
作成者以外のコメントは許可されていません