BASEプロダクトチームブログ

ネットショップ作成サービス「BASE ( https://thebase.in )」、ショッピングアプリ「BASE ( https://thebase.in/sp )」のプロダクトチームによるブログです。

Goでレイヤードアーキテクチャのボイラープレートコード自動生成ツールを作った話

f:id:budougumi0617:20191220165217p:plain
2019 アドベントカレンダー23日目

この記事はBASE Advent Calendar 2019の23日目の記事です。

devblog.thebase.in

こんにちは、11月よりBASE BANK株式会社に入社し、Dev Divisionに所属している清水(@budougumi0617)です。
23日目の本記事では、レイヤードアーキテクチャを採用している上で頻出するであろうボイラープレートの悩みを共有します。
そして、Go言語(以下Go)でコードを自動生成するためのツールを作った話と、利用したtext/templateパッケージなどの公式パッケージの概要に触れます。

自動生成時に利用するテンプレートはGoのtext/templateパッケージを利用しますが、テンプレートファイルは自由に変更できるので、将来的にはGo以外のコードも生成する予定です。

BASE BANKにおけるアーキテクチャ構成

私が所属するBASE BANK株式会社では、「YELL BANK(エールバンク)」という即時に資金調達ができる金融サービスを提供しています。
この「YELL BANK(エールバンク)」というサービスはGo製のサーバとPython製のサーバを組み合わせたバックエンド構成で提供されています。
Go製のサーバに関しても、Python製のサーバに関してもウェブアプリケーションフレームワーク(WAF)を用いていないため、実装はレイヤードアーキテクチャに則ったコード、ディレクトリ構成となっています。
Go製のサーバのpackage構成(ディレクトリ構成)と各々のパッケージの実装内容についてはBASE BANK同僚の東口さん(@hgsgtk)の登壇資料をご参照ください。

このようなレイヤードアーキテクチャによる設計アプローチは、言語やWAF特有な構成に依存しないため、Goで実装されていても、Pythonで実装されていてもディレクトリ構成自体はほぼ同じです。
また、適切に分割された各コンポーネントのコードが担う責務はシンプルな単一責務であるため、プロダクトに途中から参加したような私でも比較的簡単にコードを理解することができました。
ただ、レイヤードアーキテクチャやクリーンアーキテクチャというアプローチを取ったとき問題になるのが、似たような構造のコードを大量に生成する必要が発生することです。

レイヤードアーキテクチャとボイラープレート

レイヤードアーキテクチャやクリーンアーキテクチャに基づいて実装を行なっていくと、ひとつのユースケースに対して大量のファイルを作成する必要が発生します。
たとえば、「ユーザーを登録する」というような機能を作成する場合、我々のプロダクトでは、最低でも7ファイルを作成する必要があります。

./
├── domain
│   ├── model/user.go
│   └── repository/user.go
│ 
├── infrastructure/datastore/user.go
│ 
├── interfaces/controller
│       ├── create_user_controller.go
│       └── create_user_controller_test.go
│
└── service
     ├── create_user_service.go
     └── create_user_service_test.go

クリーンアーキテクチャやレイヤードアーキテクチャに詳しい方ならば、なんとなくファイル内容は想像できるかと思いますが、一部を記載するとこんなカタチです。

// domain/model/user.go
type UserID int
type User struct{
     ID UserID
     // has some fields...
}
// domain/repository/user.go
package repository

type UserGetter interface {
    Get(model.UserID) (model.User, error)
}

type UserSaver interface {
     Save(model.User) (mode.UserID, error)
}
// infrastructure/datastore/user.go
package datastore

type UserStore struct {
     db *sql.DB
}

func (*UserStore) Get(id model.UserID) (model.User, error) { /* do anything... */}
func (*UserStore) Save(u model.User) (model.UserID, error) { /* do anything... */}
// service/create_user_service.go
package service

type UserRegisterService interface {
    Run(UserRegisterInput) (UserRegisterResult, error)
}

func NewUserRegisterService(/* some args... */) UserRegisterService {
    return &userRegisterService{ /* fill fields... */ }
}

type userRegisterService struct {}
// service/create_user_service_test.go
package service

func TestUserRegisterService_Run(t *testing.T) {
     tests := []struct {/* test conditions */}{
          {/* test case 1 */},
          {/* test case 2 */},
     }
     for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
               // Arrange, Act, Assert...
          })
     }
}
// interfaces/controller/create_user_controller.go
package controller

type UserRegisterController struct {
    Service service.UserRegisterService
}

func NewUserRegisterController(urs service.UserRegisterService) UserRegisterController {
    return UserRegisterController{
        Service: urs,
    }
}

func (c *UserRegisterController) Handler(w http.ResponseWriter, r *http.Request) {
      // Handle Request by UserRegisterService
}

このように、ひとつのリクエストを処理するために複数の実装をする必要があります。

上記ではユーザー登録用のコードを例にしましたが、ユーザー削除を行う際には(modelコードを除いて)上記の例のように同じ構成のファイルの組み合わせを「新たに」実装します。ここで、新たに作るユーザー削除用のコード群は各構造体の引数やメソッドの処理内容は違えど、ファイルを構成する要素はユーザー登録時とほとんど同じになります。
以上の実装方針に則ると、ひとつのレイヤー(ディレクトリ)の中には上記のようなコードのファイルが大量に増えていきます。

