🧱

Goでファクトリを実装(「ドメイン駆動設計入門」Chapter9)

2022/04/14に公開約10,400字

概要

戦術的 DDD の実装パターンを Go で実装します。
今回は、「ドメイン駆動設計入門」Chapter9 のファクトリ(Factory)の実装をおこないます。

実装したソースコードは以下です。

https://github.com/Msksgm/go-itddd-09-factory

参考にしたサンプルコードは以下です。

https://github.com/nrslib/itddd/tree/master/SampleCodes/Chapter9/_06

ファクトリ

ファクトリについて説明します。
「ドメイン駆動設計入門」ではファクトリを以下のように紹介していました。

複雑な道具はその生成過程も複雑です。ともすれば生成過程がある種の知識となります。プログラムにおいてもこれは同じで、複雑なオブジェクトはその生成過程も複雑な処理になることがあります。そうした処理はモデルを表現するドメインオブジェクトの趣旨をぼやけさせます。かといって、その生成をクライアントに押し付けるのはよい方策ではありません。生成処理自体がドメインにおいて意味をもたなかったとしても、ドメインを表現する層の責務であることには変わりないのです。
求められることは複雑なオブジェクトの生成処理をオブジェクトとして定義することです。この生成の責務とするオブジェクトのことを、道具に作る工場になぞらえて「ファクトリ」といいます。ファクトリはオブジェクトの生成に関わる知識がまとめられたオブジェクトです。

出典:ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 Chapter9

複雑な生成処理をドメインオブジェクト内に書くと、責務がわかりづらくなるため他のオブジェクトに処理を分けたのがファクトリです。
保存時はファクトリで先に生成してからリポジトリを介して永続化層に保存、取得時はリポジトリで永続化層から取得してからファクトリで生成、といった流れで使われます。
ファクトリは新しい集約の生成時には集約を生成する役割を持ち、データベースから集約を取り出すときには再構成する役割を持ちます。

実装

本記事では簡略化と書籍の説明に同意できなかった部分がありました。
そのため、UserId(エンティティUserの一意な識別子)を生成するフローが複雑であると仮定してUserFactoryメソッドを実装しました。
具体的に同意できなかった部分は、考察で説明します。
Userを DB に保存する処理までを実装します。

ディレクトリ構成

同様に簡略化のため、アプリケーションサービス層、インフラ層で記述するべき処理を全て、ドメイン層に記述しています。
本筋から逸れるため、本記事ではテスト、DB(postgresql)、docker、go-migrate の説明を省略します。

ディレクトリ構成
.
├── Makefile
├── README.md
├── db-migration.sh
├── docker
│   ├── go
│   │   └── Dockerfile
│   └── postgres
│       └── Dockerfile
├── docker-compose.yml
├── domain
│   └── model
│       └── user
│           ├── user.go
│           ├── user_test.go
│           ├── userapplicationservice.go
│           ├── userapplicationservice_test.go
│           ├── userfactory.go
│           ├── userfactory_test.go
│           ├── userid.go
│           ├── userid_test.go
│           ├── username.go
│           ├── username_test.go
│           ├── userrepository.go
│           ├── userrepository_test.go
│           ├── userservice.go
│           └── userservice_test.go
├── go.mod
├── go.sum
├── main.go
└── migrations
    ├── 000001_user.down.sql
    └── 000001_user.up.sql

7 directories, 25 files

domain_model
ドメインモデル図

UserFactory

まず、UserFactoryを解説します。
UserFactoryにはインタフェースUserFactorierを実装します。
メソッドにはCreateを記述します。
抽象に依存させ、他の実装やテストを容易にするためインタフェースを用意しました。詳細はApplicationServiceで確認します。

UserFactorierの実装クラスUserFactoryに実際の処理を記述します。
今回はUserIdを生成するときにUUID の生成が複雑だと仮定しています。
実際にはライブラリを使うため簡潔な処理になっています。
引数には値オブジェクトUserNameを指定して、値オブジェクトUserIdを生成した後、エンティティUserにして返します。
呼び出した側はこれらの処理が隠蔽されているため簡潔な処理にできます。

./domain/model/user/userfactory.go
package user

import (
	"github.com/google/uuid"
)

type UserFactorier interface {
	Create(UserName) (*User, error)
}

type UserFactory struct{}

func NewUserFactory() (*UserFactory, error) {
	return &UserFactory{}, nil
}

func (uf *UserFactory) Create(name UserName) (*User, error) {
	// uuidの生成が複雑な処理だと仮定
	uuidV4, err := uuid.NewRandom()
	if err != nil {
		return nil, err
	}

	userId, err := NewUserId(uuidV4.String())
	if err != nil {
		return nil, err
	}

	user, err := NewUser(*userId, name)
	if err != nil {
		return nil, err
	}
	return user, nil
}

