GameWith Advent Calendar 2019 の記事です!
GameWithのリプレイスについて vol.1 ~概要編~ でのバックエンドの設計の知見になります。
前置き
この記事は2019/10/24にLT登壇させていただいた際に作った
下記のスライドを書き直したものです。
https://speakerdeck.com/inosy22/thinking-about-golang-like-package-architecture
概要
Golang で CleanArchitecture を参考にして、
Webアプリケーションを作成するにあたってのPackage構成の知見になります
注意
golang-standard/project-layout のようなディレクトリ構成には言及せず、
アプリケーション実装のディレクトリ構成のみに焦点を置いた話をします。
CleanArchitectureを参考にした設計
CleanArchitectureとは
レイヤードアーキテクチャの一つで、レイヤー間の依存関係を単方向にして、
それぞれのレイヤーを疎結合にできるアーキテクチャです。
詳細については、他に様々な良記事があるかと思いまですので割愛します。
こちらを採用するメリットとして一番大きいのが、疎結合になるため、
Webアプリケーションとしての多様なレスポンスや、多様なインフラ、採用してるライブラリやフレームワークの変更にとても強くなるという点だと感じています。
GolangでのCleanArchitectureを考える
WebFrameworkとして echo
、DBアクセサとして gorp
を採用している場合です。
CleanArchitectureを参考 にしているだけで、忠実には守っていません。
大きな違いは、 framework層 と adapter層 の密結合を許容している点です。
理由としては、framework自体が単方向依存を満たすような使い方をする方法がとても開発しづらくなると感じたためです。
(adapter層にframework層のinterfaceを定義すれば厳密に守ることはできる)
余談:
個人的には、これでもかなりCleanArchitectureを守っている方かなと思います。
そのため、レイヤー間をやり取りするための構造体やinterfaceが増えてしまうのが、機能の複雑性に対して過剰で面倒だなと感じる時もあります。
CleanArchitectureはあくまで理想論なので、現実に合わせて崩していくのがいいと考えてます。
CleanArchitectureのPackage構成を考える
GolangのPackageの仕様
- Packageはnamespaceのような機能を果たす
- 名前はスネークケースやキャメルケースはNG
- ディレクトリ直下のファイルは同じPacakge
- 裏を返せばディレクトリが違えば別Package
- ディレクトリ構成とPackage構成は同義
- 同じPackageの変数や関数へのアクセスは無制限
- GolangのPrivateはPackage内Private
レイヤー構成 (不採用になった)
一般的にGolang×CleanArchitectureでネットで調べると、
一番出てくるPackage構成がこのレイヤーに合わせたPackage構成です。
レイヤー名ディレクトリ > 役割名ディレクトリ(Package) > 機能名ファイル
の並びになります。
ディレクトリ(Pacakge)構成例
├── adapter
│ ├── controller
│ │ └── user.go
│ ├── gateway
│ │ └── mysql
│ │ └── user.go
│ └── presenter
│ └── json
│ └── user.go
├── application
│ ├── output
│ │ └── user.go\ (interface)
│ ├── repository
│ │ └── user.go\ (interface)
│ └── usecase
│ └── user.go
└── domain
└── user
└── entity.go
- ※ユーザーデータを取得してロジックを通して返すという場合の例です
- ※interface周りはこの例だと冗長に感じますが、実際は多様なレスポンスやインフラに対応するために必要なものです
レイヤー構成のメリデメ
メリット
- レイヤーや役割でPackageが別れているので、importで依存関係やメンバーアクセスのルールが縛れる(CleanArchitectureを守りやすい)
デメリット
- 機能が増えるにつれてPackageが肥大化する
- レイヤー内で他の機能に自由にアクセスが可能
- (IDEの補完のサジェストの肥大化)
- 機能の実装時にディレクトリの行き来が激しくてとにかく辛い...。
レイヤー構成の考察
小さいプロジェクトでレイヤー内の肥大化が発生しないような場合は有効な構成だと思います。
GolangのPackagePrivateの仕組みなどを活かしきれていないような...?
これは、役割Package > 機能File
という構成が原因です。
React×Reduxをやっていた時代に採用していた、Re-ducks構成がこの逆の考え方なので、
これを採用することによって解消できないかを検討してみます。
Re-ducks構成
Re-ducksとは
参考:https://github.com/alexnm/re-ducks
こちらは役割でディレクトリを切る形ではなく、機能でディレクトリを分けた上で、ファイル名に役割名をつける構成。
つまり、先ほどとは逆の 機能Package > 役割File
の構成になります。
Packageの縦割りと横割り
CleanArchitectureには、 役割, 機能 とは別にもう一つ レイヤー があります。
CleanArchitectureのレイヤーの概念でPackageを区切ること自体も、
GolangのPackagePrivateの特徴を潰してしまうので、一旦それすらも無視した状態で、
役割に対して、再利用性を考えた形でグルーピングしてみます。
再利用性を考えたグルーピングの基準とは、役割ごとの結合が基本的に 1:1
のものをグルーピングしていきます。
(厳密には共通Serviceのようなものが必要になるかもしれないですが、共通機能Packageとして別で定義しています)
グルーピングした結果が以下の図になります。
レイヤー×Re-ducks構成 (採用中)
先ほどのように、Re-ducksを参考にグルーピングした結果、独自のレイヤーのようなものが出来上がりました。
その結果を、app
(図左上) / infra
(図右上) / domain
(図下) という独自のレイヤーのようなグループ名をつけて、Packageの中は役割の名前でファイルを作っていきます。
ディレクトリ(Package)構成例
├── app
│ └── user
│ ├── controller.go
│ ├── output.go\ (interface)
│ ├── presenter_json.go
│ └── usecase.go
├── domain
│ └── user
│ └── entity.go
└── infra
└── user
├── datasource_mysql.go\ (gateway)
└── repository.go\ (interface)
- ※ユーザーデータを取得してロジックを通して返すという場合の例です
- ※interface周りはこの例だと冗長に感じますが、実際は多様なレスポンスやインフラに対応するために必要なものです
レイヤー×Re-ducks構成のメリデメ
メリット
- レイヤー構成のデメリットは大体解消できる
- Package内部に閉じ込められる部分が多くなるので、Privateな部分が妥協実装も許容しやすい
- (なんだかGolangのPackage仕様を活かしたPackage構成っぽい)
デメリット
- レイヤー違いで同Packageにあるので、メンバーアクセスが縛りづらくなる
- 実装者によってCleanArchitectureが崩れやすい
- importで名前が被るためaliasをつける必要あり
- (Kubenetesのコードとかもこうなっていたのでいいかな...)
結論
レイヤーにとらわれないPackage構成で実装者が幸せになりました!
GolangのPackageの仕組みを活かした構成にすることができた気がします!
しかし、実際の実装においては、デメリットに記載の通り、CleanArchitectureを崩し安くなってしまっており、CleanArchitectureゆえに、決まったファイルの構成などが多く面倒です。
そのデメリットは、自動コード生成を用いることで軽減しています。
Goのコード自動生成については、別途 GameWith Advent Calendar 2019 にて、他のメンバーが記事にしていただける予定なので、乞うご期待!