8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

GoAdvent Calendar 2022

Day 11

Let's Go "domain primitive" !

Last updated at Posted at 2022-12-10

はじめに

この記事は 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の特性を利用して作成してみましたがコード量は多くなりました。
Godomain 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)
}
8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?