Slide 1

Slide 1 text

UseCaseの凝集度を高める
 Goのpackage戦略
 2023年09月20日
 株式会社ZOZO
 ブランドソリューション開発本部 バックエンド部 FAANSバックエンドブロック
 田村 誠基 Copyright © ZOZO, Inc. 1

Slide 2

Slide 2 text

© ZOZO, Inc. 株式会社ZOZO
 ブランドソリューション開発本部 バックエンド部 FAANSバックエンドブ ロック
 
 田村 誠基
 2020年4月 入社
 自社ECの運用保守業務に従事し、その後FAANSのバックエンド エンジニアとしてAPI開発をしています。 
 好きなエディタは、Neovimです! 
 最近、M2 ProのMacBookに変え、PCが爆速になった。 
 
 2

Slide 3

Slide 3 text

© ZOZO, Inc. 3 Agenda ● ショップスタッフの販売サポートツール「 FAANS」とは ● UseCaseの変更前実装 ● 解決のアプローチ ● DIツール wire の紹介 ● まとめ

Slide 4

Slide 4 text

© ZOZO, Inc. 4 ショップスタッフの販売サポートツール「FAANS」とは ● WEAR, ZOZOTOWN, Yahoo!ショッピング, ブランド様の自社ECへのコーディネート投稿や成果の確認 など、ショップスタッフの運用に必要な機能を搭載した業務支援ツール ● お客様がZOZOTOWN上でブランド実店舗の在庫取り置きを希望した際に、ショップスタッフが FAANS上 での簡単操作で取り置き対応を完結できる機能など存在 ● FAANSバックエンドでは、APIの実装をGo言語で開発

Slide 5

Slide 5 text

© ZOZO, Inc. 5 クリーンアーキテクチャを採用している ● 今回は、UseCase部分の実装のお話 「Cloud FirestoreからPostgreSQLへ移行したお話」

Slide 6

Slide 6 text

© ZOZO, Inc. UseCaseの変更前実装
 〜 ディレクトリ構成 〜
 6

Slide 7

Slide 7 text

© ZOZO, Inc. 7 UseCaseディレクトリ構成 ● usecaseディレクトリ直下にファイルを置いている ○ 例: usecase/shop.go ● usecaseディレクトリには90ファイル近く存在していて巨大 └── usecase ├── aa.go ├── bb.go ├── cc.go └── shop.go

Slide 8

Slide 8 text

© ZOZO, Inc. UseCaseの変更前実装①
 〜 1UseCaseに複数メソッドあることで、依存関係が増えている 〜 
 8

Slide 9

Slide 9 text

© ZOZO, Inc. 9 1UseCaseに複数メソッド持っている 1/2 type ShopUseCase interface { Create(context.Context, *ShopCreateInput) (*ShopOutput, error) Get(context.Context, *ShopGetInput) (*ShopOutput, error) Update(context.Context, *ShopUpdateInput) (*ShopOutput, error) Delete(context.Context, *ShopDeleteInput) (error) } type shopUseCase struct { aRepo repository.AReopsitory bRepo repository.BRepository cRepo repository.CRepository } ● ShopユースケースにCreate, Getのように複数メソッドを持たせている ● 必要なデータを受け渡しを行う dtoのInputはメソッドごとに定義している

Slide 10

Slide 10 text

© ZOZO, Inc. 10 1UseCaseに複数メソッド持っている 1/2 ● この実装でのメリットは、ユースケースを追加する場合は、メソッドを生やすだけで可能とい うことで、サクッと実装することができる ● 短期的に見るとそれで良いかもしれないが、中長期的に見ると依存関係が分かりづらくなって いきメンテナンス性が悪くなる

Slide 11

Slide 11 text

