Lenetという生活サービスのエンジニアをやってるカイ(@kai-zoa)です。
ちなみにディープラーニングの方のLeNetとはなんの関係もございません。読み方も違います。
で、Lenetの一部のサービスでは、3年程前からAPIをGoで実装していて、ディレクトリの掘り方からパッケージ間の依存に関するルールについて長いこと苦慮してきたのでまとめてみます。(今回は触りまで…)
コメントでも直接でも指摘があると大変ありがたいです。
#コンセプト
下記のようなポイントを抑えていってるつもりです。
※DDDでよく言ううように事業の特定領域や業務に関するレイヤーをここではドメイン層と呼びます
- ドメイン層のロジックと外部との入出力(インフラ層)まわりのロジックは明確に分けたい
- 特定のフレームワークを前提にしない
- ルールに従えば循環インポートの迷路に迷い込まない
- ヘキサゴナルアーキテクチャとかクリーンアーキテクチャの雰囲気を持ちつつGoらしさを維持したい(改善の余地ありな気がしてる)
#サンプルコードについて
JSONを返す簡単なWEBサーバーを作ります。
$ curl http://localhost:8080/today.info
{"ok":true,"year":2017,"month":12,"day":13}
本日の年月日がJSONで返ってきます。
#ディレクトリレイアウトと各パッケージについて
github.com/kaizoa/go-arch-example
$ tree
.
├── app/
│ └── today.go
│
├── bootstrap/
│ └── today.go
│
├── domain/
│ └── today/
│ ├── models.go
│ └── usecase_info.go
│
├── middleware/
│ ├── http/
│ │ ├── router.go
│ │ └── serve.go
│ ├── os/
│ │ └── now.go
│ ├── now.go
│ ├── router.go
│ └── M.go
│
├── web/
│ └── today.go
│
└── main.go
8 directories, 13 files
##web/ - HTTPハンドラの実装(インフラ層)
HTTPリクエストに応答するためのnet/http.Handler
の実装をする。
基本的には応答するjsonのレイアウトはこのパッケージで定義して、外部にも出したくない。
##app/ - アプリケーション層
インフラ層の実装をインジェクトする構造体を実装する。
依存関係の緩衝地帯のように使い方になる。
##domain/ - ドメイン層
事業の特定領域や業務に関するロジックを書くところ
といっても今回は現在時刻から年月日オブジェクトへの変換を行うのみ。
package today
import "time"
type InfoOutput struct {
Year int
Month time.Month
Day int
}
type InfoUseCase struct {
Now Now
}
func (u *InfoUseCase) Get() *InfoOutput {
// 現在時刻から年月日オブジェクトへの変換を行う
now := u.Now.Time()
out := &InfoOutput{}
out.Year, out.Month, out.Day = now.Date()
return out
}
##middleware/ - ミドルウェア
ドメイン層とは直接の関わりを持たない技術的な機能の実装をパッケージにしている。
OSの機能にアクセスするためのパッケージなど、フレームワークが実装しそうな機能、特定サービスへの通信クライアントを実装することが多い。
##bootstrap/ - ブートストラップ
起動時に呼び出されるサービスごとの関数を実装する。
この関数内でmiddlewareに依存したインフラ層の実装を初期化して、アプリケーション層、ドメイン層に渡すことで依存性の注入を行い、依存の反転を実現する。
#依存グラフ
#コードを追ってみる
書いてる途中です、すいません
#続きを書きたい…
今回は簡単な例にとどめてるので、より実践的なことについては別に書こうと思います。
- 他の外部インターフェイス実装の例(mysql, gRPC)
- 複数システム・サービスをまとめるときの例
- etc
気力があれば…