Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
UseCaseの凝集度を高める Goのpackage戦略 / Go packaging str...
Search
Tomoki Tamura
September 20, 2023
Programming
3
2.5k
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
Share
Other Decks in Programming
See All in Programming
PHPで作るWebSocketサーバー ~リアクティブなアプリケーションを知るために~ / WebSocket Server in PHP - To know reactive applications
seike460
PRO
2
660
PHPUnitしか使ってこなかった 一般PHPerがPestに乗り換えた実録
mashirou1234
0
340
testcontainers のススメ
sgash708
1
130
【re:Growth 2024】 Aurora DSQL をちゃんと話します!
maroon1st
0
810
暇に任せてProxmoxコンソール 作ってみました
karugamo
2
730
各クラウドサービスにおける.NETの対応と見解
ymd65536
0
180
EC2からECSへ 念願のコンテナ移行と巨大レガシーPHPアプリケーションの再構築
sumiyae
2
450
SymfonyCon Vienna 2025: Twig, still relevant in 2025?
fabpot
3
1.2k
非ブラウザランタイムとWeb標準 / Non-Browser Runtimes and Web Standards
petamoriken
0
140
useSyncExternalStoreを使いまくる
ssssota
6
1.4k
どうして手を動かすよりもチーム内のコードレビューを優先するべきなのか
okashoi
3
610
コンテナをたくさん詰め込んだシステムとランタイムの変化
makihiro
1
150
Featured
See All Featured
Code Reviewing Like a Champion
maltzj
521
39k
ピンチをチャンスに:未来をつくるプロダクトロードマップ #pmconf2020
aki_iinuma
111
49k
Dealing with People You Can't Stand - Big Design 2015
cassininazir
365
25k
Stop Working from a Prison Cell
hatefulcrawdad
267
20k
Typedesign – Prime Four
hannesfritz
40
2.4k
A Modern Web Designer's Workflow
chriscoyier
693
190k
Making the Leap to Tech Lead
cromwellryan
133
9k
Easily Structure & Communicate Ideas using Wireframe
afnizarnur
191
16k
Evolution of real-time – Irina Nazarova, EuRuKo, 2024
irinanazarova
6
450
ReactJS: Keep Simple. Everything can be a component!
pedronauck
666
120k
Fight the Zombie Pattern Library - RWD Summit 2016
marcelosomers
232
17k
Mobile First: as difficult as doing things right
swwweet
222
9k
Transcript
UseCaseの凝集度を高める Goのpackage戦略 2023年09月20日 株式会社ZOZO ブランドソリューション開発本部 バックエンド部 FAANSバックエンドブロック 田村 誠基 Copyright ©
ZOZO, Inc. 1
© ZOZO, Inc. 株式会社ZOZO ブランドソリューション開発本部 バックエンド部 FAANSバックエンドブ ロック 田村
誠基 2020年4月 入社 自社ECの運用保守業務に従事し、その後FAANSのバックエンド エンジニアとしてAPI開発をしています。 好きなエディタは、Neovimです! 最近、M2 ProのMacBookに変え、PCが爆速になった。 2
© ZOZO, Inc. 3 Agenda • ショップスタッフの販売サポートツール「 FAANS」とは • UseCaseの変更前実装
• 解決のアプローチ • DIツール wire の紹介 • まとめ
© ZOZO, Inc. 4 ショップスタッフの販売サポートツール「FAANS」とは • WEAR, ZOZOTOWN, Yahoo!ショッピング, ブランド様の自社ECへのコーディネート投稿や成果の確認
など、ショップスタッフの運用に必要な機能を搭載した業務支援ツール • お客様がZOZOTOWN上でブランド実店舗の在庫取り置きを希望した際に、ショップスタッフが FAANS上 での簡単操作で取り置き対応を完結できる機能など存在 • FAANSバックエンドでは、APIの実装をGo言語で開発
© ZOZO, Inc. 5 クリーンアーキテクチャを採用している • 今回は、UseCase部分の実装のお話 「Cloud FirestoreからPostgreSQLへ移行したお話」
© ZOZO, Inc. UseCaseの変更前実装 〜 ディレクトリ構成 〜 6
© ZOZO, Inc. 7 UseCaseディレクトリ構成 • usecaseディレクトリ直下にファイルを置いている ◦ 例: usecase/shop.go
• usecaseディレクトリには90ファイル近く存在していて巨大 └── usecase ├── aa.go ├── bb.go ├── cc.go └── shop.go
© ZOZO, Inc. UseCaseの変更前実装① 〜 1UseCaseに複数メソッドあることで、依存関係が増えている 〜 8
© 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はメソッドごとに定義している
© ZOZO, Inc. 10 1UseCaseに複数メソッド持っている 1/2 • この実装でのメリットは、ユースケースを追加する場合は、メソッドを生やすだけで可能とい うことで、サクッと実装することができる •
短期的に見るとそれで良いかもしれないが、中長期的に見ると依存関係が分かりづらくなって いきメンテナンス性が悪くなる
© 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 } 依存が増える場合がある (既存の改修により増える場合も) 新規メソッドが追加
© ZOZO, Inc. 12 1UseCaseに複数メソッドあることで、依存関係が増加 2/2 • UseCaseに1メソッド追加されるごとに、依存関係が増える可能性がある ◦ ユースケースが大きくなればなるほど、最大公約数的に増えてしまう
• それにより、テストコードを書く際にモックすべきクラスがどれか分からなくなってくる ◦ テストコード書くのが大変になり面倒になる
© ZOZO, Inc. 13 試しに改善: 1UseCase1メソッドにする type ShopGetUseCase interface {
Get(context.Context, *ShopGetInput) (*ShopOutput, error) } type shopGetUseCase struct { aRepo repository.AReopsitory } • 無駄な依存が一切なくなり、最低限な依存のみとなり、変更による影響が最小化される • テスト書く際には、モックすべきレポジトリが一目瞭然となり書きやすい
© ZOZO, Inc. 14 「1UseCase複数メソッド」と「1UseCase1メソッド」 • 依存が最小限となっており、 1UseCase1メソッドの方が分かりやすい
© ZOZO, Inc. UseCaseの変更前実装② 15 〜 似たような命名のstructがあり、見通しが悪い 〜
© ZOZO, Inc. 16 似たような命名のstructがあり、見通しが悪い 1/2 type ShopUpdateInput struct {
Name string } type ShopDeleteInput struct { Name string } ... • ControllerからUseCaseを呼び出す際に利用する、 DTOのInputをUseCase(メソッドごと)に作成してい るので、どんどん見通しが悪くなるし、命名が被ることも考慮する必要がある
© ZOZO, Inc. 17 似たような命名のstructがあり、見通しが悪い 2/2 func convertShopModelToShopOutput(m shopModel) ShopOutput
{ return ShopOutput{} } • 他にもDTO変換処理をusecaseパッケージ内に記載しているので、被らないようにするために名前が煩 雑になっている
© ZOZO, Inc. これらの実装を整理すると 18
© ZOZO, Inc. 19 凝集度が低いと感じる • 1ユースケースに複数メソッドあることで、依存関係が増えて、変更に弱いユースケースになっている • UseCaseというパッケージが大きくて修正しづらい場面がある ◦
DTOのInputや変換処理などの命名が被らないように気を付ける必要がある
© ZOZO, Inc. 解決のアプローチ 20
© ZOZO, Inc. 21 解決の方針 • 1UseCase1メソッドにして依存関係を最小限にしたい • Inputや関数などの見通しの悪さや命名の煩わしさから解放されたい •
開発案件も進める必要もあり、現状の実装を大きく変えずに、軽量に実施できる方法にしたい
© 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
© ZOZO, Inc. 23 パッケージの命名について • パッケージの切り方の命名 • 基本的には、usecase/{ドメインモデル名}/{操作名}/usecase.goのルール •
迷ったら都度相談 ◦ Create → create ◦ Read → get ◦ Update → update ◦ Delete → delete ◦ リスト取得 → list
© ZOZO, Inc. 新しい方針の実装 24
© 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と命名
© 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する」だと煩雑に見える
© ZOZO, Inc. 27 実際の実装(input命名)3/5 // usecase/shop/create/usecase.go type Input struct
{ Name string } • ユースケースごとにパッケージが閉じているので、 Inputの命名で問題ない
© ZOZO, Inc. 28 実際の実装(関数命名)4/5 // usecase/shop/create/usecase.go func convertShopModelToOutput(m shopModel)
Output { return Output{} } • 関数もconvertShopModelToOutputとしても、他ユースケースと関数名が被ることはなくなるので、気に す ることが減った
© 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で分かりやすい
© ZOZO, Inc. 30 package戦略の効果 • 1つのユースケースを実行する独立した UseCaseモジュールとなり、凝集度が高くなり、責務や関心事が 一箇所にまとまった •
命名がシンプルになった ◦ ShopCreateInput → Input とだけ命名で済むようになった ◦ 被らないように関数名をどうするか?に対して考える必要がほとんど無くなった • 既存の実装の書き方と大きく変わらなかったため軽量に変更できるし、コスト対効果が大きかった
© ZOZO, Inc. 31 実際細かく分割を試してみて思ったこと pros • pros: packageが分割されているので責務が分かり易くなった •
pros: usecaseに何気なく置いていた共通処理は、本当に usecaseで良かったのかと考える機会が増えた ◦ 細かくpackage分割されたことにより、複数 usecaseで利用するような共通処理は、とりあえず usecaseに書いておくみたいなことが気軽にできなくなった。それにより、本当は usecaseではなく別 のところに書くべきだと気づきを得ることがある • pros: packageを分割により、関数名など命名に迷いがなくなった ◦ 前はShopUseCase, ShopCreateInputとかで、場合よってはもっと長い命名だったのでシンプル
© ZOZO, Inc. 32 実際細かく分割を試してみて思ったこと cons • cons: エイリアスをつけてimportしないと分かりづらいので、それは面倒だった ◦
前はエイリアス付けず、usecase.ShopUseCaseのようにアクセスできていた • cons: 命名は確かにシンプルになったが、そのためとはいえ分割しすぎたかも?っと感じた ◦ usecase/shop/までで十分で、usecase/shop/get.goが正しい分割だったかもしれないと感じてきた ◦ usecase/shop/まで切ったとしても多くても10ユースケースが含まれるかどうかなので、命名の衝突はほぼ無 さそうだし、shopUseCaseの関心事として十分わかりやすい構成だなと思った ◦ usecaseが巨大になってしまったのは、正しく分割できていなかったことが原因であり、モジュラモノリスを採用 してshop, companyのような粒度で垂直分割してたら、それぞれが巨大になることはなさそうだし、綺麗に分割 できていたと思う ◦ 今後モジュラモノリスを採用するとなった場合にも、 usecase/shop/までの分割だったらそのまま移行しやすそ うだなと感じた
© ZOZO, Inc. 33 今後について • 前の実装からは、問題点も解決されて良くなって、開発し易くなった • しかし、この分割の粒度が本当に正しかったかどうかは、自分の中でも答えは出ていないので、今一度検 討したいところではある
© ZOZO, Inc. DIツール wire の紹介 34
© ZOZO, Inc. 35 DIツール wire • FAANSでは、DIツールのwireを利用 • 今回の修正でpackageが細かく分割されることにより、
DI関連のコードが冗長にならないようにしたい • providerがどこに存在するかを把握しやすくするため、 packageごとにprovidorグループを作成することで 意識することを減らせる
© 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
© ZOZO, Inc. 37 DIツール wire 2/5 // usecase/shop/create/wire.go var
WireSet = wire.NewSet( NewUsecase, ) • それぞれのpackageでproviderグループを作成
© 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をまとめる
© 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をまとめる
© ZOZO, Inc. 40 DIツール wire 5/5 wire.Build( controller.NewShopController, usecase.WireSet,
) • injector側で、usecase.WireSet指定することで、DIしてくれる • これにより、packageが細かく分割されたとしても usecaseのWireSetだけ意識するだけで済むようになる
© ZOZO, Inc. まとめ 41
© ZOZO, Inc. 42 まとめ • UseCaseの凝集度を高め開発し易くするために、細かくパッケージ分割を行って問題点は解決された • 試してみてよかったところもあれば、気になるところも出てきた •
それを踏まえて今後どういう構成が良いか考え、より開発しやすい構成を模索していく
None