interface、structで書くか、functionで書くか
始めに
この正月に、関数型ドメインモデリングという本を読みました。良書でした。
そこで、今までオブジェクトで書いていたコードを関数としてかけないか?という思いつきでこの記事を書いた結果、なんだか関数型とは関係ない感じの記事になってしまいました。ご容赦ください。
ベースとなるサンプルコード
まずはオブジェクト指向でよく使う形のサンプルを用意しました。タスク管理のモデルです。簡単のため、エラーなどはあまり返さないようにしています。
package domain
import "fmt"
type status string
const (
StatusPending status = "pending"
StatusDoing status = "doing"
StatusDone status = "done"
)
func NewStatus(s string) (status, error) {
switch s {
case "pending":
return StatusPending, nil
case "doing":
return StatusDoing, nil
case "done":
return StatusDone, nil
default:
return "", fmt.Errorf("invalid status")
}
}
type Task struct {
ID int
Name string
Status status
}
func NewTask(id int, name string, status status) *Task {
return &Task{
ID: id,
Name: name,
Status: status,
}
}
func (t *Task) Do() {
t.Status = StatusDone
}
このドメインモデルを使うUsecaseのコードは、よくあるオブジェクト指向では以下のようになると思います。
package usecase
import (
"context"
"fmt"
"playground/domain"
)
type TaskDoUsecase interface {
Run(ctx context.Context, task *domain.Task) (*domain.Task, error)
}
type taskDoUsecase struct {
}
var _ TaskDoUsecase = (*taskDoUsecase)(nil)
func NewTaskDoUsecase() *taskDoUsecase {
return &taskDoUsecase{}
}
func (u *taskDoUsecase) Run(ctx context.Context, task *domain.Task) (*domain.Task, error) {
fmt.Println("Instance Usecase")
task.Do()
return task, nil
}
Usecaseオブジェクトのコンストラクタが存在し、唯一のRunメソッドによって実行がされる形です。パッケージ内にインターフェースを持っています。
これを使うhandlerはオブジェクト指向的には以下のようになります。
package handler
import (
"context"
"fmt"
"playground/domain"
"playground/usecase"
)
type taskDTO struct {
ID int
Name string
Status string
}
type TaskDoHandlerWithInstance interface {
Handle(context.Context, taskDTO) (taskDTO, error)
}
var _ TaskDoHandlerWithInstance = (*taskDoHandlerWithInstance)(nil)
type taskDoHandlerWithInstance struct {
taskDoUsecase usecase.TaskDoUsecase
}
func NewTaskDoHandlerWithInstance(
taskDoUsecase usecase.TaskDoUsecase,
) *taskDoHandlerWithInstance {
return &taskDoHandlerWithInstance{
taskDoUsecase: taskDoUsecase,
}
}
func (u *taskDoHandlerWithInstance) Handle(ctx context.Context, inputTask taskDTO) (taskDTO, error) {
fmt.Println("Instance Handler")
status, err := domain.NewStatus(inputTask.Status)
if err != nil {
return taskDTO{}, err
}
task := domain.NewTask(inputTask.ID, inputTask.Name, status)
task, err = u.taskDoUsecase.Run(ctx, task)
if err != nil {
return taskDTO{}, err
}
return taskDTO{
ID: task.ID,
Name: task.Name,
Status: string(task.Status),
}, nil
}
handlerのフィールドにusecaseを持ち、内部ではRunメソッドを実行するだけです。
そしてこれらを実体化するDIの層では以下のようになります。
taskDoUsecase := usecase.NewTaskDoUsecase()
instanceHandler := handler.NewTaskDoHandlerWithInstance(
taskDoUsecase,
)
実体化したusecaseをhandlerのコンストラクタに渡し、handlerのメソッドによって一連の流れが実行されます。
一部関数に変えてみる
以上のusecase部分を関数にしてみます。
type TaskDoFunc func(ctx context.Context, task *domain.Task) (*domain.Task, error)
var _ TaskDoFunc = TaskDo
func TaskDo(ctx context.Context, task *domain.Task) (*domain.Task, error) {
fmt.Println("Function Usecase")
task.Do()
return task, nil
}
依存方向を安定させるため、定義した関数の型に依存する形で具象を書きました。インターフェースと同じように見えますね。
コンストラクタや、structの記述がなくなった分スリムになりました。そしてこれを使う側のコードは以下のようになります。
type TaskDoHandlerWithFunc interface {
Handle(ctx context.Context, inputTask taskDTO) (taskDTO, error)
}
var _ TaskDoHandlerWithFunc = (*taskDoHandlerWithFunc)(nil)
type taskDoHandlerWithFunc struct {
taskDoFunc usecase.TaskDoFunc
}
func NewTaskDoHandlerWithFunc(
taskDoFunc usecase.TaskDoFunc,
) *taskDoHandlerWithFunc {
return &taskDoHandlerWithFunc{
taskDoFunc: taskDoFunc,
}
}
func (u *taskDoHandlerWithFunc) Handle(ctx context.Context, inputTask taskDTO) (taskDTO, error) {
fmt.Println("Function Handler")
status, err := domain.NewStatus(inputTask.Status)
if err != nil {
return taskDTO{}, err
}
task := domain.NewTask(inputTask.ID, inputTask.Name, status)
task, err = u.taskDoFunc(ctx, task)
if err != nil {
return taskDTO{}, err
}
return taskDTO{
ID: task.ID,
Name: task.Name,
Status: string(task.Status),
}, nil
}
これはほぼ記述が変わりませんが、インターフェースをフィールドとして持っていた部分が、関数をオブジェクトとして持っています。
DIの層を見てみましょう
functionHandler := handler.NewTaskDoHandlerWithFunc(
usecase.TaskDo,
)
functionHandler.Handle(ctx, inputTask)
usecaseを実体化する必要はなく、関数オブジェクトをそのまま利用し、handlerのコンストラクタに渡せています。
テストを書く際も、モックを利用することなく使い捨ての関数を入れればいいのでテストがやりやすいです。
フィールドに別のオブジェクトを持つ場合を考える
リポジトリパターンなど、他にDIしなければならないとき、ありますよね。
package repository
import "playground/domain"
type TaskRepository interface {
Save(task *domain.Task) error
}
package repository
import (
"database/sql"
"playground/domain"
domainRepo "playground/domain/repository"
)
var _ domainRepo.TaskRepository = (*taskRepository)(nil)
type taskRepository struct {
db *sql.DB
}
func NewTaskRepository(db *sql.DB) *taskRepository {
return &taskRepository{
db: db,
}
}
func (r *taskRepository) Save(task *domain.Task) error {
_, err := r.db.Exec("INSERT INTO task (id, name, status) VALUES (?, ?, ?)", task.ID, task.Name, task.Status)
if err != nil {
return err
}
return nil
}
こんな感じでリポジトリを定義してみました。普段の感じでこのリポジトリを利用、DIしていくと以下のようになります。
package usecase
import (
"context"
"fmt"
"playground/domain"
"playground/domain/repository" // 追加
)
type TaskDoUsecase interface {
Run(ctx context.Context, task *domain.Task) (*domain.Task, error)
}
type taskDoUsecase struct {
taskRepo repository.TaskRepository // 追加
}
var _ TaskDoUsecase = (*taskDoUsecase)(nil)
func NewTaskDoUsecase(
taskRepo repository.TaskRepository, // 追加
) *taskDoUsecase {
return &taskDoUsecase{
taskRepo: taskRepo, // 追加
}
}
func (u *taskDoUsecase) Run(ctx context.Context, task *domain.Task) (*domain.Task, error) {
fmt.Println("Instance Usecase")
task.Do()
u.taskRepo.Save(task) // 追加
return task, nil
}
これを関数に適用してみます。
package usecase
import (
"context"
"fmt"
"playground/domain"
"playground/domain/repository"
)
type TaskDoFunc func(ctx context.Context, task *domain.Task, taskRepo repository.TaskRepository) (*domain.Task, error)
var _ TaskDoFunc = TaskDo
func TaskDo(ctx context.Context, task *domain.Task, taskRepo repository.TaskRepository) (*domain.Task, error) {
fmt.Println("Function Usecase")
task.Do()
taskRepo.Save(task)
return task, nil
}
するとどうでしょう、関数の引数としてrepositoryが必要になってしまいました。これではクリーンアーキテクチャ的には内部の知識が漏れ出てしまっています。なので別のアプローチを考えてみます。
package usecase
import (
"context"
"fmt"
"playground/domain"
"playground/domain/repository"
)
type TaskDoFunc func(ctx context.Context, task *domain.Task) (*domain.Task, error)
func NewTaskDoFunc(taskRepo repository.TaskRepository) TaskDoFunc {
taskDoFunc := func(ctx context.Context, task *domain.Task) (*domain.Task, error) {
fmt.Println("Function Usecase")
task.Do()
taskRepo.Save(task)
return task, nil
}
return taskDoFunc
}
関数のコンストラクタ関数を作ってみました。これにより、関数オブジェクトの生成時にrepositoryを渡しておけば、それが使われます。記述も簡潔になり、かなり良さそうに見えます。
しかしこれにも問題があって、テストが非常に書きづらいです。
あくまで関数オブジェクトを返す関数を外部に公開しているだけなので、テスト範囲としては正しく関数オブジェクトを生成できているか? くらいしかチェックができません。これをしっかりテストしようとすると、プライベート関数に出力部分を切り出し、ホワイトボックステストを書く方法はありますが、ブラックボックステストを書きたい場合は不可能です。関数の責務を、正しく実行可能な関数を返す、というところまで増やすことでテスト自体は可能ですが、なかなか煩雑に見えることもあるでしょう。
まとめ
関数オブジェクトでやりくりするのはすでにオブジェクト指向で書かれているプロジェクトでは嬉しくないし、諦めてインターフェースとコンストラクタでやりくりしよう
Discussion