© ZOZO, Inc. 11 1UseCaseに複数メソッドあることで、依存関係が増加 1/2 type ShopUseCase interface { Create(context.Context, *ShopCreateInput) (*ShopCreateOutput, error) Get(context.Context, *ShopGetInput) (*ShopGetOutput, error) Update(context.Context, *ShopUpdateInput) (*ShopUpdateOutput, error) Delete(context.Context, *ShopDeleteInput) (error) BulkCreate(context.Context, *ShopBulkCreateInput) ([]*ShopOutput, error) } type shopUseCase struct { aRepo repository.AReopsitory bRepo repository.BRepository cRepo repository.CRepository dRepo repository.DRepository } 依存が増える場合がある (既存の改修により増える場合も) 新規メソッドが追加

Slide 12

Slide 12 text

© ZOZO, Inc. 12 1UseCaseに複数メソッドあることで、依存関係が増加 2/2 ● UseCaseに1メソッド追加されるごとに、依存関係が増える可能性がある ○ ユースケースが大きくなればなるほど、最大公約数的に増えてしまう ● それにより、テストコードを書く際にモックすべきクラスがどれか分からなくなってくる ○ テストコード書くのが大変になり面倒になる

Slide 13

Slide 13 text

© ZOZO, Inc. 13 試しに改善: 1UseCase1メソッドにする type ShopGetUseCase interface { Get(context.Context, *ShopGetInput) (*ShopOutput, error) } type shopGetUseCase struct { aRepo repository.AReopsitory } ● 無駄な依存が一切なくなり、最低限な依存のみとなり、変更による影響が最小化される ● テスト書く際には、モックすべきレポジトリが一目瞭然となり書きやすい

Slide 14

Slide 14 text

© ZOZO, Inc. 14 「1UseCase複数メソッド」と「1UseCase1メソッド」 ● 依存が最小限となっており、 1UseCase1メソッドの方が分かりやすい

Slide 15

Slide 15 text

© ZOZO, Inc. UseCaseの変更前実装②
 15 〜 似たような命名のstructがあり、見通しが悪い 〜 


Slide 16

Slide 16 text

© ZOZO, Inc. 16 似たような命名のstructがあり、見通しが悪い 1/2 type ShopUpdateInput struct { Name string } type ShopDeleteInput struct { Name string } ... ● ControllerからUseCaseを呼び出す際に利用する、 DTOのInputをUseCase(メソッドごと)に作成してい るので、どんどん見通しが悪くなるし、命名が被ることも考慮する必要がある

Slide 17

Slide 17 text

© ZOZO, Inc. 17 似たような命名のstructがあり、見通しが悪い 2/2 func convertShopModelToShopOutput(m shopModel) ShopOutput { return ShopOutput{} } ● 他にもDTO変換処理をusecaseパッケージ内に記載しているので、被らないようにするために名前が煩 雑になっている

Slide 18

Slide 18 text

© ZOZO, Inc. これらの実装を整理すると
 18

Slide 19

Slide 19 text

© ZOZO, Inc. 19 凝集度が低いと感じる ● 1ユースケースに複数メソッドあることで、依存関係が増えて、変更に弱いユースケースになっている ● UseCaseというパッケージが大きくて修正しづらい場面がある ○ DTOのInputや変換処理などの命名が被らないように気を付ける必要がある

Slide 20

Slide 20 text

© ZOZO, Inc. 解決のアプローチ
 
 
 20

Slide 21

Slide 21 text

© ZOZO, Inc. 21 解決の方針 ● 1UseCase1メソッドにして依存関係を最小限にしたい ● Inputや関数などの見通しの悪さや命名の煩わしさから解放されたい ● 開発案件も進める必要もあり、現状の実装を大きく変えずに、軽量に実施できる方法にしたい

Slide 22

Slide 22 text

© ZOZO, Inc. 22 パッケージ分割のアプローチ ● createShop、getShopなどは、完全に独立した実装が可能 ● 命名の煩わしさをなくすために、思い切って usecase/shop/create/usecase.goのようにパッケージを分割 ● Goのお作法的には細かく切らない方が良いと思うが、 FAANS においてはこの方針の方が開発しやすいのではということで新 規APIに関しては、模索しつつお試し実装をしてみた └── usecase ├── company │ └── list │ └── usecase.go └── shop ├── create │ └── usecase.go ├── delete │ └── usecase.go ├── get │ └── usecase.go └── update └── usecase.go