UserApplicationService

つづいて、UserFactoryに呼び出し側である、UserApplicationServiceを解説します。
メソッドRegisterでエンティティUserの作成、保存をおこないます。
1. エンティティUserを生成で、UserIdを渡すだけでUserが作成され、UUID を生成する処理が隠蔽されています。このようにファクトリは複雑な処理を隠蔽できることがわかります。
2. エンティティUserを保存で、作成したエンティティUserを保存しています。保存時にはファクトリがエンティティを生成し、リポジトリが保存する流れになることがわかりました。

./domain/model/user/userapplicationservice.go
package user

import (
	"fmt"
	"log"
)

type UserApplicationService struct {
	userFactory    UserFactorier
	userRepository UserRepositorier
	userService    UserService
}

func NewUserApplicationService(userFactory UserFactorier, userRepository UserRepositorier, userService UserService) (*UserApplicationService, error) {
	return &UserApplicationService{userFactory: userFactory, userRepository: userRepository, userService: userService}, nil
}

func (uas *UserApplicationService) Register(name string) (err error) {
	defer func() {
		if err != nil {
			err = &RegisterError{Name: name, Message: fmt.Sprintf("userapplicationservice.Register err: %s", err), Err: err}
		}
	}()
	userName, err := NewUserName(name)
	if err != nil {
		return err
	}

	// 1. エンティティUserを生成
	user, err := uas.userFactory.Create(*userName)
	if err != nil {
		return err
	}

	isUserExists, err := uas.userService.Exists(user)
	if err != nil {
		return err
	}
	if isUserExists {
		return fmt.Errorf("user name of %s is already exists.", name)
	}

	// 2. エンティティUserを保存
	if err := uas.userRepository.Save(user); err != nil {
		return err
	}

	log.Printf("user name of %s is successfully saved", name)
	return nil
}

type RegisterError struct {
	Name    string
	Message string
	Err     error
}

func (err *RegisterError) Error() string {
	return err.Message
}

その他のドメインオブジェクト UserRepository、UserService、User、UserId、UserName

実装したソースコードには他にもドメインオブジェクトを用意しています。
詳しくは説明しませんが、もし興味があれば自分の Github のリポジトリと過去記事を参考にしてみてください。

ドメインオブジェクト ドメインオブジェクト名 役割 過去記事
UserRepository リポジトリ DB とUserのやり取り(保存、検索、etc...)を隠蔽する。 Go でリポジトリを実装(「ドメイン駆動設計入門」Chapter5)
UserService ドメインサービス Userが振る舞いとして持つべきでないステートレスな処理を持つ。 Go でドメインサービスを実装(「ドメイン駆動設計入門」Chapter4)
User エンティティ 集約ルートになる可能性がある。一意な識別子を持ち、他の属性を可変にしても良い Go でエンティティを実装(「ドメイン駆動設計入門」Chapter3)
UserId UserName 値オブジェクト 属性が不変。エンティティの一意な識別子になることがある。 Go で値オブジェクトを実装(「ドメイン駆動設計入門」Chapter2)

動作確認

今までの実装から動作確認を行います。
docker-compose を使用します。

main.go

main.goを動作確認のためだけに作成しました。
DB との接続を main 関数の中でおこない、ApplicationService のメソッドであるRegisterでおこないます。
ユーザー名user-nameを保存する処理になります。ユーザー ID は UUID が生成されます。

./main.go
package main

import (
	"database/sql"
	"fmt"
	"log"
	"os"

	"github.com/msksgm/go-itddd-09-factory/domain/model/user"
)

func main() {
	uri := fmt.Sprintf("postgres://%s/%s?sslmode=disable&user=%s&password=%s&port=%s&timezone=Asia/Tokyo",
		os.Getenv("DB_HOST"), os.Getenv("DB_NAME"), os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), os.Getenv("DB_PORT"))
	db, err := sql.Open("postgres", uri)
	if err != nil {
		panic(err)
	}
	if err := db.Ping(); err != nil {
		panic(err)
	}
	log.Println("successfully connected to database")

	userFactory, err := user.NewUserFactory()
	if err != nil {
		panic(err)
	}
	userRepository, err := user.NewUserRepository(db)
	if err != nil {
		panic(err)
	}
	userService, err := user.NewUserService(userRepository)
	if err != nil {
		panic(err)
	}

	userApplicationService, err := user.NewUserApplicationService(userFactory, userRepository, *userService)
	if err != nil {
		panic(err)
	}

	if err := userApplicationService.Register("test-user"); err != nil {
		log.Fatal(err)
	}
}