./
└── service
     ├── create_user_service.go
     ├── create_user_service_test.go
     ├── read_user_service.go
     ├── read_user_service_test.go
     ├── update_user_service.go
     ├── update_user_service_test.go
     ├── delete_user_service.go
     ├── delete_user_service_test.go
     ├── create_company_service.go
     ……

同様の設計アプローチを取られている方ならば、同様の悩みをもったことがあるのではないでしょうか。
ほぼ似た構造体をつくることになるので、私は作成済みのエンドポイントのファイルをコピーして再利用可能な部分だけ残してから実装を行なっていました。エンジニアの三大美徳(怠惰(怠慢)、短気、傲慢)に反してますね。

rails generateコマンドやcake bakeコマンドのようなWAFのコード自動生成機能を使えるならばスケルトンコードをすぐに生成できるでしょう。
ということで、今回は与えた「モデル名」と「アクション名」、「テンプレートファイル」に基づいて各レイヤーのボイラープレートを自動生成するためのCLIツールを作りました。

レイヤードアーキテクチャ用ボイラープレートの自動生成ツール lgen

作成したCLIツールは以下になります。

レイヤードアーキテクチャの実装はプロダクトやチームによっても微妙に差異があります。 そこでこのツールは以下のような特徴を持ちます。

  • 実行オプションに渡されたモデル名アクション名を使ってコードを自動生成する
  • 生成するコードは指定されたディレクトリ配下にあるファイルを読み込んで決める
  • ディレクトリ構成もコピーすることで、各プロダクトのレイヤー構成に対応する
  • テンプレートやディレクトリ構成を自由に設定できるので、様々なレイヤー構造に柔軟に対応できる

ざっくり使い方を書くと、以下のようになります。

まず最初に、後述するtext/templateパッケージを使ってアクション名モデル名を変数としたこのようなボイラープレートの雛形を書いておきます。

// templates/usercases/get_user_usecase.go
package usecase

type {{ .Action | title}}{{ .Model | title }}Input struct{}

type {{ .Action | title}}{{ .Model | title }}Result struct{}

type {{ .Action | title}}{{ .Model | title }}Usecase interface {
  Run({{ .Action | title}}{{ .Model | title }}Input) ({{ .Action | title}}{{ .Model | title }}Result, error)
}

func New{{ .Action | title}}{{ .Model | title }}Usecase() {{ .Action | title}}{{ .Model | title }}Usecase {
  return &{{ .Action }}{{ .Model | title }}Usecase{
  }
}

type {{ .Action }}{{ .Model | title }}Usecase struct {}

func (u *{{ .Action }}{{ .Model | title }}Usecase) Run(
    in {{ .Action | title}}{{ .Model | title }}Input,
  ) ({{ .Action | title}}{{ .Model | title }}Result, error){
  // Need to implement usercase logic
  return {{ .Action | title}}{{ .Model | title }}Result{
    // Need to build result
  }
}

次に、自分のプロダクトのレイヤー(ディレクトリ)構造に合わせてそれぞれのファイルを配置します。

$ tree templates
templates
├── repositories
│   ├── repository.go
│   └── repository_test.go
├── controllers
│   ├── controller.go
│   └── controller_test.go
└── usercases
    ├── usecase.go
    └── usecase_test.go

そしてモデル名アクション名などを指定してツールを実行します。
次の例では、GetUser操作に関するコードをtemplatesディレクトリ配下のレイヤー構造とテンプレートを使って、myproductディレクトリ配下に自動生成します。

$ lgen -action get -model user -template ./templates -dist myproduct

実行すると、以下のようにtemplatesディレクトリと同じディレクトリ構造の場所にファイルが生成されます。

$ tree myproduct
myproduct
├── repositories
│   ├── get_user_repository.go
│   └── get_user_repository_test.go
├── controllers
│   ├── get_user_controller.go
│   └── get_user_controller_test.go
└── usercases
    ├── get_user_usecase.go
    └── get_user_usecase_test.go

最初に書いたボイラープレートの中身は、モデル名アクション名が展開されて以下のようなコードが生成されました。

// myproduct/usercases/get_user_usecase.go
package usecase

type GetUserInput struct{}

type GetUserResult struct{}

type GetUserUsecase interface {
        Run(GetUserInput) (GetUserResult, error)
}

func NewGetUserUsecase() GetUserUsecase {
        return &getUserUsecase{}
}

type getUserUsecase struct{}

func (u *getUserUsecase) Run(
        in GetUserInput,
) (GetUserResult, error) {
        // Need to implement usercase logic
        return GetUserResult{
                // Need to build result
        }
}

サンプルではテンプレートを数個しか用意しませんでしたが、これに加えて他のレイヤーのコード、対になるテストコードもテンプレートさえ用意すれば全て自動生成することができます。
この結果、いままで新しいロジックを書くときにコピペと置換を繰り返していた時間を削減できます。
これで2020年はロジックに集中してコードを書けそうです。

