はじめに
クリーンアーキテクチャの考え方を採用したコードをGoで書いてみました。
すでにいろんな方がソースを書かれておりますが、一例として見ていただければと思います。
ソース(メモとタグを新規登録、閲覧できるAPI)
https://github.com/muroon/memo_sample
クリーンアーキテクチャの構成図とソースの一覧
左側はクリーンアーキテクチャの構成図です。
右側は各ソースがクリーンアーキテクチャのどのレイヤーに属しているかを示しています。
Clean Architectureとは
Clean Architecture (クリーンアーキテクチャ) とは、 Robert C. Martin (Uncle Bob) 氏が提唱したアーキテクチャである。
こちらの書籍を読んでいる方が多いのではないでしょうか
Clean Architecture 達人に学ぶソフトウェアの構造と設計
導入のメリット
-
テストが書きやすい
Mockを生成しやすくなることによりテストが実施しやすくなる -
フレームワークに依存しない
技術環境が変化、進化に対応しやすい
あまりないケースかもしれないが、
Webアーキテクチャとコンソールアーキテクチャを同じドメイン(ドメイン駆動設計で言うところのドメイン)で作成 -
データリソースからの独立
フレームワークに依存しないのと同様、技術環境が変化に対応しやすい
例えば、postgreSQLからMySQL、またはMongo, KVSに変更など
ルール
4つのレイヤー
アプリケーションの各機能を下記の4つのレイヤーに分類します。
レイヤー | 内容 |
---|---|
Enterprise Business Rules | エンティティーモデル |
Application Business Rules | ビジネスロジックを担う |
Interface Adapter | Controller, Presenter |
Frameworks & Drivers | Frwamework, Database, View |
Enterprise Business Rules
ビジネスルールの為のデータ構造を持ったオブジェクト。
データの実態を表す場所。
Application Business Rules
ビジネスルールを操作する場所。
つまりこのアプリケーションで何ができるかを実践します。
Interface Adapter
外部からの入力、データの永続化、表示を担当する場所
Frameworks & Drivers
Webフレームワーク、DB操作の実際に担うソース、
フロントエンドのUIなどがここに所属しています。
外側のレイヤーの要素を直接参照してはならない
上記の図におけるこの矢印は依存を表しており、
内側のレイヤーから外側のレイヤーの要素への依存を禁じます。
ここでいう依存とは要素(構造体、変数など)への直接参照をさせないということです。
では外側のレイヤー要素を参照せざる得ないは、どうするのでしょうか?
例えば、ビジネスロジックからデータアクセスオブジェクト(敷いてはリーソースへ管理する箇所)
そのようなときは必ずインターフェイスを使用します。
詳しくは依存性逆転の原則
ソースについて
メモとタグを新規登録、閲覧できるRestAPIアプリです。
- メモは関連するタグを付けることが可能
- タグとメモはN対N
Repository
インターフェイス
- アプリの使用要件からタグとメモそれぞれにrepositoryを設けます
- tag_repository
- memo_repository
- トランザクションを担うtransaction_repositoryを設けます
実装部
Repositoryの実装はadapter配下に設けてあります。
今回下記の2種類用意してあります。
- db
- memory (ソースのみ、Mock用)
一つのリポジトリに対して、dbとmemoryに分けたのは
例えば、usecaseの単体テストのときに一時的にdbではなく、Mockを使用したりすることが可能だからです。
規模が大きくなるとdbとmemoryのそれぞれ用意するほうがコスト高になるかもしれませんが
あるリポジトリだけMockにしたいみたいなケースもあるかと思います。
トランザクションなしの場合
// memo save
_, err = memoRepo.Save(ctx, "Memo Text")
if err != nil {
panic(err)
}
// tag save
_, err = tagRepo.Save(ctx, "Tag Text")
if err != nil {
panic(err)
}
トランザクションありの場合
defer func() {
if err := recover(); err != nil {
transactionRepo.Rollback(ctx)
}
}()
// begin
ctx, err := transactionRepo.Begin(ctx)
if err != nil {
panic(err)
}
// memo save
_, err = memoRepo.Save(ctx, "Memo Text")
if err != nil {
panic(err)
}
// tag save
_, err = tagRepo.Save(ctx, "Tag Text")
if err != nil {
panic(err)
}
// commit
_, err = transactionRepo.Commit(ctx)
if err != nil {
panic(err)
}
つまり、memoリポジトリとtagリポジトリのメソッドはトランザクション有無にかかわらず同一のメソッドが使用できます。
メソッド内部でトランザクションの有無を見ています。
こちらgoroutineにも対応しており、並行処理でも実行可能です。
https://github.com/muroon/memo_sample/blob/master/adapter/db/multi_thread_test.go
Controller, Presenter
インターフェイス
presenterのインターフェイスはUseCase層に所属します。
実装部
controller, presenterの実処理はInterface Adapter層に記載してあります。
Controller
requestパラメータをInput用モデルへ組み込み、UseCase Interactorに処理を渡しています。
func (c Controller) PostMemo(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx = addResponseWriter(ctx, w)
ipt := &input.PostMemo{Text: r.URL.Query().Get("text")}
c.it.PostMemo(ctx, *ipt) // ←Interactorに処理を渡している
}
UseCase Interactor
Interactor内から各処理を行い、Presenterへ処理を渡します。
func (i Interactor) PostMemo(ctx context.Context, ipt input.PostMemo) {
// 新規メモの登録
id, err := i.memo.Post(ctx, ipt)
if err != nil {
i.pre.ViewError(ctx, err) // ←Presenterに処理を渡している
return
}
// 登録メモの取得
iptf := &input.GetMemo{ID: id}
memo, err := i.memo.GetMemo(ctx, *iptf)
if err != nil {
i.pre.ViewError(ctx, err) // ←Presenterに処理を渡している
return
}
i.pre.ViewMemo(ctx, memo) // ←Presenterに処理を渡している
}
Presenter
プレゼンターでViewモデルの生成、表示を行います。
func (m presenter) ViewMemo(ctx context.Context, md *model.Memo) {
defer deleteResponseWriter(ctx)
w := getResponseWriter(ctx)
m.JSON(ctx, w, m.render.ConvertMemoJSON(md))
}
最後に
Dependency Injectionを利用して環境変化に対応した仕組みを作る考え方はすごいと思いました。
自分でつくってこんなにも時間がかかるものかと思いました。
ただ、正しいソフトウェアをつくることが最終的には最もはやくソフトウェアをつくることに繋がるとボブおじさんもおっしゃってました。
参考
【ボブおじさんのClean Architectureまとめ】オブジェクト指向 ~SOLIDの原則~