Goで実践クリーンアーキテクチャ

はじめまして。仙台オフィスMIRAILチームのsaisaiです。

今回は弊社のRESTAPIで実践・導入しているGoのアーキテクチャをご紹介します。

採用したアーキテクチャと経緯

仙台オフィスで開発している動画配信サービスMIRAILは、バックエンドにGoを採用しており、frameworkはGin、ORMはGormを使っています。 アーキテクチャについてはクリーンアーキテクチャを元にしています。「元にしている」という具体的な内容は後述しますが、クリーンアーキテクチャを完全に踏襲してはいません。一部は独自規約を制定の上設計しています。

まず前提としてクリーンアーキテクチャとはという所ですが、よく紹介される下図をみるとわかる通り円の中心にEntitiesがありそこに向かって依存関係を注入していきます。なのでFramework & Driversレイヤーはどこにも依存してはいけないということになります。この概念を念頭においた上で各レイヤーを組み立てて行きます。

f:id:saihiromichi:20190723110012j:plain
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

アーキテクチャとしては他にレイヤード、オニオン、ヘキサゴナル等がありますが、目的は一緒でそれぞれのレイヤーを独立させることで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を実装できないのです。 ではどうやって解決するかというと同心円の右下にある図が該当します。

f:id:saihiromichi:20190730083859p:plain

これは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/mysqlgorm を使いコネクションを司っているのですが、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で紹介いたします! 後日資料も公開されるはず、、ですのでまたご紹介いたします

cloud.withgoogle.com