golangでClean Architecture[2]
概要
前回クリーンアーキテクチャの実践的学習のため簡易な非クリーンgolangアプリを作成してみた。そこから「実践ドメイン駆動設計」をある程度読んでLv3からLv5くらいになった気がしたのでWebアプリとコンソールアプリをクリーンで実装してみる(GitHub[最新版(main)])。
レイアウト
修正後のfix_interactorブランチ版のレイアウトは以下の形になった。
├── domain
│ └── model
│ └── task
│ ├── repository.go
│ └── task.go
├── external
│ ├── common
│ │ ├── app_type.go
│ │ ├── app.go
│ │ ├── deploy_type.go
│ │ └── store_type.go
│ ├── console
│ │ ├── command
│ │ │ ├── command.go
│ │ │ ├── create.go
│ │ │ ├── delete.go
│ │ │ ├── exit.go
│ │ │ ├── help.go
│ │ │ ├── none.go
│ │ │ ├── read.go
│ │ │ ├── readall.go
│ │ │ └── update.go
│ │ ├── constant
│ │ │ └── constant.go
│ │ └── app.go
│ ├── infurastructure
│ │ └── persistence
│ │ ├── inmemory
│ │ │ └── task
│ │ │ └── repository.go
│ │ └── rdb
│ │ └── mysql
│ │ ├── setting
│ │ │ └── mysql_setting.go
│ │ └── task
│ │ └── repository.go
│ └── web
│ ├── config
│ │ ├── config.go
│ │ ├── config.yml
│ │ └── constant.go
│ ├── controller
│ │ ├── task
│ │ │ └── controller.go
│ │ └── notfound_controller.go
│ ├── middleware
│ │ └── middleware.go
│ ├── model
│ │ └── route.go
│ ├── route
│ │ └── task_route.go
│ ├── view
│ │ ├── layouts
│ │ │ └── layout.html
│ │ └── views
│ │ ├── common
│ │ │ └── notfound.html
│ │ └── task
│ │ ├── edit.html
│ │ ├── index.html
│ │ ├── new.html
│ │ └── show.html
│ └── app.go
├── usecase
│ └── task
│ └── interactor.go
├── go.mod
├── go.sum
└─ main.go
前回(non_clean_web)からの変更点
▼ファイル名やディレクトリ名の整理
クリーンに関わるところではないが、ファイル名やディレクトリ名を整理した。
├── domain
│ └── model
│ └── task
│ ├── repository.go
│ └── task.go
→ 境界づけられたコンテキストを意識して1段ディレクトリ階層を作った。
│ ├── infurastructure
│ │ └── persistence
│ │ ├── inmemory
│ │ │ └── task
│ │ │ └── repository.go
│ │ └── rdb
│ │ └── mysql
│ │ ├── setting
│ │ │ └── mysql_setting.go
│ │ └── task
│ │ └── repository.go
→ 永続化領域であることを明示し、メモリ管理とRDB管理(またその種類)で分別した。
▼アプリケーション呼び出しの修正
前回は決め打ちでコンソールかWebのアプリ起動を行なっていた。
func main() {
/*
app := web.App{}
app.Run()
*/
app := console.App{}
app.Run()
}
標準ライブラリのflagを用いて起動時オプションで切り替えられるように変更した。
// main.go
func main() {
app := NewApp()
log.Fatal(app.Run())
}
func NewApp() common.App {
dep := flag.String("dep", string(common.Develop), "デプロイ環境")
appType := flag.String("type", string(common.Web), "アプリ種別")
store := flag.String("store", string(common.Memory), "永続化種別")
flag.Parse()
log.Printf("\n >>dep: %s\n >>type: %s\n >>store: %s", *dep, *appType, *store)
if *appType == string(common.Web) {
return web.NewWebApp(common.DeployType(*dep), common.StoreType(*store))
} else {
return console.NewConsoleApp(common.DeployType(*dep), common.StoreType(*store))
}
}
前回の考察に記載したが、それぞれは「external/console」「external/web」に配置している。CAの「Use Cases」の外側の円に当たる部分で、ヘキサゴナルアーキテクチャで言うアダプター、または「システムの外部」に当たる。
それら「システムの外部」と、「Use Cases」層より内側の円の「システムの内部」を切り離すことで、外部(アダプター)を差し替えるだけでアプリケーションの本来の関心事を流用することができる。
▼DIP(依存関係逆転の原則)
前回はdomain層からinfurastructure層のrepositoryの詳細(具象)クラスを直接参照しており、domain→infuraの依存の流れがあった。ただしdomain層は最も安定度が高く(他に影響されず)、柔軟性が低い(手が入る)レイヤーであるべきなため、何らか影響を受けやすいものに依存してはいけない。そのためDIPを用いてinfura→domainの依存の流れへ変える。
// external/infurastructure/persistence/inmemory/task/repository.go
package task
import (
"context"
"github.com/ArtefactGitHub/Go_T_Clean/domain/model/task"
)
type inMemoryTaskRepository struct {
tasks []task.Task
}
// TaskRepositoryインターフェース型で返す
func NewInMemoryTaskRepository() (task.TaskRepository, error) {
// 仮データ
tasks := []task.Task{task.NewTask(0, "first")}
r := inMemoryTaskRepository{
tasks: tasks,
}
return &r, nil
}
// domain/model/task/repository.go
package task
import (
"context"
)
type TaskRepository interface {
GetAll(ctx context.Context) ([]Task, error)
Get(ctx context.Context, id int) (*Task, error)
Create(ctx context.Context, task Task) (int, error)
Update(ctx context.Context, task Task) (*Task, error)
Delete(ctx context.Context, id int) (bool, error)
Finalize()
}
domain層で定義したrepositoryの抽象(interface)に、infura層が依存する形になっている。
ここでrepositoryのinterfaceは何故domain層に置くかがピンと来なかった。repository自体はアプリケーションサービスのusecaseから呼び出される形になっており、infura→usecaseの依存の流れでも良いのではと思った。
// usecase/task/interactor.go
〜〜〜
type taskInteractorImpl struct {
repository task.TaskRepository
}
func NewTaskInteractor(repository task.TaskRepository) TaskInteractor {
return taskInteractorImpl{repository: repository}
}
func (i taskInteractorImpl) GetAll(ctx context.Context) ([]task.Task, error) {
return i.repository.GetAll(ctx)
}
〜〜〜
「実践ドメイン駆動設計」では集約などと同じモジュール、つまりdomainにインターフェースを配置し、実装はinfurastructureに配置するとある。
この前段の「12.1 コレクション志向のリポジトリ」を少し読んで何となく気が付いた気がする。
repositoryというものが永続化領域にアクセスするかどうかはさておいておかなければいけない。アプリケーションサービスは、集約の取得や追加、更新などの操作を行うための何らか詳細はよく知らない窓口オブジェクト(インターフェース)を使いたいと思っている。よく知らないのだから同じ階層ではないし、ビジネスロジックが絡むのであればusecaseではなくdomainにあるよねと思った。repositoryの機能的な部分だけ捉えてどこに配置するのが正しいのかを考えてしまって見えていなかった。
▼依存性の注入
前回は直接repositoryを生成していたが、簡単に依存性の注入を実装した。
// external/web/app.go
〜
func (app *webApp) getRoutes(cfg config.MyConfig) []model.Route {
if app.storeType.IsMySql() {
repository, err := rdb.NewMySqlTaskRepository(
setting.NewMySqlSetting(
cfg.SqlDriver, cfg.User, cfg.Password, cfg.Protocol, cfg.Address, cfg.DataBase,
))
if err != nil {
log.Fatalf("NewMySqlTaskRepository() error: %s", err.Error())
}
interactor := task.NewTaskInteractor(repository)
return route.NewTaskRoute(interactor).GetRoutes()
} else {
repository, err := inmemory.NewInMemoryTaskRepository()
if err != nil {
log.Fatalf("NewInMemoryTaskRepository() error: %s", err.Error())
}
task := task.NewTaskInteractor(repository)
return route.NewTaskRoute(task).GetRoutes()
}
}
専用に処理を切り出して行うべきところではあるが、簡単に試すためこの辺りは手抜き。一応実行時オプションでインメモリかMySQLかを選択でき、インフラを自由に差し替えられる形でドメインを扱える。
▼改善
usecaseの辺りもまだ手抜きではある。きちんとやるのであればそれぞれのユースケースに対応した用意する。
usecase
└── task
├── getall_interactor.go
├── get_interactor.go
├── create_interactor.go
├── update_interactor.go
└── delete_interactor.go
// getall_interactor.go
package task
import (
"context"
"github.com/ArtefactGitHub/Go_T_Clean/domain/model/task"
)
type GetAllTaskInteractor interface {
Execute(ctx context.Context) ([]task.Task, error)
}
type getAllTaskInteractor struct {
repository task.TaskRepository
}
func NewGetAllTaskInteractor(repository task.TaskRepository) GetAllTaskInteractor {
return taskInteractorImpl{repository: repository}
}
func (i getAllTaskInteractor) Execute(ctx context.Context) ([]task.Task, error) {
return i.repository.GetAll(ctx)
}
ただDDDは複雑な業務プロセスを適切に分解する用途なため、CRUDのそれほど複雑ではない上記サンプルのようなものであれば流石に分けなくてもよいかと思う(interfaceにしなくても)。
その他同様にサンプルに含まれていないもの。
・Presenter(Output Boundary)
→Input Boundaryで戻り値を返さず、OutputBoundaryで返すようにすれば
よさそうだろうか。でもいる?
・Input Boundary受け渡しのInput Data
→ 各ユースケース毎にクラス作ればよいだろうか。いる?
・ViewModel
→ これはWebアプリの形であればよく使いそう。
・テスト
→ そういえば書いていなかった。
あとがき
つい最近実践ドメイン駆動設計のサンプルがあることを知って見てみたが階層関係でとても参考になった。DDD自体は特定のアーキテクチャに依存していないが、ヘキサゴナルを通してクリーンの理解に繋がった。またなるセミさんのQiita記事や紹介されている動画が非常に分かりやすく、助けられた。もうちょっと早く勉強しておけばよかった系ではあったが、まだまだ血や肉になっていないのでクリーン本も(そのうち)買って、今後活用して身につけたいと思う。