はじめまして。仙台オフィスMIRAILチームのsaisaiです。
今回は弊社のRESTAPIで実践・導入しているGoのアーキテクチャをご紹介します。
採用したアーキテクチャと経緯
仙台オフィスで開発している動画配信サービスMIRAILは、バックエンドにGoを採用しており、frameworkはGin、ORMはGormを使っています。 アーキテクチャについてはクリーンアーキテクチャを元にしています。「元にしている」という具体的な内容は後述しますが、クリーンアーキテクチャを完全に踏襲してはいません。一部は独自規約を制定の上設計しています。
まず前提としてクリーンアーキテクチャとはという所ですが、よく紹介される下図をみるとわかる通り円の中心にEntitiesがありそこに向かって依存関係を注入していきます。なのでFramework & Driversレイヤーはどこにも依存してはいけないということになります。この概念を念頭においた上で各レイヤーを組み立てて行きます。
アーキテクチャとしては他にレイヤード、オニオン、ヘキサゴナル等がありますが、目的は一緒でそれぞれのレイヤーを独立させることでUIやDBに依存しない可用性のある環境を構築することです。 その中でもクリーンアーキテクチャはビジネスロジックをEntitiesに向かって作るというシンプルな考えなのでアーキテクチャの理解度が早いと感じました。 結果、今回のRESTAPIでは最終的にjsonを整形して返す形なのでEntitiesから必要なものをjsonで返すというさらに簡素化された作りが出来ています。
MIRAILのRESTAPIアーキテクチャ
こちらがクリーンアーキテクチャに基づいて作成したパッケージ構成です。
|- app |- infrastructure | L dao |- interfaces | L presentation | L repository |- usecase |- domain |- helpers
クリーンアーキテクチャとの対応
レイヤー | 今回の構成 |
---|---|
Framework & DriversのUI | app |
Framework & DriversのDB | infrastructure/dao |
Interface AdaptersのControllers | interfaces/presentation |
Interface AdaptersのPresenters | interfaces/repository |
Application Business Rules | usecase |
Entities | domain |
※helpersは共通機能となります
Framework & Driversレイヤー (app、infrastructure)
appにはrouterやGoのエントリポイントをおいています。 infrastructureには DBとのコネクションや3rdPartyとのやりとりなどを記載します。 レイヤーに対してディレクトリを分けているのはDBとUI部分を分離しているためです。
Interface Adaptersレイヤー(interfaces)
presentationにはrouterから呼ばれるコントローラー処理を記載し、ビジネスロジックとなるusecaseを呼び出します。
以下はpresentation記載例です。UserInteractorというusecaseを実装しています。実装内容の詳細はrepositoryの際に説明します。 ここではusecaseはビジネスロジックでpresentationから呼び出されるといことのみご理解ください。
package presentation import ( "domain" "helpers" "infrastructure/dao" "interfaces/repository" "usecase" ) type UserPresentation struct { Interactor usecase.UserInteractor } func IUserPresentation(conn *dao.Connection) *UserPresentation { return &UserPresentation{ Interactor: usecase.UserInteractor{ UserRepository: &repository.UserRepository{ Conn: conn, } }, } } func (Presentation *UserPresentation) Index(c *gin.Context) { //コントローラ処理 ui, err := Presentation.Interactor.UserFromID() res := Response{ Name: ui.Name } c.JSON(200, res) }
次にキモとなるrepositoryです。 repositoryはpresentationとは逆方向でusecaseから呼び出され、クエリを記載しています。前述しましたがusecaseはビジネスロジックなのでDBにアクセスする必要がでてきます。 ここで矛盾が発生します。 クリーンアーキテクチャは中心に向かって依存するので、外側のレイヤーは依存してはいけません。要はusecaseではinfrastructureを実装できないのです。 ではどうやって解決するかというと同心円の右下にある図が該当します。
これはDIP は Dependency Inverse Principal (依存関係逆転の原則)を表していて、UseCaseのInput・Outputの各インターフェイスを定義しそこに依存することで外側のレイヤーにアクセス可能となります。 今回はControllers=interfaces/presentation、Presenters=interfaces/repositoryとしていますので、presentationではrepositoryを抽象化(interface)したものを実装しているusecaseを呼び出しています。
そして、以下がrepositoryです。
こちらにクエリを記載してデータのCRUDを行います。
このソースでおや?と思う箇所がinfrastructure/daoをimportしているところです。散々外部レイヤーに依存しないという話を繰り返しましたが、ここでちゃぶ台がひっくり返ります。実は上のpresentationの参考ソースでもinfrastructure/daoをimportしています。ここがクリーンアーキテクチャを元にしているという所以です。
何故かというとinfrastructure/daoは github.com/go-sql-driver/mysql
と gorm
を使いコネクションを司っているのですが、DIPを取り入れるとinterfaceが必要になります。interfaceには使用するgormのメソッドを記載する必要が出てくるので記載が冗長になることを危惧してdaoに関してはプロジェクトとして依存注入をOKとする規約にしています。
package repository import ( "api/domain" "api/infrastructure/dao" "database/sql" ) type ( // UserRepository Connection UserRepository struct { Conn *dao.Connection } ) func (r *UserRepository) FindByUserID(id int) (d domain.User, err error) { //クエリを記載 }
Application Business Rulesレイヤー(usecase)
Interactorとしてビジネスロジックを書きます。 同レイヤーのUserRepositoryinterfaceを実装してDIPを実現しています。
package usecase type UserPurchaseInteractor struct { UserRepository UserRepository } func (interactor *UserInteractor) GetUser(key string) (user domain.User) { user = interactor.UserRepository.FindUser(key) return }
interfaceです。
package usecase import "api/domain" type UserRepository interface { FindUser(int) domain.User }
Entitiesレイヤー(domain)
中心円の箇所でデータ構造だけを定義します
package domain type User struct { ID int `column:"id"` Name string `column:"name"` } type Users []User
まとめ
データフローとしては以下となります。
app.router->interfaces/presentation->usecase.repository -実装-> usecase.interactor-> interfaces/repository -> domain
GoはPHPでいうLaravelやRubyのRails等アプリケーションフレームワークがほぼないので、ある程度規模があるプロジェクトにおいてはアーキテクチャの決めが必要になってきます。 弊社ではdaoをinterfaces層まで依存させており、クリーンアーキテクチャを完全には遵守していませんが、実際のプロジェクトで実践する際もカッチリはめる必要はなく、概念を捉えた上でルールを決めながら進めていく事が重要だと思います。今回ご紹介したのは数ある例の一つではありますが、もしクリーンアーキテクチャを実践される際の参考になれば嬉しいです。
PR
そんなGoを採用しているプロダクト公式動画配信サービスMIRAILの構築事例をCloud Next 19で紹介いたします! 後日資料も公開されるはず、、ですのでまたご紹介いたします