go言語でクリーンアーキテクチャっぽいもの

2018年12月06日 木曜日

「go言語でクリーンアーキテクチャっぽいもの」のイメージ

IIJ 2018 TECHアドベントカレンダー 12/6(木)の記事です】

はじめに

最近go言語でDDDやクリーンアーキテクチャを実践してみたという記事を多く目にするようになりました。
自分が半年ほど前に初めてgoでサーバーを実装することになった際も、先人達の記事のおかげでどうにか形にすることができました。

特に pospome さんの Goのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考える はとても参考にさせていただきました。
(勝手に出してしまって申し訳ありません。)

恩返しというわけでは無いですが、自分なりに消化して実装したものを紹介したいと思います。サンプルコードは実際のコードを元にでっち上げて書いているので、ところどころ粗があるかもしれません。

アプリケーションの要件

まだ外部には未公開のサービスなので詳細は出せませんが、今回紹介するアプリケーションは以下のような要件を持ちます。

  • REST HTTP サーバーとして動作する
  • バックエンドとしてmysqlとredisを利用する
  • redisのpubsubを通して他のサーバーとやり取りする

つまり通常のRESTとは別に、pubsubからの入力も受けつつ、2種類のバックエンドをうまいこと使い分ける必要があります。
この要件を満たすサーバーを出来るだけシンプルに実装するために、処理の流れを整理しつつ各レイヤが隔離されたアーキテクチャが必要でした。

アーキテクチャ詳細

概要

最初にアーキテクチャの概要図を紹介します。矢印は依存の方向を示します。

基本的にはクリーンアーキテクチャに従い、handler -> usecase -> domain <- infra の流れに依存性を固定します。
後で具体的に書きますが、infra層のstructのinstanceを作成し、singleton として保持しておく「registry」を設けています。

handler層

handler層では主にrequestからパラメータを取り出しそれをusecase層に投げ、レスポンスの生成を行います。
大きく分けると

  • HTTP Request を受け取るhandler
  • Redis のsubscribeから受け取るhandler

の2種類を定義しています。Redisへのsubscribe処理はinfra層で行いますが、入力を受け取る時はhandler層をかますようにします。

またどの層もですが、テストを容易にするため必ず下の層はDIを通して呼び出します。
(例えばhandlerではdomain層のrepositoryやserviceを「new」して、usecase層へ依存性を注入しています。)

以下はHTTTP Requestを処理するhandlerの例です。

usecase層

usecase層ではhandlerから入力を受け取り、ビジネスロジックに従ってdomain層を呼び出します。
例として Create メソッドでは、受け取ったパラメーターからデータを作成し、他サービスに通知する流れを記述しています。

domain層

domain層は中で3つの層に別れており、それぞれservice => repository => modelの順に依存しています。ここでも依存の方向は一方向に固定しています。

repository

repositoryには依存性の逆転を適用するためinterfaceのみを記述します。
DBへ保存や検索を行うメソッドが多いですが、実装は全てinfra層にあります。

model

modelはドメインモデルの定義を記述します。
自分自身を変更するようなメソッドなど、若干の実装を含みます。

現状はほぼDBのカラムに引っ張られてmodelが定義されてしまっているため、今後その辺りはリファクタリングしていきたいところです。

service

serviceにはdomain層に閉じる何らかの処理を記述するようにしています。usecase層と責務が曖昧になりがちですが

  • 処理の内容がdomain層に閉じるべきである時
  • テストが必要なロジックをusecaseから分離したい時
  • その他domainに関わるちょっとしたメソッド

あたりはserviceに実装するようにしています(usecase層でメソッドを作りたくなったらserviceにするイメージ)。
以下にサンプルを載せますが、「HTTP」や「Mail」といったinfra層にあるべき名前が出てきてしまっていて、あまりいい例ではないですね。

infra層

infra層にはdomain層のrepositoryの具体的な実装を記述します。主にDBへの保存などある意味一番泥臭いコードを書いていきます。
下の例ではNewTodoRepositoryがrepository.Todoのinterfaceを返すことで、usecase層がdomain層に依存できるようになっています。

infra層の中でもDBとのコネクション管理などを行う部分はdaoとして独立させています。

registry

クリーンアーキテクチャなどにregistryは登場しませんが、 Goのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考える で紹介されている通り、実装とinterfaceを結びつけてusecaseにDIするために利用します。

handlerとinfraの橋渡し的な存在で、シングルトンの管理やinfraの初期化処理を一箇所にまとめておけるため便利です。
さらにinfra層でpubsubからのinputをhandlerに渡すために、handlerを登録してinfraで利用するのにもregistryを利用しています。
(PubsubHandleでhandlerを登録して、EventRepositoryの初期化処理時に渡してるあたり)

終わりに

goでクリーンアーキテクチャっぽい設計を実践するための具体的なコードを紹介しました。
当然これで完成という訳ではなく、domain層を含めて日々リファクタリングを続けています。それでもアーキテクチャとしての破綻は今の所なく、テストも容易に書けているため、まずまず成功したのではないかと思っています。

もし~した方がいいというアドバイスや、その他理解不足なところがあれば、はてなブックマークやTwitterでコメントなど頂けると幸いです。

余談

実際のコードをぼかして書いたことで、何を見せたかったのかぼやけてしまったところが多々あったかもしれません。
サンプルはサンプルとして一通り動作するようなものを用意できれば良かったかなーと思います。

藤本 椋也2

藤本 椋也

2018年12月06日 木曜日

IIJ ネットワーク本部 IoT基盤開発部 センサーネットワーク課所属。2015年に新卒入社。webアプリケーション実装・運用を中心にやりたいことがあれば何でも手を出してみる所存です。

Related
関連記事