はじめに
この記事は Go Advent Calendar 2022 11日目の記事です。
Go歴1年半くらいの @takokun778 が執筆します。
what's domain primitive ?
タイトルにあるdomain primitive
(ドメインプリミティブ)とは以下の特徴を持つものです。
- ドメイン・モデルを構築する最小の要素
- ドメイン・モデルの基礎を形成するもの
- 不変(
immutable
)であり有効となる場合にだけ存在
セキュア・バイ・デザイン 安全なソフトウェア設計という本の第5章ドメインプリミティブで紹介されています。
こちらの本はJava
を用いて実装例が示されています。
そんな実装をあえてGo
で書いてみようと思ったのがこの記事の内容となります。
また、本の内容をGo
で書き換えてみようというよりいろんなところで見聞きしたエッセンスが混ざっている @takokun778 流の実装になります。
とくにプロダクトコードで実績があるわけではない趣味レベルのコーディングになります。
「こんな実装もあるんだなぁ」くらいの視点で読んでいただけると幸いです。
environment
本記事を記述する際に利用した環境は以下の通りです。
uname -a
Linux HP-Spectre-x360 5.4.72-microsoft-standard-WSL2 x86_64 x86_64 x86_64 GNU/Linux
go version
go version go1.19.4 linux/amd64
interface!
domain primitive
であることを宣言するためにinterface
を以下のように用意します。
domain primitive
に必要そうなメソッドを羅列します。
package primitive
// Primitive interface.
type Primitive[T any] interface {
New(T) (Primitive[T], error)
Validate(T) error
Value() T
String() string
Reconstruct(T) Primitive[T]
}
duck typing!
interface
を満たすために以下のように実装します。
文字列をラップした型Name
ドメインプリミティブを考えます。
それぞれ実装ごとにポイントを紹介します。
package sample
import (
"domain/primitive"
)
// (1)
// Name for call New() Reconstruct() Validate().
var Name name //nolint:gochecknoglobals
// (2)
// name domain primitive struct.
type name struct {
value string
}
// (3)
// New domain primitibe construct function.
func (name) New(v string) (primitive.Primitive[string], error) {
if err := Name.Validate(v); err != nil {
return name{}, err
}
return name{value: v}, nil
}
// (4)
// Reconstruct reconstruct domain primitive function.
func (name) Reconstruct(v string) primitive.Primitive[string] {
return name{value: v}
}
// (5)
// Validate validation function.
func (name) Validate(v string) error {
if len(v) == 0 {
return primitive.NewValidationError("name is empty", nil)
}
return nil
}
// (6)
// Value get value.
func (n name) Value() string {
return n.value
}
// (7)
// String get string mask primitive information.
func (n name) String() string {
return "*****"
}
(1) 空の大文字変数を宣言
package名.大文字変数.メソッド
で呼び出すためにあえて大文字変数を宣言してみました。
こうしておくことで外部パッケージから呼び出したときに見た目がなんとなくきれいだなってなります。
n, err := sample.Name.New("sample")
型宣言を大文字とすれば以下のような形式で呼び出せますが{}
がちょっと見た感じ邪魔だなぁって思ったのでこんな謎の変数宣言をしてみました。
n, err := sample.Name{}.New("sample")
Value()
やString()
メソッドも呼び出せてしまうので扱いには注意が必要となります。呼び出してた場合はデフォルト値(string
の場合は空文字)が取得されます。
(2) 小文字での型宣言
immutable
とするために小文字としました。
構造体フィールドのvalue
も小文字なのでNew
メソッドでしか設定できないです。
(3) New()メソッドでコンストラクタ
value
に対してアクセスする必要はないので(name)
だけとします。
また、返り値をprimitive.Primitive[string]
としておくことでname
構造体がduck typing
できているかどうかの確認ができます。
必要なメソッドが足りない場合はビルドエラーとなります。
func (name) New(v string) (primitive.Primitive[string], error) {
// ...
}
(4) Reconstruct()メソッドでバリデーションを無視
安全に扱えることが確定している場合はReconstruct()
メソッドを利用します。
利用想定としてはDBに保存してある値を再構築したいときなどです。
New()
を利用しても良いですがerror
を_
で無視する実装を避けたかったので用意しています。
(5) Validate()メソッドで妥当性確認
New()
メソッドの中で呼び出すことを想定していますが外部からも呼び出せるようにしています。
また、こちらの実装がdomain primitive
の仕様となるので重要度が高い実装になります。
(6) Value()メソッドで値を取得
value
を小文字宣言して外部から隠蔽しているので値を取得する際はValue()
メソッドを利用します。
(7) String()メソッドで文字列表現を取得
どんな型でも文字列として扱いたいタイミングは発生するので用意しています。
また。名前などの個人情報を扱う場合はログなどで出力されないようにString()
メソッドの返り値はマスクした値*****
を設定します。
let's use code!
実際のコードで利用する場合は以下のような形式になります。
package main
import (
"fmt"
"go-domain-primitive/internal/sample"
"log"
)
func main() {
name, err := sample.Name.New("new")
if err != nil {
log.Fatal(err)
}
fmt.Printf("name.String():\t\t %s\n", name.String())
fmt.Printf("name.Value():\t\t %s\n", name.Value())
name = sample.Name.Reconstruct("reconstruct")
fmt.Printf("name.String():\t\t %s\n", name.String())
fmt.Printf("name.Value():\t\t %s\n", name.Value())
fmt.Printf("sample.Name.Value():\t\t %s\n", sample.Name.Value())
fmt.Printf("sample.Name.String():\t %s\n", sample.Name.String())
if _, err := sample.Name.New(""); err != nil {
fmt.Printf("failed to new name error: %v\n", err)
}
}
実行結果は以下となります。
name.String(): *****
name.Value(): new
name.String(): *****
name.Value(): reconstruct
sample.Name.Value():
sample.Name.String(): *****
failed to new name error: name is empty
おわりに
今回作成したコードのすべてはこちらに置いておきます。
ほかにも専用のエラー型、テストコードを作成してあります。
Go
の特性を利用して作成してみましたがコード量は多くなりました。
Go
でdomain primitive
みたいなことをするなら以下のようなコードでいい気がします。
package sample
type Name string
func (n Name) Validate() error {
if n == "" {
return errors.New("name is empty")
}
return nil
}
func (n Name) String() string {
return "*****"
}
func (n Name) Value() string {
return string(n)
}