kanachi-blog

notionでの公開記事をastro-notion-blogを使って公開するよ

アプリケーションを実現するためのパターン『ドメイン駆動設計入門 』part2

データにまつわる処理を分離する「リポジトリ」

リポジトリとは

リポジトリはデータを永続化し、再構築するといった処理を抽象的に扱うためのオブジェクトです。オブジェクトのインスタンスを保存したいときは直接データストアに書き込みを行うのではなくリポジトリに実行を依頼する。

リポジトリの責務

リポジトリの責務はドメインオブジェクトの永続化や再構築を行うことです。永続化するデータストアが RDB か NoSQL かファイルなのかドメインにとっては重要ではありません。

リポジトリのインターフェース

リポジトリはドメイン層にインターフェースで定義します。リポジトリの責務はあくまでもオブジェクトを永続化することです。

type IUserRepository interface {
    SaveUser(user *model.User) error
    FindUserByName(name string) (*model.User, error)
}
package main

import (
	"database/sql"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	"model"
)

type UserRepository struct {
	db *sql.DB
}

func NewUserRepository(dataSourceName string) (*UserRepository, error) {
	db, err := sql.Open("mysql", dataSourceName)
	if err != nil {
		return nil, err
	}
	return &UserRepository{db: db}, nil
}

func (r *UserRepository) SaveUser(user *model.User) error {
	_, err := r.db.Exec("INSERT INTO users (name) VALUES (?)", user.Name)
	if err != nil {
		return err
	}
	return nil
}

func (r *UserRepository) FindUserByName(name string) (*model.User, error) {
	var user model.User
	err := r.db.QueryRow("SELECT id, name FROM users WHERE name = ?", name).Scan(&user.ID, &user.Name)
	if err != nil {
		if err == sql.ErrNoRows {
			return nil, fmt.Errorf("no user found with name: %s", name)
		}
		return nil, err
	}
	return &user, nil
}

func main() {
	// Replace with your own database data source name.
	repo, err := NewUserRepository("user:password@/dbname")
	if err != nil {
		panic(err)
	}

	user := &model.User{Name: "John Doe"}
	err = repo.SaveUser(user)
	if err != nil {
		fmt.Println("Error saving user:", err)
	}

	foundUser, err := repo.FindUserByName("John Doe")
	if err != nil {
		fmt.Println("Error finding user:", err)
	} else {
		fmt.Printf("Found user: %+v\n", foundUser)
	}
}

永続化に関するふるまい

永続化のふるまいは永続化を行うオブジェクトを引数にとります。

良い例

type Repository interface {
    SaveUser(user *model.User) error
    DeleteUser(user *model.User) error
}

悪い例

type Repository interface {
    UpdateUserByName(id, name string) error
    UpdateUserByEmail(id, email string) error
    UpdateUserByAddress(id, address string) error
}

エンティティの識別子と更新項目を引き渡して「更新」させるようなメソッドはリポジトリには含めるべきではない。

そもそもエンティティが保持するデータの変更はエンティティ自身に実装するべき。

同様にエンティティを生成する処理もリポジトリには含めない。コンストラクタ以外からオブジェクトを生成する必要がある場合はファクトリ(後述)を利用する。

再構築に関するふるまい

再構築、言い換えると復元をするパターンは主に、データの検索を行う時に利用される。

パフォーマンスの観点から、検索を定義する際にはそれに適したメソッドを定義する。

ユースケースを実現する「アプリケーションサービス」

アプリケーションサービスとは

ユースケースを実現するオブジェクトである。

このチャプターでは基本的なCRUD操作を持つSNSのユーザー機能を取り扱う。

Goでの実装コード

ユーザー登録処理

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
	}

	// uuidV4 := uuid.New().String()
	uuidV4 := "test-id"
	userId, err := NewUserId(uuidV4)
	if err != nil {
		return err
	}

	user, err := NewUser(*userId, *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)
	}

	if err := uas.userRepository.Save(user); err != nil {
		return err
	}

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

上記のようにアプリケーションサービスではドメインサービスやリポジトリを使いユースケースをプログラムとして実装する。

ユーザー情報取得

//DTO
type UserData struct {
	Id   string
	Name string
}

