【Golang】Value Objectを生成するvogenライブラリをお試しで作った話

前書き:GolangでValue Objectを作りづらい

2025年の抱負で「ブログのアウトプットを増やす(リンク先の末尾を参照)」と宣言したので、早速アウトプットします。

 

今回取り扱うValue Objectとは、主に以下のような特徴を持ちます(本記事の本題と関係ない要素は意図的に省略しています)

  • 不変(Immutable)
  • オブジェクトは、値が同値の時に等しい

 

Golangで不変の仕様を満たすには、構造体の中にプライベートフィールドを用意し、プライベートフィールドにアクセスするGetterメソッドを準備する必要があります。以下に、サンプルコードを示します。

 

Person構造体が持つnameフィールドは可視性がプライベートなため、NewPerson()した後は不変です。Getterメソッドで経由で値にアクセスします。値が同値かどうかは、Equal()でチェックします。

 

このような実装は、ハッキリ言って面倒くさいです。また、Getter()Equal()にユニットテストを書くのも馬鹿らしいです。私は「Kotlinのvalue classdata classがGolangにもあればな〜」と無い物ねだりをしていました。

 

仕組みがなければ作ればいいじゃない。

ということで、nao1215/vogenパッケージを2時間ぐらいで作りました。

 

vogenは、メタデータからValue Objectを生成

nao1215/vogenパッケージは、New()GetterEqual()を持つValue Objectコードを自動生成するライブラリです。Value Object Generatorの略です。

GolangでValue Objectのメタデータを書き、そのメタデータを元にコードを自動生成します。この仕様の元ネタは、shogo82148/myddlmaker(メタデータからDB DDLを生成するライブラリ)です。

 

想定の利用方法としては、value_object/gen/main.goにメタデータを定義し、go generate ./...value_object/value_object.goファイルを生成します。出力先を複数のファイルに分散することもできます。

以下、value_object/gen/main.goに実装例です。

 

vogen.New() では、生成先のファイルパスやパッケージ名を指定していますが、省略できます。省略した場合は、value_objects.go ファイルが vo パッケージとして生成されます。

vogen.ValueObject()がメタデータに相当します。構造体コメントや構造体フィールドコメントは省略可能です。省略した場合は、魂のこもっていない英文コメントが出力されます。型にはDefined Type(ユーザーが定義した型)を指定できますが、その場合はモジュールパスを指定する必要があります。Defined Type絡みはテストしていないので、意図的にExampleコードからその存在を隠しています(後日テスト予定)

 

上記のサンプルコードを利用して自動生成されたコードを以下に示します。

         

微妙な点1:大して楽ではない

勢いで作ったので、利便性の低さが気になっています。

 

「自動生成コードは、テストカバレッジの計測対象外にしよう」というルールがあったとしても、vogenパッケージに便利さを感じていません。「メタデータを書く量」と「自動生成されるコード量」が見合ってません。”自分で書いた方が早インパラ(元ネタ)”という気持ちになる可能性があります。

 

まず、厳密なValue Objectをプロダクションコードに導入したことがないので、「今までValue Objectがなくても上手く動いていたのに、今更こんな厳密な仕組みを導入するんですか?」という気持ちが前にでてきてしまいます。

 

微妙な点2:New()時にバリデーションできない

「バリデーションされた値を保持している」という状態は、安心感があります。しかし、vogenパッケージはそのような機能を導入できていません。構造体生成時に受け取る値を無条件で信用します。ピュアな奴め。

 

メタデータにバリデーション用関数を追加する案が思い浮かびましたが、その場合は実装が素直になりません。また、メタデータの記述量が増えるので、「vogenパッケージを使わず、素直に自分でValue Objectを実装したほうが楽」と考えてしまいます(少なくとも私はそのように考えます)

 

2025年1月1日追記

バリデーション機能を追加してみました。以下のように、vogen.ValueObject.Validatorsに任意のバリデーションを追加します。

 

以下のようなNewPersonStrictly()が自動生成されます。バリデーションのないNewPerson()も自動生成されます。

 

最後に

最近はDDD(Domain-Driven Design)やアーキテクチャを勉強していることもあり、「理論に合わせてなるべく厳密に実装してみよう」とトライしている最中です。その一環で、vogenパッケージを施策してみました。

 

「Golangを使わない」、「Golangで厳密なValue Objectを採用しない」、「GolangでもキッチリValue Objectを適用する」など、色々な判断ができます。手探りで色々試していきたい所存です。

 

おすすめ