ツールだけでの紹介では終わってしまうので味気ないので、利用しているGoの標準パッケージについて紹介しておきます。

text/templateパッケージ

Goにおいて、コードの自動生成ツールをつくるのならば、text/templateパッケージ1を使うことになるでしょう2。このパッケージは名前の通りテンプレートエンジンの機能を提供しています。
あまりこのパッケージを直接利用することはないかもしれませんが、Go製のOSSでそのままtext/templateパッケージの仕組みが使われていることも多いです。
例えば、KubernetesエコシステムのひとつでGoで作られているHelmのChartの書き方もtext/templateパッケージそのままです。

https://helm.sh/docs/topics/chart_template_guide/

よって、直接利用することはなくともtext/templateパッケージの記法を覚えて損はないと思います。

text/templateパッケージの使い方はGoDocの該当パッケージの説明を読むのが一番です。

ざっくりと使い方を書くと次のようになります。

// https://play.golang.org/p/-bAjX-K1_TT
buf := bytes.Buffer{}
tmpl := `
username: {{ .Name }}
email: {{ .Email }}
`
params := struct {
    Name  string
    Email string
}{
    Name:  "John Doe",
    Email: "john.doe@exampl.com",
}
if err := template.Must(template.New("samples").Parse(tmpl)).Execute(&buf, params); err != nil {
    panic(err)
}

fmt.Printf("%s\n", buf.Bytes())
// username: John Doe
// email: john.doe@example.com
  1. bufは変数などを展開したあとの最終結果を格納するための変数です。
  2. tmplがテンプレート文字列です。{{ .Name}}などは「Executeメソッドの引数で受け取った構造体のNameフィールドをここに出力する」という意味になります。
  3. paramstmplテンプレートに埋め込むための構造体です。
  4. if文ではsamplesという名前でtmplテンプレート文字列からテンプレートを作り、params構造体を使ってテンプレートを展開する。という処理をしています。

ツールの紹介で記載したテンプレートでは、{{ .Model | title }}というような記述をしていました。これは変数の値を出力前に関数にパイプして加工することができる機能です。
利用関数はtype FuncMap map[string]interface{}が実体であるtemplate.FuncMapにマッピングしたあと、Executeメソッドを呼ぶ前にParseメソッドを使うことでテンプレート内で利用することが可能になります。
今回自作したツールの場合はstringsパッケージのstrings.Title関数をtitleとして登録しています。

// templates/usercases/get_user_usecase.go
var fmap = template.FuncMap{
    "title": strings.Title,
}

if err := template.Must(template.New(sp).Funcs(fmap).Parse(dtmpl)).Execute(&buf, l.params); err != nil {
   return err
}

もちろん自作関数を登録しておくことも可能です。

文字列長を返すlenなどは既定で登録済みだったりします。
その他、if文やrangeなどの制御構文もテンプレート内で利用することができます。詳細はtext/templateパッケージのGoDoc冒頭をご覧ください。

ちなみに、GoLandを利用している場合は、テンプレートファイルに{{- /*gotype: package/import/path.type_name*/ -}}といったコメントを入れておきます。
そうすることで、テンプレートファイル中でも構造体のフィールドに対する補完が有効になります。

path/filepathパッケージとosパッケージを使ったファイルの処理

今回自作したツールでは、コードの自動生成をする上で、指定されたディレクトリにあるテンプレートファイル群を利用します。
Goで「あるディレクトリの配下にあるディレクトリ、ファイルを使った処理」を書くには、path/filepath.Walk関数を使います。

他にも相対パスを絶対パスに変換するなどの処理があるのですが、ファイル・ディレクトリ処理に関しては@mattnさんがまとめてくださっている記事を見ればほぼ完結するので、ここでは説明を省略させていただきます。

今回は使っていないのですが、(上記記事が公開されたあとにリリースされた)Go1.12やGo1.13で、実行ユーザーのホームディレクトリを返すos.UserConfigDir関数やos.UserHomeDir関数も追加されています。

おわりに

今回はレイヤードアーキテクチャを使ってGoの実装をする際の悩みの共有をし、課題解決のためにコードを自動生成するコマンドラインツールを実装した話を共有しました。
みなさんのプロジェクトでどのようにコードのボイラープレート問題を解決しているのかも聞けたら幸いです。

また、コマンドラインツールを作る上で役に立つtext/templatefile/filepathパッケージの紹介をしました。
Goでコマンドラインツールを作ればクロスプラットフォーム対応が簡単にできます。みなさんもぜひ自作コマンドラインツールを作ってみてください。
また、「こういう機能があるならlgenを使ってもいいんだけどなあ」みたいな要望があればissueを立ててもらうか、Twitterで私にメンションを飛ばしてもらえると嬉しいです。

明日は、VP of Productの神宮司さんと、基盤グループのめもり〜さんです。

参考

最後に今回の記事・ツールの作成時に参照した情報を再掲しておきます。


  1. 今回は触れませんが、HTML生成用のテンプレートエンジンとしてはhtml/templateパッケージもあります。

  2. Goでコードの自動生成というと、gogenerateもありますが、今回は用途が違うので触れません。