はじめに
GameWith AdventCalendar 2019 の17日目の記事になります。
普段はサーバサイドエンジニアとしてリプレイスの担当をしています。自宅に戻るとひたすら、スプラトゥーン2をプレイしています・・・。先週の週末には当記事を書き終える予定でしたが、スプラトゥーン2の欲に負けて急いで書き上げている今日です。ポケモン剣盾も両パッケージおよび Switch Lite まで用意したのに未だにスプラトゥーン2をやり続け...(略
・・・余談はさておき、本題に入ります。
GameWith Developer Blog でも取り上げられていますが、現在弊社では GameWith のリプレイス を行なっています。
目次
今回はリプレイスでコード自動生成を実装したお話をさせていただきます。
内容は次のとおりになります。
導入した経緯
@inosy22 の CleanArchitectureでGolangらしいPackage構成を考える でも言及されていますが、CleanArchitecture/DDDで設計した場合以下のデメリットが実装時に発生しました。
- 各機能の実装時に決まりきったディレクトリ/ファイル定義が辛い
- importの依存関係を毎回定義するのが辛い
- Entity 等のほぼ決まりきった属性値の定義が辛い
短期的な観点では辛いのを我慢して実装をすれば良いのですが、長期的な観点で考えた場合いち早く導入した方が、チーム全員ドメイン部分の開発に注力出来るため開発速度の向上しメリットが多いことと、私のような貧肉勢では我慢の限界に達した...(笑) と言っても過言ではありません。
選定ライブラリ
- Code generator
- https://github.com/dave/jennifer
- 選定理由としては、Golang で完結したかった事と
ast
で定義する程の内容でもないためです。 - まだシンプルな定義しかしていませんが、初期学習コストを乗り越えれば、比較的追加/変更は、そんなに手間でもない状況です。
- CLI
- https://github.com/spf13/cobra
- 選定理由としては一番メジャーであった事と入力値のバリデーションなど実現したい事が網羅されていたためです。
構成
リプレイスは CleanArchitecture/DDD で設計しているため、以下の図のように全層/各層/各ジェネレーター単位でコードの生成が行えるようになっています。各ジェネレーターを疎結合にすることで今後仕様変更が起きた際に追加/削除/組み合わせの変更
等を容易に出来るようにしています。
またコード自動生成で実装する範囲に関しては、基本 interface
, struct
, constructor
の定義のみに大抵のものは留めています。理由としましては、詳細度を上げると仕様変更をした場合に変更コストが高くなってしまい、変更に対して億劫になってしまうためです。現時点では常日頃から細かいリファクタリングを行う事が多いため必要最低限にしています。
軽い実装例
今回はある程度共通部分が多いので一部抜粋して説明させていただきます。
はじめにdave/jennifer
の大雑把な構造は次のようになっています。
File // mixin *Group
↓↓↓
Statement // File.items ([]Code) "Comment, Type 等が Statement になります"
// "Return" 等の場合は "Statement" の中で Group を生成するケースもあります
↓↓↓
Code
レンダリングに関しては、 package
etc...をレンダリングした後に File.items
に append されたものを順番にレンダリングして行きます。
以下はコードを生成するコマンドの例です。
$ ./codegen scaffold -n=dummy -a=id@int,name@string
Created: /domain/dummy/entity.go
Created: /infra/dummy/aggregate.go
Created: /infra/dummy/repository.go
Created: /infra/dummy/datasource_xxx.go
Created: /app/dummy/service.go
Created: /app/dummy/output.go
Created: /app/dummy/input.go
Created: /app/dummy/response_xxx.go
Created: /app/dummy/presenter_xxx.go
Created: /app/dummy/controller_xxx.go
この生成されたコードの中から controller
と entity
に絞って実装例を説明させていただきます。
Controller
まずは生成されたコードの例です
基本定義しかしていないので、中身は薄いですが必要最低限の interface
, struct
, constructor
のみ定義しています。
package dummy
// ControllerXXX interface for controller
type ControllerXXX interface{}
// controllerXXX implements ControllerXXX
type controllerXXX struct {
dummySvc Service
}
// NewControllerXXX is constructor
func NewControllerXXX(dummySvc Service) ControllerXXX {
return &controllerXXX{dummySvc: dummySvc}
}
次に生成するコードの例です
このままではデカいため NewController(Generator)
, interface
, struct
, constructor
それぞれ分けて説明いたします。
// NewController is constructor
func NewController(entity *ControllerEntity) Generator {
pkgName := NewNameCreator().CreatePkgName(entity.Name)
return &controller{
entity: entity,
source: jen.NewFile(pkgName),
pkgName: pkgName,
}
}
func (c *controller) Generate() (*Result, error) {
interfaceName := "ControllerXXX"
structName := strcase.ToLowerCamel(interfaceName)
serviceName := "Service"
serviceVariableName := strcase.ToLowerCamel(c.entity.Name) + "Svc"
// interface
c.source.Commentf("%s interface for controller", interfaceName)
c.source.Type().Id(interfaceName).Interface()
// struct
c.source.Commentf("%s implements %s", structName, interfaceName)
c.source.Type().Id(structName).Struct(
jen.Id(serviceVariableName).Id(serviceName),
)
// constructor
c.source.Commentf("New%s is constructor", interfaceName)
c.source.Func().
Id(fmt.Sprintf("New%s", interfaceName)).
Params(
jen.Id(serviceVariableName).Id(serviceName),
).
Id(interfaceName).
Block(
jen.Return(
jen.Op("&").Id(structName).Values(jen.Dict{
jen.Id(serviceVariableName): jen.Id(serviceVariableName),
}),
),
)
return &Result{
source: c.source,
relativePath: fmt.Sprintf("/app/%s/controller_xxx.go", c.pkgName),
}, nil
}
NewController
こちらは単純な constructor
ですが、 Generator
interface で統一すると、 Command
の定義がシンプルになると新たにコードの自動生成を追加したい場合ルールが出来て楽になるため統一しています。
// Generator is code generator interface
type Generator interface {
Generate() (*Result, error)
}
// NewController is constructor
func NewController(entity *ControllerEntity) Generator {
pkgName := NewNameCreator().CreatePkgName(entity.Name)
return &controller{
entity: entity,
source: jen.NewFile(pkgName),
pkgName: pkgName,
}
}
interface
とてもシンプルなのであまり説明する事はないかもしれませんが、
なので、今回の場合はコメント > インタフェース
の順番でレンダリングされます。
インターフェースの Type().Id(interfaceName).Interface()
は、
Type() -> type
Id(interfaceName) -> ControllerXXX
Interface() -> interface{}
とパースされ type ControllerXXX interface{}
がレンダリングされます。
c.source.Commentf("%s interface for controller", interfaceName)
c.source.Type().Id(interfaceName).Interface()
↓↓↓
// ControllerXXX interface for controller
type ControllerXXX interface{}
補足
Interface()
に引数を渡す事で関数の定義も出来ます。
c.source.Type().Id(interfaceName).Interface(
Id("Index").Params().Error(),
)
↓↓↓
// ControllerXXX interface for controller
type ControllerXXX interface{
Index() error
}
struct
構造体の定義になります。こちらも非常にシンプルですが以下のように展開がされます。
また引数が複数ある場合は jen.Id(serviceVariableName).Id(serviceName),
の箇所に引数を追加してください。
c.source.Commentf("%s implements %s", structName, interfaceName)
c.source.Type().Id(structName).Struct(
jen.Id(serviceVariableName).Id(serviceName),
)
↓↓↓
// controllerXXX implements ControllerXXX
type controllerXXX struct {
dummySvc Service
}
補足
プリミティブな型の場合は keywords.Types に定義されているメソッドを利用することが可能です。
例:
c.source.Type().Id(structName).Struct(
jen.Id(serviceVariableName).Id(serviceName),
jen.Id("hoge").String(),
)
↓↓↓
// controllerXXX implements ControllerXXX
type controllerXXX struct {
dummySvc Service
hoge string
}
constructor
constructor (関数)の定義です。今までと比べると少々複雑ですが、以下のパースされレンダリングします。
Func() -> func
Id -> NewControllerXXX
Params -> (
Id -> dummySvc
Id -> Service
)
Id -> ControllerXXX
Block -> {
Return -> return
Op -> &
Id -> controllerXXX
Values -> {dummySvc: dummySvc}
}
// func NewControllerXXX(dummySvc Service) ControllerXXX {
// return &controllerXXX{dummySvc: dummySvc}
// }
// constructor
c.source.Comment(fmt.Sprintf("New%s is constructor", interfaceName))
c.source.Func().
Id(fmt.Sprintf("New%s", interfaceName)).
Params(
jen.Id(serviceVariableName).Id(serviceName),
).
Id(interfaceName).
Block(
jen.Return(
jen.Op("&").Id(structName).Values(jen.Dict{
jen.Id(serviceVariableName): jen.Id(serviceVariableName),
}),
),
)
// NewControllerXXX is constructor
func NewControllerXXX(dummySvc Service) ControllerXXX {
return &controllerXXX{dummySvc: dummySvc}
}
Entity
Entity に関しては動的な部分の生成について説明いたします。
まずは生成されたコードです
Entity の構造体は属性の定義が、面倒になるケースが多いためコード自動生成の対象に含めました。また他にもこの属性値を利用するケースがあり、1度で済ませたい欲も含まれています...!
以下は ./codegen domain -n=dummy -a=ID@int,Name@string,Address@*string
を実行すると作成されます。
package dummy
// Entity is dummy attributes
type Entity struct {
ID int
Name string
Address *string
}
// Entities is entity list
type Entities []Entity
次に生成するコードです
基本的に大したことはしていませんが、 ID@int,Name@string,Address@*string
の部分を jen.Id(name).Id(typ)
にパースして Entity
の構造体に渡しています。
func (d *domain) Generate() (*Result, error) {
attrs, err := d.parseAttributes()
if err != nil {
return nil, err
}
d.source.Type().Id("Entity").Struct(attrs...)
...
}
func (d *domain) parseAttributes() ([]jen.Code, error) {
attrs := strings.Split(d.entity.Attributes, ",")
codes := make([]jen.Code, 0, len(attrs))
for _, attr := range attrs {
s := strings.Split(attr, "@")
if 1 >= len(s) {
return nil, fmt.Errorf("%s is undefined variable type", s[0])
}
name, typ := s[0], s[1]
codes = append(codes, jen.Id(name).Id(typ))
}
return codes, nil
}
補足
状況によりますが、 import が必要な型を定義する場合は予め定義するか、または後から自身で import する必要があります。
https://github.com/dave/jennifer/#importname
Command
最後にコマンドの緩い説明です。
こちらも特別な事は特にしておりませんが、コマンドではロジックをほぼ書かず引数の検証程度で後は Generator の 追加/削除
だけで完結するような構成にしています。
func CreateAppCommand() (*cobra.Command, error) {
appCmd := &cobra.Command{
Use: "app",
Run: NewTasker().Create(func(flags *pflag.FlagSet) *TaskParams {
name, err := flags.GetString("name")
if err != nil {
return nil, err
}
dir, err := flags.GetString("dir")
if err != nil {
return nil, err
}
return &TaskParams{
Generators: []Generator{
NewService(&ServiceEntity{Name: name}),
NewOutput(&OutputEntity{Name: name}),
NewInput(&InputEntity{Name: name}),
NewResponse(&ResponseEntity{Name: name}),
NewPresenter(&PresenterEntity{Name: name}),
NewController(&ControllerEntity{Name: name}),
},
Dir: dir,
}
}),
}
f := appCmd.Flags()
f.StringP("dir", "d", "", "base dir")
f.StringP("name", "n", "", "name ex: dummy")
if err := appCmd.MarkFlagRequired("name"); err != nil {
return nil, err
}
return appCmd, nil
}
まとめ
- 各ファイルの定義で思考停止することがなくなりドメイン部分の開発に注力できるのでチーム全体の開発速度が向上しました。
- 一度大きな構成変更をしているのですが、必要最低限の定義に留めているため短時間で改修することができました。
- コード自動生成により設計のルールがより強固になりました。
-
dave/jennifer
を利用して一定の学習コストはありますが、それを乗り越えれば、そんなに支障がないので、個人的にはお気に入りです。 - スプラ欲に負けると後の祭りになることを改めて再認識しました...(苦)