コンテナを起動・マイグレーション

実行準備をします。

コンテナの起動
> make up
docker compose up -d
# 完了までまつ
マイグレーション
> make run-migration
docker compose exec app bash db-migration.sh
1/u user (13.817ms)

実行

動作確認をします。
最初の実行では、test-userが登録されていないため、登録されます。

test-user 登録 1回目
> make run
docker compose exec app go run main.go
2022/04/14 21:36:19 successfully connected to database
2022/04/14 21:36:19 user name of test-user is successfully saved

2 回目では、test-userがすでに登録されているため、エラーがログに出力されます。

test-user 登録 2回目
> make run
docker compose exec app go run main.go
2022/04/14 21:36:30 successfully connected to database
2022/04/14 21:36:30 userapplicationservice.Register err: user name of test-user is already exists.
exit status 1
make: *** [run] Error 1

適切に動作することを確認しました。

考察

今回実装するにあたっての考察を記述していきます。
個人的な考察なので飛ばしてもらってかまわないです。

命名規則について

ファクトリはドメイン層にあるドメインオブジェクトの 1 つです。
そのため、ドメインの意図を反映させるために名前にはユビキタス言語を採用したほうが良いです。
今回は、サンプルコードに従いUserFactoryという名前にしましたが、Factoryという言葉はユビキタス言語に含まれない限り避けた方が良いです。

なぜなら、UserFactoryという名前では、UserFactoryの中に多くのファクトリメソッドを実装してしまい、責務が不明瞭になります。
例えば、管理者ユーザーと一般ユーザーがいる場合は、UserFactoryの中にCreateAdminCreateUserという名前のファクトリメソッドを作ってはいけません。
その場合は、AdminUserCommonUserという名前のオブジェクトを作成します。
その後はコンストラクタを使うか、ファクトリメソッドを実装するか、という方針を決めます。

ドメイン層にある限りユビキタス言語から命名し、ドメインの意図を明確にした名前をつけるべきです。機会的に命名するべきではないというのが自分の意見です。

UserId の生成方法について

今回、UUID を生成させる処理は複雑な責務として分離させました。
「ドメイン駆動設計入門」では、ファクトリ内で DB にアクセスをして、シーケンスを使ってテーブルが userId を生成する処理をしていました。
つまり、DB との接続の役割をもつリポジトリを介さずに直接ドメイン層からアクセスしていました。これは、ドメイン駆動設計では普通はおこなわれないパターンです。
リポジトリを介さなかった理由について、「ドメイン駆動設計入門」では以下のように記述されていました。

ファクトリとは少し外れますが、リポジトリに採番処理を行うメソッドを用意するパターンもあります。
(中略)
このパターンはその手軽さからして受け入れられやすいものであることも確かです。開発チームでの合意が取れているのであればこのパターンを採用することは問題ではありません。
筆者の個人的な感覚では、そもそもリポジトリはデータの永続化と再構築を行うオブジェクトです。採番処理にまで手を伸ばすのは少し責務を広げ過ぎているように感じるため推奨していません。

出典:ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 Chapter9

私は、この方法について反対です。

筆者の個人的な感覚では、そもそもリポジトリはデータの永続化と再構築を行うオブジェクトです。採番処理にまで手を伸ばすのは少し責務を広げ過ぎているように感じるため推奨していません。

出典:ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 Chapter9

の部分については理由を理解できますが、それでも反対する理由は 2 つあります。
1 つめは、ドメインオブジェクトの 1 つであるはずのファクトリが DB という技術的な詳細を知ることになるからです。これはドメイン駆動設計の思想に反しています。
2 つめは、リポジトリの実装を置くインフラ層はアーキテクチャにおける外部に位置しています。つまりインフラ層におくことで、腐敗防止層のような役割を持ちドメイン層に影響を与えなくなります。ドメインに影響を与えなくなることはドメイン駆動設計において良い方針だと考えます。
以上の理由から同意ができなかったため、今回はシーケンスによる採番処理を実装せず、UUID の生成を複雑な処理と仮定して実装しました。

まとめ

サンプルコードを参考しながら、Go で DDD の戦術的パターンの 1 つである、ファクトリを実装しました。
複雑な処理を隠蔽することで、ソースコードの意図を明確にすることがファクトリの役割です。
これらを実装しながら確認できました。

サンプルコードと書籍に書かれた、命名規則と DB の接続についての観点から反対意見を持ちました。
そのため、すこし修正を加えた実装になっています。この点について意見がありましたら、コメントのほどお願いします。

Discussion

ログインするとコメントできます