Slide 23

Slide 23 text

© ZOZO, Inc. 23 パッケージの命名について ● パッケージの切り方の命名 ● 基本的には、usecase/{ドメインモデル名}/{操作名}/usecase.goのルール ● 迷ったら都度相談 ○ Create → create ○ Read → get ○ Update → update ○ Delete → delete ○ リスト取得 → list

Slide 24

Slide 24 text

© ZOZO, Inc. 新しい方針の実装
 24

Slide 25

Slide 25 text

© ZOZO, Inc. 25 実際の実装 1/5 // usecase/shop/create/usecase.go type UseCase interface { Run(context.Context, *Input) (*Output, error) } type usecase struct { aRepo repository.AReopsitory } type Input struct { Name string } 一律UseCaseと命名 呼び出しメソッドはRunで統一 必要な依存のみ 一律Inputと命名

Slide 26

Slide 26 text

© ZOZO, Inc. 26 実際の実装(usecase)2/5 // usecase/shop/create/usecase.go type UseCase interface { Run(context.Context, *Input) (*Output, error) } ● usecaseからの分割で、shopのcreateのUseCaseだと分かる ○ UseCaseの命名で問題ない ● 1ユースケース1メソッドなので、Runで問題ない ○ 呼び出し側は「shop作成のユースケースを Runする」で扱いやすい ○ 「shop作成のユースケースを Createする」だと煩雑に見える

Slide 27

Slide 27 text

© ZOZO, Inc. 27 実際の実装(input命名)3/5 // usecase/shop/create/usecase.go type Input struct { Name string } ● ユースケースごとにパッケージが閉じているので、 Inputの命名で問題ない

Slide 28

Slide 28 text

© ZOZO, Inc. 28 実際の実装(関数命名)4/5 // usecase/shop/create/usecase.go func convertShopModelToOutput(m shopModel) Output { return Output{} } ● 関数もconvertShopModelToOutputとしても、他ユースケースと関数名が被ることはなくなるので、気に す ることが減った

Slide 29

Slide 29 text

© ZOZO, Inc. 29 実際の実装(controllerからの呼び出し)5/5 import ( createShopUseCase "hoge/usecase/shop/create" ) func (c *ShopController) Create(...) { shop, err := c.createShopUseCase.Run(ctx, createShopUseCase.Input{}) } ● 呼び出す際には、どの UseCaseの操作かを把握しやすいように package名にエイリアスをつける ● createShopUseCase.Input, createShopUseCase.Outputのようにアクセス可能 ○ パッケージが別れていることにより、 .Input, .Ouputで分かりやすい

Slide 30

Slide 30 text

© ZOZO, Inc. 30 package戦略の効果 ● 1つのユースケースを実行する独立した UseCaseモジュールとなり、凝集度が高くなり、責務や関心事が 一箇所にまとまった ● 命名がシンプルになった ○ ShopCreateInput → Input とだけ命名で済むようになった ○ 被らないように関数名をどうするか?に対して考える必要がほとんど無くなった ● 既存の実装の書き方と大きく変わらなかったため軽量に変更できるし、コスト対効果が大きかった

Slide 31

Slide 31 text

© ZOZO, Inc. 31 実際細かく分割を試してみて思ったこと pros ● pros: packageが分割されているので責務が分かり易くなった ● pros: usecaseに何気なく置いていた共通処理は、本当に usecaseで良かったのかと考える機会が増えた ○ 細かくpackage分割されたことにより、複数 usecaseで利用するような共通処理は、とりあえず usecaseに書いておくみたいなことが気軽にできなくなった。それにより、本当は usecaseではなく別 のところに書くべきだと気づきを得ることがある ● pros: packageを分割により、関数名など命名に迷いがなくなった ○ 前はShopUseCase, ShopCreateInputとかで、場合よってはもっと長い命名だったのでシンプル

Slide 32

Slide 32 text

