はじめに
サマリ
- Goを書くようになってからそろそろ1年が経ちます。
- GoでWeb APIを実装する際の個人的パッケージ構成が定まりつつあるので備忘録を書き連ねます。
- 当初はMVCで実装していました。
- 最終的にレイヤードアーキテクチャを用いたパッケージ構成に落ち着きました1。
- 本稿ではDDD等の文脈は切り離して、パッケージ構成を責務毎に分割するための取り組みとしてのみレイヤ系アーキテクチャを取り扱います。
レイヤ系アーキテクチャとは?
レイヤ構造を持ったアーキテクチャ(クリーン/オニオン/レイヤード...)等を指します。
MVCもレイヤ構造を持ちますが、今回はレイヤ系とは区別して説明いたします。
記事の内容
- (主に)Web APIを実装する際に採用している個人的アーキテクチャについて説明します。
- まず現在のレイヤードアーキテクチャに至る試行錯誤を話し、その後具体的なコードを交えながら解説します。
対象読者
- レイヤ系アーキテクチャを理解する最初の一歩が欲しい方向けです。
- レイヤ系アーキテクチャを極めるフローが下記の6段階あったとすると、自分は②と③の間くらいです。メチャクチャ赤ちゃんです。生暖かく見守っていただけると幸いです。
- ① 既存パッケージ構成に負債が蓄積される → ② アーキテクチャの試行錯誤を開始する → ③ レイヤ系アーキテクチャを導入する(軽量DDD) → ④ DDDを完全理解する → ⑤ アーキテクチャの原理原則にこだわらなくなる、敢えてフラットパッケージを採用する → ⑥ ...
MVCからスタートしてレイヤードに至った流れ
それでは早速Web APIのアーキテクチャ試行錯誤についてお話します。
レイヤ系アーキテクチャを理解していなかった当初、ある程度手に馴染んだMVC2を用いて実装を行なっていました。
MVCを用いてWeb APIを実装する場合、
- コントローラ - クライアントからのリクエストを受け付けモデルやビューを呼び出す処理を記述
- モデル - 永続化対象のクラスや構造体、永続化の処理やロジックを記述
- ビュー - JSON/HTML等を定義しレスポンスを返す処理を記述
みたいな感じになるかと思います。
シンプルなレスポンスの場合、ビューは作らずコントローラでそのまま返す場合も多いかも(自分も同様でした)。
余談ですが、MVCについてはGMOの成瀬さんの動画が神がかって分かりやすいです。
MVCをやっていく中で、いわゆるFat Controller/Fat Model問題にブチ当たりました。
ビュー無しのふたつのレイヤで処理をまかなっているので、コントローラかモデルのどちらかが大きな責務を担うことになります。
自分の場合はコントローラにたくさんゴニョゴニョ処理を書いており、可読性が低くテストもツライ感じに...。
そこで各種書籍を参考に、サービスレイヤをプラスした「MVC + S」アーキテクチャにリファクタしていきました。
コントローラにゴニョゴニョ書いていた処理を全てサービスレイヤに切り出し、コントローラは「入力を受け、サービスを呼び出し、レスポンスを返す」処理に終始します。
ビジネスロジックが全てサービスレイヤに剥がれたおかげでコントローラの見通しが良くなりました。
しかし、
- サービスとモデルのどちらにロジック書くか迷う問題
- 「MVC + S」がメチャクチャメジャーなわけでない(要出典)ので人によって解釈がブレる
- 多層になるなら最早レイヤ系アーキテクチャでも良いのでは?
- 技術的な実装は別の層に切り出したい...
等々、悶々とする部分がありまして、最終的にはレイヤードアーキテクチャに落ち着いていきました。
導入したアーキテクチャ
ここからは実際にWeb APIを実装する際に採用しているアーキテクチャを説明していきます。
成果物
コードがあった方が分かりやすいと思うので、こちらのリポジトリに格納しました。
パッケージ構成
パッケージ構成はこちらとほぼ(というか全く)同じです。
[GoでのAPI開発現場のアーキテクチャ実装事例 / go-api-architecture-practical-example](https://speakerdeck.com/hgsgtk/go-api-architecture-practical-example)責務の分割はレイヤードアーキテクチャ的にしつつ、ドメインとインフラの依存関係を逆転しています。
なので、正確には「レイヤード**(っぽい)**アーキテクチャ」です。
依存とは「AパッケージがBパッケージをimportしている状態(AがBに依存)」を指します。
レイヤードアーキテクチャではドメインがインフラをimportするのですが、本アーキテクチャではinterfaceを使って依存関係を逆転しています。
実際のパッケージ構成は下記の通りです。
.
├── application
│ ├── create_user_service.go
│ ├── find_user_service.go
│ ├── notification_adapter.go
│ └── ping_service.go
├── domain
│ ├── user.go
│ └── user_repository.go
├── handler
│ ├── create_user_handler.go
│ ├── find_user_handler.go
│ ├── ping_handler.go
│ └── shared.go
├── infra
│ ├── firestore.go
│ ├── slack_local_notifier.go
│ └── user_repository_impl.go
├── main.go
└── router.go
なぜこのパッケージ構成に?
- 用語が独特なアーキテクチャは難しそうだったので(The Clean Architecture等)、ある程度直感的(?)なレイヤードアーキテクチャをベースにしています。
- ドメインとインフラの依存逆転の理由は、ただの一般論ですがテスタブルかつプラガブルにするためです3。
- それと、本質ではないですがドメインに定義したモデルをリポジトリで利用可能になるのは実装上良かったです。
各レイヤの個人的方針メモ
ここからは各層にどんなコードを記述したか、個人的な方針をメモ代わりにつらつらと書きます。
handler
- 入力の受け取りと出力を記述します。
-
craete_user_handler.go
ではなくuser_handler.go
にCreate
とかDelete
メソッドをぶらさげる例もありますが、共通化のメリットをそこまで感じなかったので前者を採用しました。 - ハンドラーを呼び出すパブリックなメソッドはHandleにしました。
- 他の選択肢としてはRun等もあるかと思います(というか何でも良いと思います)。
application
- 処理のアウトライン(ビジネスロジック、ユースケース)を記述します。
- ユースケースとは?
- アプリケーションに入力を与えるとき、何かしらの挙動をアプリケーションに期待しています。
- 例えば「新規ユーザの登録」をしたくてWeb APIを叩いたりするわけです。
- ばっくり言えば、この「新規ユーザの登録」こそユースケースです。コード上は、新規ユーザを登録するために必要な「ユーザ情報を受け取り、DBに引き渡しクエリを発行し、結果を受け取る」といった一連のフローになります。
- 各ユースケースで再利用できる共通の処理があったとしても、ユースケースからユースケースを呼び出すのを禁止します。
shared
ディレクトリを切ったりSharedServiceを作る等して対応します。 - ドメインにはモデル構造体に付随する程度のロジックのみを書くので、複雑なビジネスロジックが必要なユースケースがあれば、本レイヤ内に処理を分割して記述していきます(レイヤ内設計の重要性)。
domain
- いわゆるモデル等を格納するレイヤです。初心者実装だとDBのレコードと紐付くするアイツです4。あとはバリデーションロジックとかファクトリ(特殊なルールを持たせたコンストラクタみたいな感じ)とか、値オブジェクトやドメインサービス等を置いたりするレイヤです。
- 値オブジェクトは、特殊なルールを持った値を
type Foo string
等と定義し、メソッドをぶら下げてバリデーションロジックを付与したりします。例えば「UserのIDは何桁以下にする」等のルールがあるのなら、プリミティブなstring
で扱うのではなく、値オブジェクト化した方が便利です。
- 値オブジェクトは、特殊なルールを持った値を
- モデルを永続化する「リポジトリ」のインターフェースを定義するのもこのレイヤです。
本来domainというのはどこにも依存せず、参照される定義や、構造体に少しパラメーターの加工を担う関数が生えている程度に収めるべき
infra
- 永続化や通知等の技術的な実装5を書いていきます。
- 永続化は「アプリケーション外部に値を渡したり、受け取ったりすること」をおおよそ指すと理解しています。つまりDBに限りません。外部APIへのアクセスやSaaSへのアクセス等、その他諸々を含みます。
その他
- 基本的に別レイヤから呼び出されるパブリックメソッドは、ひとつのみにします(あくまで「基本的」に)。パブリックメソッドに与える引数はユースケースレイヤの場合
XXXServiceInput
やXXXServiceOutput
等とし、レイヤをまたぐ時は独自の構造体(DTO)を定義するルールにします。これによりシステマティックに実装が可能です。 - 本来はWebアプリケーションFWも技術的な実装と捉えられますが、Web APIは割とFWの機能にべったりになったりもするので、直接
main.go
やroute.go
、ハンドラーでFWをimportしています。原理原則よりも、自分がやりやすい形に落とし込めれば良いと感じています。 - レイヤ系アーキテクチャではDIすることが多いです。動的にNewを行わず、コードの最初の方でDIを行い、全てのオブジェクトがFixした状態で処理を開始します。実行時エラーの危険性が減りますし、コードの見通しもよくなります。また、DI時にコード中で参照する変数も定義でき、一覧性が上がって分かりやすい面もあるかと思います(変数の注入がコードに散らばらない)。DIパッケージを切るのもありだと思います。
- リポジトリについて
- リポジトリはリスト構造が持つようなメソッドを生やすのを基本にします(ドメイン駆動設計 モデリング/実装ガイド p69)。
- こちらにリポジトリに生やすメソッドの一覧が載っていて分かりやすいです。
-
FindByXXX
系がたくさん生えてきたらFindBySpecification
のように抽象化して、検索条件(XXXSpecification)を引数で与える方法もあるようです。 - 永続化に関するコードを記述するので、Slack通知もある意味「Slack上にメッセージを固定化する」という観点から見れば永続化です。ただし今回はSlackへのメッセージ自体にドメイン的な意味がないと考えたので、ユースケースレイヤに
interface
を定義しインフラにはSlackLocalNotifier
を定義して注入しています(詳細はドメイン駆動設計 モデリング/実装ガイド p73)。ただ、外部との連携部分は基本的に永続化と捉えられるケースが多いと思うので、全てリポジトリとして実装するのもアリかなとは感じています。
さいごに
- レイヤ構造に流行り廃りはあるようですが、一度基礎的な部分を理解しておくと、レイヤ構造を採用しないときでもエッセンスを利用できるな、と感じました。
- レイヤ系アーキテクチャを理解すると、私のようにJava等を通っていない人間の場合、設計の基礎となる部分が身に付く気がしました。
- それこそGUIアプリケーションを考えても、ボタンをポチッと押したコールバック関数が入力で6、ボタンを押すことで実現したい処理のアウトラインを書くところがユースケースで、ドメインロジックを書くところがあって、技術的な実装があって...といったように、レイヤ系アーキテクチャを流用して設計することも可能だと思われます。
参考資料
- 書籍
-
pospomeさんのアーキテクチャ本
- 特に1と4が良かったです。1はレイヤ構造の話で、4はレイヤ内の実装方針の話でした。レイヤ内の設計方針の資料は貴重な気がします。
-
ドメイン駆動設計 モデリング/実装ガイド
- DDDを実装する上での実践的なTipsが載っていて良いです。
-
pospomeさんのアーキテクチャ本
- Web