Upgrade to Pro — share decks privately, control downloads, hide ads and more …

UseCaseの凝集度を高める Goのpackage戦略 / Go packaging strategy to increase use case cohesion

Tomoki Tamura
September 20, 2023

UseCaseの凝集度を高める Goのpackage戦略 / Go packaging strategy to increase use case cohesion

UseCaseの凝集度を高める Goのpackage戦略 / Go packaging strategy to increase use case cohesion

Tomoki Tamura

September 20, 2023
Tweet

Other Decks in Programming

Transcript

  1. © ZOZO, Inc. 株式会社ZOZO
 ブランドソリューション開発本部 バックエンド部 FAANSバックエンドブ ロック
 
 田村

    誠基
 2020年4月 入社
 自社ECの運用保守業務に従事し、その後FAANSのバックエンド エンジニアとしてAPI開発をしています。 
 好きなエディタは、Neovimです! 
 最近、M2 ProのMacBookに変え、PCが爆速になった。 
 
 2
  2. © ZOZO, Inc. 4 ショップスタッフの販売サポートツール「FAANS」とは • WEAR, ZOZOTOWN, Yahoo!ショッピング, ブランド様の自社ECへのコーディネート投稿や成果の確認

    など、ショップスタッフの運用に必要な機能を搭載した業務支援ツール • お客様がZOZOTOWN上でブランド実店舗の在庫取り置きを希望した際に、ショップスタッフが FAANS上 での簡単操作で取り置き対応を完結できる機能など存在 • FAANSバックエンドでは、APIの実装をGo言語で開発
  3. © ZOZO, Inc. 7 UseCaseディレクトリ構成 • usecaseディレクトリ直下にファイルを置いている ◦ 例: usecase/shop.go

    • usecaseディレクトリには90ファイル近く存在していて巨大 └── usecase ├── aa.go ├── bb.go ├── cc.go └── shop.go
  4. © 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はメソッドごとに定義している
  5. © ZOZO, Inc. 10 1UseCaseに複数メソッド持っている 1/2 • この実装でのメリットは、ユースケースを追加する場合は、メソッドを生やすだけで可能とい うことで、サクッと実装することができる •

    短期的に見るとそれで良いかもしれないが、中長期的に見ると依存関係が分かりづらくなって いきメンテナンス性が悪くなる
  6. © 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 } 依存が増える場合がある (既存の改修により増える場合も) 新規メソッドが追加
  7. © ZOZO, Inc. 12 1UseCaseに複数メソッドあることで、依存関係が増加 2/2 • UseCaseに1メソッド追加されるごとに、依存関係が増える可能性がある ◦ ユースケースが大きくなればなるほど、最大公約数的に増えてしまう

    • それにより、テストコードを書く際にモックすべきクラスがどれか分からなくなってくる ◦ テストコード書くのが大変になり面倒になる
  8. © ZOZO, Inc. 13 試しに改善: 1UseCase1メソッドにする type ShopGetUseCase interface {

    Get(context.Context, *ShopGetInput) (*ShopOutput, error) } type shopGetUseCase struct { aRepo repository.AReopsitory } • 無駄な依存が一切なくなり、最低限な依存のみとなり、変更による影響が最小化される • テスト書く際には、モックすべきレポジトリが一目瞭然となり書きやすい
  9. © ZOZO, Inc. 16 似たような命名のstructがあり、見通しが悪い 1/2 type ShopUpdateInput struct {

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

    { return ShopOutput{} } • 他にもDTO変換処理をusecaseパッケージ内に記載しているので、被らないようにするために名前が煩 雑になっている
  11. © 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
  12. © ZOZO, Inc. 23 パッケージの命名について • パッケージの切り方の命名 • 基本的には、usecase/{ドメインモデル名}/{操作名}/usecase.goのルール •

    迷ったら都度相談 ◦ Create → create ◦ Read → get ◦ Update → update ◦ Delete → delete ◦ リスト取得 → list
  13. © 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と命名
  14. © 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する」だと煩雑に見える
  15. © ZOZO, Inc. 27 実際の実装(input命名)3/5 // usecase/shop/create/usecase.go type Input struct

    { Name string } • ユースケースごとにパッケージが閉じているので、 Inputの命名で問題ない
  16. © ZOZO, Inc. 28 実際の実装(関数命名)4/5 // usecase/shop/create/usecase.go func convertShopModelToOutput(m shopModel)

    Output { return Output{} } • 関数もconvertShopModelToOutputとしても、他ユースケースと関数名が被ることはなくなるので、気に す ることが減った
  17. © 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で分かりやすい
  18. © ZOZO, Inc. 30 package戦略の効果 • 1つのユースケースを実行する独立した UseCaseモジュールとなり、凝集度が高くなり、責務や関心事が 一箇所にまとまった •

    命名がシンプルになった ◦ ShopCreateInput → Input とだけ命名で済むようになった ◦ 被らないように関数名をどうするか?に対して考える必要がほとんど無くなった • 既存の実装の書き方と大きく変わらなかったため軽量に変更できるし、コスト対効果が大きかった
  19. © ZOZO, Inc. 31 実際細かく分割を試してみて思ったこと pros • pros: packageが分割されているので責務が分かり易くなった •

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

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

    DI関連のコードが冗長にならないようにしたい • providerがどこに存在するかを把握しやすくするため、 packageごとにprovidorグループを作成することで 意識することを減らせる
  22. © 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
  23. © ZOZO, Inc. 37 DIツール wire 2/5 // usecase/shop/create/wire.go var

    WireSet = wire.NewSet( NewUsecase, ) • それぞれのpackageでproviderグループを作成
  24. © 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をまとめる
  25. © 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をまとめる
  26. © ZOZO, Inc. 40 DIツール wire 5/5 wire.Build( controller.NewShopController, usecase.WireSet,

    ) • injector側で、usecase.WireSet指定することで、DIしてくれる • これにより、packageが細かく分割されたとしても usecaseのWireSetだけ意識するだけで済むようになる