© ZOZO, Inc. 32 実際細かく分割を試してみて思ったこと cons ● cons: エイリアスをつけてimportしないと分かりづらいので、それは面倒だった ○ 前はエイリアス付けず、usecase.ShopUseCaseのようにアクセスできていた ● cons: 命名は確かにシンプルになったが、そのためとはいえ分割しすぎたかも?っと感じた ○ usecase/shop/までで十分で、usecase/shop/get.goが正しい分割だったかもしれないと感じてきた ○ usecase/shop/まで切ったとしても多くても10ユースケースが含まれるかどうかなので、命名の衝突はほぼ無 さそうだし、shopUseCaseの関心事として十分わかりやすい構成だなと思った ○ usecaseが巨大になってしまったのは、正しく分割できていなかったことが原因であり、モジュラモノリスを採用 してshop, companyのような粒度で垂直分割してたら、それぞれが巨大になることはなさそうだし、綺麗に分割 できていたと思う ○ 今後モジュラモノリスを採用するとなった場合にも、 usecase/shop/までの分割だったらそのまま移行しやすそ うだなと感じた

Slide 33

Slide 33 text

© ZOZO, Inc. 33 今後について ● 前の実装からは、問題点も解決されて良くなって、開発し易くなった ● しかし、この分割の粒度が本当に正しかったかどうかは、自分の中でも答えは出ていないので、今一度検 討したいところではある

Slide 34

Slide 34 text

© ZOZO, Inc. DIツール wire の紹介
 34

Slide 35

Slide 35 text

© ZOZO, Inc. 35 DIツール wire ● FAANSでは、DIツールのwireを利用 ● 今回の修正でpackageが細かく分割されることにより、 DI関連のコードが冗長にならないようにしたい ● providerがどこに存在するかを把握しやすくするため、 packageごとにprovidorグループを作成することで 意識することを減らせる

Slide 36

Slide 36 text

© ZOZO, Inc. 36 DIツール wire ファイル構成 1/5 ● packageごとにproviderグループを作るために、それぞれ wire.goを持つようにしている └── usecase ├── shop │ ├── create │ │ ├── usecase.go │ │ └── wire.go │ ├── get │ │ ├── usecase.go │ │ └── wire.go │ └── wire.go └── wire.go

Slide 37

Slide 37 text

© ZOZO, Inc. 37 DIツール wire 2/5 // usecase/shop/create/wire.go var WireSet = wire.NewSet( NewUsecase, ) ● それぞれのpackageでproviderグループを作成

Slide 38

Slide 38 text

© ZOZO, Inc. 38 DIツール wire 3/5 // usecase/shop/wire.go import ( "github.com/google/wire" "hoge/usecase/shop/create" "hoge/usecase/shop/get" ) var WireSet = wire.NewSet( create.WireSet, get.WireSet, ) サブモジュールのWireSetをまとめる

Slide 39

Slide 39 text

© ZOZO, Inc. 39 DIツール wire 4/5 // usecase/wire.go import ( "github.com/google/wire" "hoge/usecase/company" "hoge/usecase/shop" ) var WireSet = wire.NewSet( company.WireSet, shop.WireSet, ) サブモジュールのWireSetをまとめる

Slide 40

Slide 40 text

© ZOZO, Inc. 40 DIツール wire 5/5 wire.Build( controller.NewShopController, usecase.WireSet, ) ● injector側で、usecase.WireSet指定することで、DIしてくれる ● これにより、packageが細かく分割されたとしても usecaseのWireSetだけ意識するだけで済むようになる

Slide 41

Slide 41 text

© ZOZO, Inc. まとめ
 41

Slide 42

Slide 42 text

© ZOZO, Inc. 42 まとめ ● UseCaseの凝集度を高め開発し易くするために、細かくパッケージ分割を行って問題点は解決された ● 試してみてよかったところもあれば、気になるところも出てきた ● それを踏まえて今後どういう構成が良いか考え、より開発しやすい構成を模索していく

Slide 43

Slide 43 text

No content