func (uas *UserApplicationService) Get(userId string) (_ *UserData, err error) {
	defer func() {
		if err != nil {
			err = &GetError{UserId: userId, Message: fmt.Sprintf("userapplicationservice.Get err: %s", err), Err: err}
		}
	}()
	targetId, err := NewUserId(userId)
	if err != nil {
		return nil, err
	}
	user, err := uas.userRepository.FindByUserId(targetId)
	if err != nil {
		return nil, err
	}

	if user == nil {
		return nil, nil
	}
	//ここでDTOを利用してドメインオブジェクトを直接返さないようにしている!
	userData := &UserData{Id: user.id.value, Name: user.name.value}
	return userData, nil
}

ドメインオブジェクトを外部に公開すると、ドメインオブジェクトのメソッドが外部に露出してしまう。それを防ぐためにDTO(データ転送用オブジェクト)を利用する。

ユーザー情報更新

type UserUpdateCommand struct {
	Id   string
	Name string
}

func (uas *UserApplicationService) Update(command UserUpdateCommand) error {
	targetId, err := NewUserId(command.Id)
	if err != nil {
		return err
	}
	user, err := uas.userRepository.FindByUserId(targetId)
	if err != nil {
		return err
	}
	if user == nil {
		return fmt.Errorf("user is not found")
	}

	if name := command.Name; name != "" {
		newUserName, err := NewUserName(name)
		if err != nil {
			return err
		}
		user.ChangeName(*newUserName)

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

	if err := uas.userRepository.Update(user); err != nil {
		return err
	}

	log.Println("successfully updated")
	return nil
}

「アプリケーションサービス」の処理(登録処理や更新処理)を呼ぶ際に、今後の変更でいちいちシグネチャが変わってしまう可能性があります。

「アプリケーションサービス」のメソッドのシグネチャが増えるたびにいちいちメソッドを書き換えなくても良いように、シグネチャを管理するためにコマンドオブジェクトを利用する。

近年の開発ではコードチェックツールのリンターなどを使って開発することが多いと思いますが、「シグネチャの数が多すぎないか」や「メソッドの行数が長すぎないか」もチェックに入っているリンターも多いかと思いますが、その対策にもなります。

ユーザー情報削除

type UserDeleteCommand struct {
	Id string
}

func (uas *UserApplicationService) Delete(command UserDeleteCommand) error {
	targetId, err := NewUserId(command.Id)
	if err != nil {
		return err
	}
	user, err := uas.userRepository.FindByUserId(targetId)
	if err != nil {
		return err
	}
	if user == nil {
		return nil
	}

	if err := uas.userRepository.Delete(user); err != nil {
		return err
	}
	log.Println("successfully deleted")
	return nil
}

ドメインのルールの流出

ドメインのルールはドメインオブジェクトに記述する。アプリケーションサービスはそのドメインオブジェクトを利用するように仕立てます。

このようにすることで変更に強いコードにできる。

アプリケーションサービスと凝集度

一般的には凝集度(LCOM)が下がるようにモジュールを設計することが好ましい。

サービスとは何か?

サービスとはクライアントのために何かを行うもの。それがドメインに向いているか、アプリケーションに向いているかでドメインサービスかアプリケーションサービスかが変わる。

サービスは状態を持たない

サービスに自身の振る舞いを変化させる目的で状態を持たないようにする。サービスが状態を保ち始めると、サービスが今どのような状態なのかを気にする必要があり、多くの開発者を混乱させる。

柔軟性をもたらす依存関係のコントロール

依存関係逆転の原則(Dependency Inversion Principle)

  • 上位レベルの方針のコードは、下位レベルの詳細の実装コードに依存するべきではなく、逆に詳細側が方針に依存するべきである
  • 抽象は、実装の詳細に依存してはならない。実装の詳細が抽象に依存すべきである

ソフトウェアシステムを組み立てる

  • ユーザーインターフェイスにはソフトウェアのメインロジックは持たせないようにし、インターフェイスは交換可能な状態にしておく
  • ソフトウェアを柔軟に変更できるようにするためにはユニットテストが必須

複雑な生成処理を行う「ファクトリ」

この世には便利な道具(オブジェクト)がたくさんある、それらをプログラムで表現しようとする必要がある。ただ、便利さに比例して、道具は複雑さを増す。そして、複雑な道具はその生成過程も得てして複雑である。

求められることは複雑なオブジェクトの生成処理をオブジェクトとして定義することです。この生成を責務とするオブジェクトのことを、道具を作る工場になぞらえて「ファクトリ」といいます。ファクトリはオブジェクトの生成に関わる知識がまとめられたオブジェクトです。

ここではUserId(エンティティUserの一意な識別子)を生成するフローが複雑であると仮定してUserFactoryメソッドを実装する具体例を説明します。

Goでの実装コード

UserFactrory

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

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
}