$30 off During Our Annual Pro Sale. View Details »

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. UseCaseの凝集度を高める

    Goのpackage戦略

    2023年09月20日

    株式会社ZOZO

    ブランドソリューション開発本部 バックエンド部 FAANSバックエンドブロック

    田村 誠基
    Copyright © ZOZO, Inc.
    1

    View Slide

  2. © ZOZO, Inc.
    株式会社ZOZO

    ブランドソリューション開発本部 バックエンド部 FAANSバックエンドブ
    ロック


    田村 誠基

    2020年4月 入社

    自社ECの運用保守業務に従事し、その後FAANSのバックエンド
    エンジニアとしてAPI開発をしています。

    好きなエディタは、Neovimです!

    最近、M2 ProのMacBookに変え、PCが爆速になった。


    2

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  6. © ZOZO, Inc.
    UseCaseの変更前実装

    〜 ディレクトリ構成 〜

    6

    View Slide

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

    View Slide

  8. © ZOZO, Inc.
    UseCaseの変更前実装①

    〜 1UseCaseに複数メソッドあることで、依存関係が増えている 〜

    8

    View Slide

  9. © 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はメソッドごとに定義している

    View Slide

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

    View Slide

  11. © 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
    }
    依存が増える場合がある
    (既存の改修により増える場合も)
    新規メソッドが追加

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  15. © ZOZO, Inc.
    UseCaseの変更前実装②

    15
    〜 似たような命名のstructがあり、見通しが悪い 〜

    View Slide

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

    View Slide

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

    View Slide

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

    18

    View Slide

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

    View Slide

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



    20

    View Slide

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

    View Slide

  22. © 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

    View Slide

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

    View Slide

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

    24

    View Slide

  25. © 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と命名

    View Slide

  26. © 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する」だと煩雑に見える

    View Slide

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

    View Slide

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

    View Slide

  29. © 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で分かりやすい

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    34

    View Slide

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

    View Slide

  36. © 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

    View Slide

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

    View Slide

  38. © 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をまとめる

    View Slide

  39. © 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をまとめる

    View Slide

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

    View Slide

  41. © ZOZO, Inc.
    まとめ

    41

    View Slide

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

    View Slide

  43. View Slide