
【Go言語】クリーンアーキテクチャで作るREST API
2022.03.15
【Go言語(Golang)】クリーンアーキテクチャで作るREST API
以前に投稿された以下の笹川先生の実装をクリーンアーキテクチャで作り替えてみよう!という内容です。
クリーンアーキテクチャとは?

参考:https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
クリーンアーキテクチャで調べるとよく出てくる例の円ですね。
ざっくり説明すると、円の中の各項目は矢印の向き(内側)に依存(参照)するようにして、特定の項目の仕様変更が他の項目に影響を及ぼさないようにしています。
それによってそれぞれが外部の項目を気にすることなくテストできたり、仕様変更の影響範囲が少なくなったりといった利点があります。
実装:クリーンアーキテクチャ化
では早速、クリーンアーキテクチャ化を進めていきましょう!
Entities
外側は内側の参照を持つため、内側から順に実装していきます。
1 2 3 4 5 6 7 | package entities type User struct { Name string `json:"name"` Age int `json:"age"` Address string `json:"address"` } |
こちらはそのままUserの構造体を入れているだけですね。
笹川先生のものと同じく、JSONでの受け取りからJSONでの返却まで、このUser構造体でやりとりするようにします。
Use Cases
UseCasesではFirestoreだったりecho(Http通信)だったりの技術的な要素を省いた、ビジネスルールを記載していきます。
具体的には、今回作成するaddUserの実装は以下のようになっているので、
- echoでユーザーデータ(JSON)を受け取り
- Firestoreにユーザーデータを追加
- echoで全てのユーザーデータ(JSON)を返す or エラーを返す
技術的要素を省いて、
- ユーザーデータを受け取り
- 何かしらにユーザーデータを追加
- 全てのユーザーデータを返却する or エラーを返す
といった感じに実装していきます。
また、addUserで全てのユーザーを返却するついでに、全ユーザー返却単品のものも追加してあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | package ports import ( "context" "firestore_clean/entities" ) // 1. ユーザーデータを受け取り type UserInputPort interface { AddUser(ctx context.Context, user *entities.User) error GetUsers(ctx context.Context) error } // 3. 全てのユーザーを返却 or エラーを返す type UserOutputPort interface { OutputUsers([]*entities.User) error OutputError(error) error } // 2. 何かしらにユーザーデータを追加、全ユーザーを返す type UserRepository interface { AddUser(ctx context.Context, user *entities.User) ([]*entities.User, error) GetUsers(ctx context.Context) ([]*entities.User, error) } |
Portではinterfaceのみを記述し、実体は別のgoファイルで実装します。
次は、同じくUseCasesのInteractorです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | package interactors import ( "context" "firestore_clean/entities" "firestore_clean/usecases/ports" ) type UserInteractor struct { OutputPort ports.UserOutputPort Repository ports.UserRepository } func NewUserInputPort(outputPort ports.UserOutputPort, repository ports.UserRepository) ports.UserInputPort { return &UserInteractor{ OutputPort: outputPort, Repository: repository, } } func (u *UserInteractor) AddUser(ctx context.Context, user *entities.User) error { users, err := u.Repository.AddUser(ctx, user) if err != nil { return u.OutputPort.OutputError(err) } return u.OutputPort.OutputUsers(users) } func (u *UserInteractor) GetUsers(ctx context.Context) error { users, err := u.Repository.GetUsers(ctx) if err != nil { return u.OutputPort.OutputError(err) } return u.OutputPort.OutputUsers(users) } |
Interactorでは、ユーザーデータを受け取るInputPortの実体と、先ほどの技術的要素を省いた1~3の手順を実装します。
技術的要素を省いた箇所についてはinterfaceを呼び出すことよって、UseCasesの外側にある技術的要素を隠蔽していますね。
これを依存性逆転の原則といい、円の外側への依存を回避した上で、コード変更の影響を最小限にすることができます。
Presenters
Presentersでは、OutputPortの実体を実装します。
また、ここからは技術的要素を含んでの実装になるので、echoやFirestore関連の実装も行なっていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | package presenters import ( "firestore_clean/entities" "firestore_clean/usecases/ports" "log" "net/http" "github.com/labstack/echo/v4" ) type UserPresenter struct { ctx echo.Context } func NewUserOutputPort(ctx echo.Context) ports.UserOutputPort { return &UserPresenter{ ctx: ctx, } } func (u *UserPresenter) OutputUsers(users []*entities.User) error { return u.ctx.JSON(http.StatusOK, users) } func (u *UserPresenter) OutputError(err error) error { log.Fatal(err) return u.ctx.JSON(http.StatusInternalServerError, err) } |
出力自体は、echoが行ってくれるので、echoのPOSTメソッド等でreturnする値を定義しています。
今回実装するREST APIでは、「全てのユーザーを返す」or 「エラーを返す」しかないので、この2パターンのみ実装していますね。
Gateways
Gatewaysでは、DB操作の実体を実装します。
なので、Firestore関連の実装をする感じになりますね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | package gateways import ( "context" "encoding/json" "errors" "firestore_clean/entities" "firestore_clean/usecases/ports" "fmt" "cloud.google.com/go/firestore" ) type FirestoreClientFactory interface { NewClient(ctx context.Context) (*firestore.Client, error) } type UserGateway struct { clientFactory FirestoreClientFactory } func NewUserRepository(clientFactory FirestoreClientFactory) ports.UserRepository { return &UserGateway{ clientFactory: clientFactory, } } func (gateway *UserGateway) AddUser(ctx context.Context, user *entities.User) ([]*entities.User, error) { if user == nil { return nil, errors.New("user is nil") } client, err := gateway.clientFactory.NewClient(ctx) if err != nil { return nil, fmt.Errorf("failed AddUser NewClient: %v", err) } defer client.Close() _, err = client.Collection("users").Doc(user.Name).Set(ctx, map[string]interface{}{ "age": user.Age, "address": user.Address, }) if err != nil { return nil, fmt.Errorf("failed AddUser Set: %v", err) } return getUsers(ctx, client) } func (gateway *UserGateway) GetUsers(ctx context.Context) ([]*entities.User, error) { client, err := gateway.clientFactory.NewClient(ctx) if err != nil { return nil, fmt.Errorf("failed GetUsers NewClient: %v", err) } defer client.Close() return getUsers(ctx, client) } func getUsers(ctx context.Context, client *firestore.Client) ([]*entities.User, error) { allData := client.Collection("users").Documents(ctx) docs, err := allData.GetAll() if err != nil { return nil, fmt.Errorf("failed GetUsers GetAll: %v", err) } users := make([]*entities.User, 0) for _, doc := range docs { u := new(entities.User) err = mapToStruct(doc.Data(), &u) if err != nil { return nil, fmt.Errorf("failed GetUsers mapToStruct: %v", err) } u.Name = doc.Ref.ID users = append(users, u) } return users, nil } // map -> 構造体の変換 func mapToStruct(m map[string]interface{}, val interface{}) error { tmp, err := json.Marshal(m) if err != nil { return err } err = json.Unmarshal(tmp, val) if err != nil { return err } return nil } |
ここはほとんど、笹川先生が実装したものと同じですね。
少し違う点としては、NewUserRepositoryでFirestoreのClientの初期化用interfaceを要求することで、AddUserとGetUsersそれぞれでFirestoreClientの初期化とClose処理を行うようにしています。
これで本番環境と開発環境を外部から変更することが可能になりますね。
また、errorを返却する際には fmt.Errorf でerror文を加工し、PresentersのOutputErrorに渡した際にそのerrorを表示するようにすることで、出力関連の処理を全てPresentersに任せるようにしています。
Controllers
Controllersでは、InputPort・OutputPort・Repositoryを組み立てて、InputPortを実行します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | package controllers import ( "context" "firestore_clean/adapters/gateways" "firestore_clean/entities" "firestore_clean/usecases/ports" "github.com/labstack/echo/v4" ) type User interface { AddUser(ctx context.Context) func(c echo.Context) error GetUsers(ctx context.Context) func(c echo.Context) error } type OutputFactory func(echo.Context) ports.UserOutputPort type InputFactory func(ports.UserOutputPort, ports.UserRepository) ports.UserInputPort type RepositoryFactory func(gateways.FirestoreClientFactory) ports.UserRepository type UserController struct { outputFactory OutputFactory inputFactory InputFactory repositoryFactory RepositoryFactory clientFactory gateways.FirestoreClientFactory } func NewUserController(outputFactory OutputFactory, inputFactory InputFactory, repositoryFactory RepositoryFactory, clientFactory gateways.FirestoreClientFactory) User { return &UserController{ outputFactory: outputFactory, inputFactory: inputFactory, repositoryFactory: repositoryFactory, clientFactory: clientFactory, } } func (u *UserController) AddUser(ctx context.Context) func(c echo.Context) error { return func(c echo.Context) error { user := new(entities.User) if err := c.Bind(user); err != nil { return err } return u.newInputPort(c).AddUser(ctx, user) } } func (u *UserController) GetUsers(ctx context.Context) func(c echo.Context) error { return func(c echo.Context) error { return u.newInputPort(c).GetUsers(ctx) } } func (u *UserController) newInputPort(c echo.Context) ports.UserInputPort { outputPort := u.outputFactory(c) repository := u.repositoryFactory(u.clientFactory) return u.inputFactory(outputPort, repository) } |
UserControllerのメソッドであるAddUserとGetUsersでは、echo.POSTの第二引数の型である func(c echo.Context) error を返しています。
なので、基本的には元々そちらに書かれていた内容を満たすような実装をしているだけですね。
Database(Firestore)
これは単純に、Firestoreのクライアントを取得するための実装です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | package database import ( "context" "cloud.google.com/go/firestore" firebase "firebase.google.com/go" "google.golang.org/api/option" ) type MyFirestoreClientFactory struct{} func (f *MyFirestoreClientFactory) NewClient(ctx context.Context) (*firestore.Client, error) { sa := option.WithCredentialsFile("credentialsFile.json") app, err := firebase.NewApp(ctx, nil, sa) if err != nil { return nil, err } client, err := app.Firestore(ctx) if err != nil { return nil, err } return client, nil } |
Gatewaysで宣言している、FirestoreClientFactoryを実装している感じですね。
Gatewaysの項目でも説明しましたが、これで容易に外部から本番環境とか開発環境の変更をすることが可能です。
Drivers
Driversでは、Controllerを呼び出し、echoの設定を行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | package drivers import ( "context" "firestore_clean/adapters/controllers" "github.com/labstack/echo/v4" ) type User interface { ServeUsers(ctx context.Context, address string) } type UserDriver struct { echo *echo.Echo controller controllers.User } func NewUserDriver(echo *echo.Echo, controller controllers.User) User { return &UserDriver{ echo: echo, controller: controller, } } func (driver *UserDriver) ServeUsers(ctx context.Context, address string) { driver.echo.POST("/users", driver.controller.AddUser(ctx)) driver.echo.GET("/users", driver.controller.GetUsers(ctx)) driver.echo.Logger.Fatal(driver.echo.Start(address)) } |
今回は、笹川先生の記事にはなかったツールとして、wireを使用してみました。
DIする時の面倒な初期化処理を自動で生成してくれるツールです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | //go:build wireinject // +build wireinject package drivers import ( "context" "firestore_clean/adapters/controllers" "firestore_clean/adapters/gateways" "firestore_clean/adapters/presenters" "firestore_clean/database" "firestore_clean/usecases/interactors" "github.com/google/wire" "github.com/labstack/echo/v4" ) func InitializeUserDriver(ctx context.Context) (User, error) { wire.Build(NewFirestoreClientFactory, echo.New, NewOutputFactory, NewInputFactory, NewRepositoryFactory, controllers.NewUserController, NewUserDriver) return &UserDriver{}, nil } func NewFirestoreClientFactory() database.FirestoreClientFactory { return &database.MyFirestoreClientFactory{} } func NewOutputFactory() controllers.OutputFactory { return presenters.NewUserOutputPort } func NewInputFactory() controllers.InputFactory { return interactors.NewUserInputPort } func NewRepositoryFactory() controllers.RepositoryFactory { return gateways.NewUserRepository } |
上記のコードを書いた後、drivers上でwireコマンドを実行することで、以下のコードが生成されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | // Code generated by Wire. DO NOT EDIT. //go:generate go run github.com/google/wire/cmd/wire //go:build !wireinject // +build !wireinject package drivers import ( "context" "firestore_clean/adapters/controllers" "firestore_clean/adapters/gateways" "firestore_clean/adapters/presenters" "firestore_clean/database" "firestore_clean/usecases/interactors" "github.com/labstack/echo/v4" ) // Injectors from wire.go: func InitializeUserDriver(ctx context.Context) (User, error) { echoEcho := echo.New() outputFactory := NewOutputFactory() inputFactory := NewInputFactory() repositoryFactory := NewRepositoryFactory() firestoreClientFactory := NewFirestoreClientFactory() user := controllers.NewUserController(outputFactory, inputFactory, repositoryFactory, firestoreClientFactory) driversUser := NewUserDriver(echoEcho, user) return driversUser, nil } // wire.go: func NewFirestoreClientFactory() gateways.FirestoreClientFactory { return &database.MyFirestoreClientFactory{} } func NewOutputFactory() controllers.OutputFactory { return presenters.NewUserOutputPort } func NewInputFactory() controllers.InputFactory { return interactors.NewUserInputPort } func NewRepositoryFactory() controllers.RepositoryFactory { return gateways.NewUserRepository } |
Controllerを初期化するために各interfaceの実装を指定して、最終的にDriversの初期化を行っているだけですね。
main.go
最後に、main.goでuser_driverを呼び出してみましょう!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | package main import ( "context" "firestore_clean/drivers" "fmt" "os" ) func main() { ctx := context.Background() userDriver, err := drivers.InitializeUserDriver(ctx) if err != nil { fmt.Printf("failed to create UserDriver: %s\n", err) os.Exit(2) } userDriver.ServeUsers(ctx, ":8000") } |
ここまで複雑な構造でしたが、main.goから見るとすごい簡単ですね!
動作確認
Postmanで確認してみると、以下のようになりました。
GetUsers

AddUser

ちゃんと動作してることが確認できました!
まとめ
取り扱ったコードは結構単純な処理だったので、クリーンアーキテクチャ化の恩恵は少し薄いかもしれませんが、仕様変更に強く、テストが書きやすいコードができたと思います。
今回のコードは以下のリポジトリにまとめてありますので、ぜひ確認してみてください!
書いた人はこんな人

IT技術10月 27, 2023Jiraの自動化
IT技術8月 16, 2023Lighthouseで計測したパフォーマンススコアのばらつきを減らす方法
IT技術11月 7, 2022Ruby on Rails&GraphQLのエラーレスポンス
IT技術7月 13, 2022Ruby on Rails & GraphQLの環境構築と実装