この記事はBASE Advent Calendar 2019の23日目の記事です。
こんにちは、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
buf
は変数などを展開したあとの最終結果を格納するための変数です。tmpl
がテンプレート文字列です。{{ .Name}}
などは「Execute
メソッドの引数で受け取った構造体のName
フィールドをここに出力する」という意味になります。params
はtmpl
テンプレートに埋め込むための構造体です。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/template
やfile/filepath
パッケージの紹介をしました。
Goでコマンドラインツールを作ればクロスプラットフォーム対応が簡単にできます。みなさんもぜひ自作コマンドラインツールを作ってみてください。
また、「こういう機能があるならlgen
を使ってもいいんだけどなあ」みたいな要望があればissueを立ててもらうか、Twitterで私にメンションを飛ばしてもらえると嬉しいです。
明日は、VP of Productの神宮司さんと、基盤グループのめもり〜さんです。
参考
最後に今回の記事・ツールの作成時に参照した情報を再掲しておきます。
- https://github.com/budougumi0617/lgen
- 私が愛した怠惰・短気・傲慢 - BASE開発チームブログ
- The Chart Template Developer’s Guide - helm.sh
- text/template上で動く計算機を作る #golang - Qiita
- Go templates made easy | GoLand Blog
- Package template - The Go Programming Language
- Package filepath - The Go Programming Language
- Package os - The Go Programming Language
- Big Sky :: Golang で物理ファイルの操作に path/filepath でなく path を使うと爆発します。