yyh-gl's icon

yyh-gl's Tech Blog

技術ネタ中心のブログです。主な扱いはバックエンド技術と設計です。

yyh-gl

2 分で読めます

featured

GoでDDD

今担当しているプロジェクトでは、GoでAPIを作っています。
このプロジェクトでは、DDDの考え方や設計パターンも取り入れています。

今回はDDDの設計パターンの中でもEntityとValue Object(VO)について、
僕がGoでどうやって実装しているのか紹介していきます。

実装例

兎にも角にも、まずはコードを示します。

// animal/dog/dog.go

package dog

type Dog struct {
	name Name
}

func New(name string) (*Dog, error) {
	n, err := newName(name)
	if err != nil {
		return nil, err
	}

	return &Dog{
		name: *n,
	}, nil
}
// animal/dog/name.go

package dog

import (
	"errors"
	"unicode/utf8"
)

type Name string

func newName(v string) (*Name, error) {
	// 名前は3文字以上というビジネスロジック
	if utf8.RuneCountInString(v) < 3 {
		return nil, errors.New("名前は3文字以上!")
	}
	n := Name(v)
	return &n, nil
}
// main.go

package main

import (
	"fmt"
	"playground/animal/dog"
)

func main() {
	// d := dog.Dog{name: "犬太郎"} できない
	d, _ := dog.New("犬太郎") // できる
	fmt.Printf("%+v\n", d)
	
	d, err := dog.New("犬")
	if err != nil {
		fmt.Println(err) // 犬の名前が「犬」は可愛そうだからできない()
	}
}

playground


今回の例では、DogというstructがEntityで、NameがVOです。

Dogのnameは必ず3文字以上にするというビジネスロジックがあります。

ポイント1

EntityとVOでファイルを分けています。
また、両者は同じディレクトリ内に置いています。

どれがEntityでどれがVOか分かりづらいと思われる方もおられるかもしれませんが、
個人的には言うほど分かりづらくありません。

なぜかと言うと、EntityとVOが入っているディレクトリ(パッケージ)名と、
Entityのファイル名が一致するからです。

今回の例で言えば、
Dog Entityは/animal/dogディレクトリ配下のdog.goの中にあります。
ディレクトリ名とEntityのファイル名が一致しています。
このルールが分かっていれば、特に問題はありません。

加えて、EntityとVOは同一のパッケージ内にあるべきだと考えています。

ポイント2

Dog Entityのnameフィールドを小文字にすることで、
New()(コンストラクタ的なの)を使用しないと、
nameの値をセットできないようにしています。
dog.Dog{name: "hoge"} はできない)

また、New()を経由することで、必ずnewName()が使われる ため、
Dogのnameは 3文字以上にするというビジネスロジックを確実に守ることができます。


社内の方に「dog.Dog{} はできちゃうね」とコメントをいただきました。
この件については、(願望的なところも入ってきてしまうのですが)
不用意なsetterを用意していなければ、
「あれ?フィールドに値セットできない!」ってなるはずなので、
そこで気づいてもらえると思っています。。。
(さすがに初期化しただけの構造体を保存するようなことはないと信じてます)

ポイント3

VO自体はexportします。
VO(型)を引数として指定することもあるのでこうしています。

exportしちゃうと、dog.Name("ねこ太郎")とすることで、
不正なnameを作れるのでは?と考える方もおられると思います。

たしかに作れます。
しかしながら、保存はEntity単位で行うため、
不正なnameがEntityにセットできないようになっていれば無問題であると考えています。

ポイント4

VO→基本型への変換が必要になることは、往々にしてあると思います。

このとき必要になる、基本型への変換処理はVO自身に持たせています。
(ここは特に議論の余地があると思っています)

以下のサンプルをご覧ください。

// animal/dog/dog.go

package dog

type Dog struct {
	name Name
}

func New(name string) (*Dog, error) {
	n, err := newName(name)
	if err != nil {
		return nil, err
	}

	return &Dog{
		name: *n,
	}, nil
}

func (d Dog) Name() *Name {
	return &d.name
}
// animal/dog/name.go

package dog

import (
	"errors"
	"unicode/utf8"
)

type Name string

func newName(v string) (*Name, error) {
	// 名前は3文字以上というビジネスロジック
	if utf8.RuneCountInString(v) < 3 {
		return nil, errors.New("名前は3文字以上!")
	}
	n := Name(v)
	return &n, nil
}

func (n Name) String() string {
	return string(n)
}
// main.go

package main

import (
	"fmt"
	"playground/animal/dog"
)

func main() {
	d, _ := dog.New("犬太郎")
	fmt.Println(d.Name().String())
}

playground


まず、Dog Entityに(不本意ながら)nameのgetter(Name())を追加しました。
次に、Name VOにString()メソッドをもたせました。

そして、呼び出し元では d.Name().String() とすることで、
基本型(string)としてのnameを取得できます。


Dog Entityにnameのgetterを用意したことについて、
DTOへの変換やレイヤ間で値を受け渡すときなどに、
構造体の詰め替えが発生すると思います。
このときに、Goの場合どっちみちgetterが必要になることでしょう。
よって、どうせ必要になることが分かっているので用意した形になります。


ただし、このgetterは、
値の詰め替えや基本型取得といった、複雑なロジックを持たない処理にのみ使用し、
不用意な使い方はしないことを
運用(PRレビュー)で100%カバーすること前提で許可しています。

・・・
確実にできないようにした方がいいんでしょうね。。。
賛否両論ありますよね、、、わかります


他の方法として、Dog EntityのName()を以下のようにもできます。

func (d Dog) Name() string {
	return string(d.name)
}

ただし、ある関数の引数として、NameというVO(型)のまま渡したい場合、
この方法では対応できません。


引数で渡す用の値(VOのまま)を取得する処理と
基本型としての値を取得する処理を別関数として用意するのが一番いいのかなと思っています。

…が、今のところ、VOにString()メソッドを持たせる方式で特に困ったことがないため、
このまま進めています。

おわりに

ざっとポイントを洗い出してみましたが、
実装方法を考えていた時期とブログを書いている時期がずれているため、
書き忘れているポイントがあるかもしれません。
なにか思い出したタイミングで追記していきます。


最後にお願いです!

Go+DDDの事例は他の言語に比べるとまだまだ少ないと思います。
よって、僕も日々、試行錯誤し、より良い実装方法を探しています。
今回紹介した実装方法には、まだまだ抜けもあれば、より良い実装方法もあると考えています。

なので、みなさん、ぜひコメントください!
Twitter

よろしくお願いします〜

最近の投稿

About

東京で働くソフトウェアエンジニアです。バックエンドがメインですが、フロントエンドやインフラもさわっています。