golangでClean Architecture[1]
概要
これまでクリーンアーキテクチャについて、自分には縁遠いものとしてスルーしてきた。最近こちらのなるセミさんの動画の実践的で分かりやすい説明を見て理解がLv1からLv3くらいになった気がした。そして自分で手を動かしてやりたくなってしまう病なので手を出してみた(GitHub[最新版(main)])。
クリーンアーキテクチャ
クリーンアーキテクチャ(CA)とは
The Clean Code Blog記載のソフトウェア設計手法。どんなものか、何が利益かは「結論」に記されている。
自分の解釈は、CAは端的に、詳細な実装を置き換え可能にして以下のような色んなメリットを得ることが目的ということ。
・テストがしやすい
・開発時に仮実装で進めやすい(詳細な実装を保留にしておける)
・運用時の変更に強い(データベースやフレームワークを変えたり)
ルール
CAにはルールがある。プログラムを適切に整理して階層に分ける。その階層は同心円の幾つかの円の領域を指し、外側の階層から内側へ、プログラム的に依存している関係としなければならない。
実装(非クリーン)
先の「なるセミ」さんの動画のものを参考に、「タスク」を一覧・作成・更新・削除する簡単なWebアプリを、まずは非クリーンの形で実装してみる。この段階の実装はGitHub[non_clean_web]になる。
.
├── domain
│ ├── interactor
│ │ └── task_interactor_impl.go
│ └── model
│ └── task.go
├── external
│ └── web
│ ├── config
│ │ └── constant.go
│ ├── controller
│ │ ├── notfound_controller.go
│ │ └── task_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
├── infurastructure
│ └── inmemory_store.go
└── main.go
各階層について
▼external
「システム外部」の階層。図では「Controllers」「Gateways」の緑の円になる。
実行するプログラムがアプリケーション外部と関わる際の入口/出口の処理を行う。先のブログで「GUIのMVCアーキテクチャが完全に含まれるのはこのレイヤーです」と記されたインターフェースアダプターにあたる。「インターフェース」がプログラムの概念でもあり紛らわしいため単に「外部」を意味するexternalを採用した。
サンプルではエントリーポイントとなるmain関数を実装した「app.go」や、ブラウザからのリクエストとレスポンスを処理するControllerやView、ルーティングが含まれる。
func (app *App) Run() {
url := "localhost"
port := "8080"
router := mux.NewRouter()
routes := getRoutes()
for _, r := range routes {
switch r.Method {
case http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete:
router.HandleFunc(r.Path, r.Handler).Methods(r.Method)
default:
}
}
router.NotFoundHandler = http.HandlerFunc(controller.Notfound)
router.MethodNotAllowedHandler = http.HandlerFunc(controller.Notfound)
address := fmt.Sprintf("%s:%s", url, port)
log.Printf("running on %s", address)
log.Fatal(http.ListenAndServe(address, middleware.MethodOverride(router)))
}
type TaskController struct {
taskInteractor *interactor.TaskInteractor
}
「アプリケーション内部と切り分け可能」な状態を実現した際には、ここは別のものに置き換えられることを意味する。サンプルの状態でもビジネスロジックはdomainに閉じ込めているため、Webアプリではなくコンソールアプリに置き換えることも可能となっている。これはプログラムを適切に整理することが一部上手くいっている効果であると思われる。もしControllerへ直接実装するなどexternal層へビジネスロジックが侵食していたらそれは叶わない。
▼domain
アプリケーションのビジネスロジックを実装する階層。図では「Entities」の黄色の円になる。「task_interactor_impl.go」のサービスと「task.go」のドメインモデルクラスがある。
type TaskInteractor struct {
repository *infurastructure.InMemoryTaskRepository
}
type Task struct {
Id int `json:"id"`
Name string `json:"name"`
}
taskはこのアプリケーションの最も内側の「一般的(↔︎抽象的)」な概念を表したものであり、サービスは「一覧する」などの特定の振る舞いに応じてtaskを取り扱う。その際DBなど永続化領域からデータを取得したり保存するため、Repositoryを介してそれら操作を行うことになる。この例では「InMemoryTaskRepository」を通して行なっているが、同心円の依存方向的にルールを逸しており、非クリーンである。
また赤の円の「Use Cases」が無く、externalのControllerから詳細な実装であるdomainの階層が直接呼ばれており置き換え可能ではないため、こちらも非クリーンであるということと思われる。
▼infurastructure
アプリケーションの永続化領域の処理などを実装する階層。図では「external interfaces」の青色の円になる。永続化処理をメモリ上で行う「inmemory_store.go」がある。
type InMemoryTaskRepository struct {
tasks []model.Task
}
同心円を見るとexternalに含める形と思われたが、一応動画先の実装例を参考にひとまず別としてみた。
考察
サンプルでは一部上手く出来そうで、一部上手くいかないものがある。
Webアプリの「ガワ」をConsoleアプリに着せ替えることは可能だった(GitHub[non_clean])。先にも書いたがビジネスロジックの階層とそうでないものを分けることが出来た成果と言えると思う。
〜 〜 〜
├── external
│ ├── 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
〜 〜 〜
しかしそれぞれの階層間で実装の詳細に依存しており、置き換え可能となっていない。リポジトリは直接詳細な実装クラスを利用しているため、もし永続先をMySQLのデータベースへ変更したいと思っても、必ず「task_interactor_impl.go」に手を入れることになってしまう。詳細に依存せず抽象に依存する形となればそれらが解決するということだと思う。
またそれはマイクロサービスで特定のドメイン領域だけリレーショナルデータベースではなくキューストレージへの変更であったり、処理をサーバーレスのFaasに置き換える、というようなこともありうるのかなと思った。
あとがき
ひとまず非クリーンでの実装を行い、上手く出来たところ出来なかったところが見えた。またここまで書いたがクリーンアーキテクチャ本は読んでいない。先に実践ドメイン駆動設計を買ってしまい面白いのと、なるセミさんの動画でひとまず分かった気になったのでDDD本読了後とかで気が向いたら手に入れようかと思う。