Hori Blog

フリーランスでバックエンドエンジニアとして活動している Ryota Hori のブログです。
最近はテック系記事より雑記ブログ気味。

DDD するのに Go でも required を表現したくて go-genconstructor を作った

go-genaccessor に続き、今度は required を表現するために constructor を生成する go-genconstructor を作りました。

hori-ryota/go-genconstructor

  • 対象ディレクトリ内のファイルを走査して constructor を生成する
  • //genconstructor のついた struct を検知して自動生成
  • required の struct tag がついたもののみコンストラクタの引数にとることで required を表現
  • タグのパラメータに指定することで const value 対応
  • デフォルトがカレントディレクトリなので //go:generate go-genconstructor を対象ディレクトリに記述すれば go generate で起動する

基本的な思想は go-genaccessor と同じですね。

DDD がしたいので Go の Accessor を go generate する go-genaccessor を作った

動作サンプル

こんな感じで定義します。

person.go
package example

import "time"

//go:generate go-genconstructor

//genconstructor
type Person struct {
	id        string `required:""`
	name      string `required:""`
	tags      []string
	createdAt time.Time `required:"time.Now()"`
}

こんな感じで出力されます。

constructor_gen.go
// Code generated by go-genconstructor; DO NOT EDIT.

package example

import (
	"time"
)

func NewPerson(
	id string,
	name string,
) Person {
	return Person{
		id:        id,
		name:      name,
		createdAt: time.Now(),
	}
}

モチベーション

  • ドメイン駆動設計(以下 DDD )をやっていきたい
  • DDD ではドメイン層で対象領域の概念をコードで表現することが重要
  • 本質以外の作業はノイズなので人間にやらせたくない
  • requied を表現するには constructor が必要だが、
    • 各 struct に実装して回るのは非本質的
    • field の増減時に編集作業が生じるのは非本質的
    • コード量が増えるため概念の読み取りにノイズが大きすぎる

ということで例のごとく go generate で自動生成することにしました。超捗る。

こだわりポイント

基本的には go-genaccessor と同じですが

  • DDD なので概念がシンプルに表現できていて欲しい
    • struct tag への記載による定義とすることで、何が required なのかわかりやすく、表現力が高い
    • struct tag で表現できているので constructor の実装はノイズになる。よって 1 ファイルにまとめて生成。
  • 概念を表現する作業に集中したい
    • 型解析ではなく ast での parse とすることで、コンパイルエラーが生じる状態のコードでも generator を実行可能にした
  • 固定値や現在時刻を持つ field 定義もサポートしたい
    • createdAt 系とか
    • TypeName() string みたいな struct 固定の値とか
  • ライブラリにロックインしそうな定義記述はできれば避けたい。実現したいことを定義で表現し、ツールは何でもいいとしたい。

表現力を高め、かつ楽ができるようにしています。

go-genaccessor との組み合わせ

struct タグは干渉しないので go-genaccessor と組み合わせることができます。

package example

import "time"

//go:generate go-genaccessor
//go:generate go-genconstructor

//genconstructor
type Person struct {
	id        string    `required:"" getter:""`
	name      string    `required:"" getter:"" setter:"Rename"`
	tags      []string  `getter:"" setter:""`
	createdAt time.Time `required:"time.Now()" getter:""`
}

コードに各実装が溢れかえるよりも遥かに意図を汲み取りやすい表現になっているのではないでしょうか。

以上

go-genconstructor はドメイン層に限らず、 required の概念が必要なところで各所使っていけるものです。 stateless な各 service 系でも依存実装を必ず持つことが保証できます。

Go の互換性を大事にする思想は素晴らしいものではありますが、 required は required なのできちんと制約を表現することが大事です。そもそも required なものが追加される時点で互換性は失っています。実行時エラーとするよりはコンパイルエラーの恩恵を享受するほうが利便性としても、安全性としても、表現力としても良いと思います。

readonly と required が表現できるようになったことで DDD における値周りのモデリングは快適になったかと思います。 DDD の他の要素は愚直にコードで表現していってもいいのですが、 generics が無いこともあって人間がやるには辛い作業が生じる概念がいくつかあります。

次は代表的な「型の恩恵を受けたいが丁寧にやると手が疲れる」概念である error と event について generator を仕込んでいく予定です。

  • エラーを表現するための go-generror
  • 結果整合性のために用いるアプリケーションイベントのための go-genappevent

乞うご期待。