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

長期運用プロダクトの開発速度を維持し続けるためのリファクタリング実践例

Wataru
August 27, 2024

 長期運用プロダクトの開発速度を維持し続けるためのリファクタリング実践例

Wataru

August 27, 2024
Tweet

Other Decks in Programming

Transcript

  1. © LayerX Inc. 2 • バクラク事業部 請求書受取チーム • 2023年8⽉⼊社 •

    バックエンドの開発が中⼼ 靴、服 旅⾏ 祭り 過去の経歴 ネイティブゲームのクライ アント開発 ニュースアプリのiOS、広 告配信システムのバックエ ンド開発 趣味 wataru ⾃⼰紹介
  2. © LayerX Inc. 6 バクラクシリーズの全体像 バクラクは、企業取引の前段となる「稟議の統⼀」と「債権‧債務の⼀元管理」が可能。 従業員‧経理のそれぞれが係る業務領域において、なめらかな業務連携により企業経営を加速させます。 仕訳データ 振込データ ⼊⾦データ

    取引先 発注 請求 発注 請求 債権管理 債務管理 従業員 経理 ※ 開発予定の機能を含む 銀⾏ 会計ソフト 請求書 処理 経費 精算 振込 稟議 法⼈ カード 請求書 発⾏ 仕訳 (※) ⼊⾦消込 (※) 仕訳 © LayerX Inc.
  3. © LayerX Inc. 9 • initial commitは4年前 • 開発初期は速く出すことが重要、変化も多い •

    初期のコードもまだまだ現役 • 変化の代償として負債が残るのは当然 状況 仕様が複雑 • そもそもドメインが複雑、開発者に馴染みがない領域 • 仕訳や源泉税 • 振込データ • 様々な会計ソフトとの連携 • などなど 課題 請求書受取はバクラク最初期のプロダクト
  4. © LayerX Inc. 10 • ⼟台を⼤きくかえずに既存コードの上に実装してきた • 機能開発すると意図してない既存機能が壊れたりする ◦ 売上が⼀定あり、お客様も多くいる状況で壊すのは不味い

    • 問い合わせ、インシデント対応のコストが⾼い ◦ 1⽇潰れることも • 正しい仕様がだれもわからない箇所がある • この先も⼤きな機能開発がいくつか控えているが、変更箇所や影響範囲がすぐに分からなかったり ⼤きすぎたり… 課題 課題
  5. Presentation Tier Service Tier Repository Tier Repository S3 Repository Imp

    xo model REST Handler GraphQL Resolver Connect Usecase proto buf repositoryの実態はトランザクションスクリプト+DAOになっており、 操作ごとにメソッドを作成していたので請求書関連で100以上あった
  6. © LayerX Inc. 13 • 閲覧制限(β版として⼀部のお客様に限定公開している機能) ◦ ユーザーが閲覧できる書類を制限する機能 ◦ 既存のデータフェッチしている箇所ほぼすべてに影響がありそう

    ◦ repository層のメソッドが多すぎて、全部書くのがキツイ • 外貨請求書対応 ◦ 多通貨対応や、⼩数対応などで様々な箇所を触る必要がありそう ◦ 影響範囲が読めない • mysql 5.7 -> 8 ◦ 安全のためunit testは書いておきたい ◦ repository層のメソッドが多すぎて、全部書くのがキツイ • GORM V1->V2化 ◦ 同上 控えていた⼤きな開発 課題
  7. © LayerX Inc. 15 リファクタリングしたいけど • 正しい仕様がわからん ◦ そういう仕様なのか、たまたまそうなってるのか ◦

    ドメイン知識も要求されるしなんか壊れそうだから触りたくない 障壁 課題 • 事業優先度の問題 ◦ 機能開発でやりたいことがたくさんある ◦ リファクタリングのビジネス上の価値は算定しづらい
  8. © LayerX Inc. 18 リファクタリングしたいけど • 正しい仕様がわからん ◦ 💡PDMや関係者と相談して仕様整理から始める ◦

    💡なければ⾃分で仕様書を書く気概でやる 障壁 課題 • 事業優先度の問題 ◦ 機能開発でやりたいことがたくさんある、余裕がない ◦ リファクタリングのビジネス上の価値は算定しづらい
  9. © LayerX Inc. 19 リファクタリングしたいけど • 正しい仕様がわからん ◦ そういう仕様なのか、たまたまそうなってるのか ◦

    ドメイン知識も要求されるしなんか壊れそうだから触りたくない 障壁 課題 • 事業優先度の問題 ◦ 💡 機能開発の速度は落とさなければ問題ない ◦ 💡 機能開発のためのリファクタリング
  10. © LayerX Inc. 20 • フルリプレイスやそれくらいの規模のリファクタリングでは短期的な開発速度は落ちるしその実施 判断は難しいので今回のスコープ外 • 今回は開発期間として最低でも数週間~の状況、あまりにも短いと厳しいかも •

    リファクタリングの⽬的が明確にあると良い ◦ ステークホルダーからの理解が得られやすい ▪ やる場合、やらない場合のpros/consを⾔語化して伝える ◦ キレイにしたいからという⾃⼰満⾜にならない • 開発期間の前半にリファクタリングをしておけば、機能開発の効率は⼤幅に上がると判断 ◦ リファクタパート、開発パート合わせて当初の予定通り出せそう • 機能開発と同時にはリファクタリングしない ◦ リファクタリングだけした状態でtestやQAを通しておきたい • 直接今回の機能開発に関係ないことはやらない 機能開発とセット 課題
  11. © LayerX Inc. 23 • 閲覧制限(β版として⼀部のお客様に限定公開している機能) ◦ ユーザーが閲覧できる書類を制限する機能 ◦ 既存のデータフェッチしている箇所ほぼすべてに影響がありそう

    ◦ repository層のメソッドが多すぎて、全部書くのがキツイ • 外貨請求書対応 ◦ 多通貨対応や、⼩数対応などで様々な箇所を触る必要がありそう ◦ 影響範囲が読めない • mysql 5.7 -> 8 ◦ 安全のためunit testは書いておきたい ◦ repository層のメソッドが多すぎて、全部書くのがキツイ • GORM V1->V2化 ◦ 同上 控えていた⼤きな開発(再掲) 課題
  12. © LayerX Inc. 25 service層のI/O変更 • handler層から呼ばれるserviceのI/Oは変更しない • serviceレベルで、外から⾒た振る舞いに変更はない •

    service層のすでに存在するunit testが通れば安⼼ やらないこと やらないこと 既存テーブルの設計変更 • 影響が⼤きすぎる • 振る舞いを変えずに変更することが難しい
  13. © LayerX Inc. 26 機能開発に影響を与えない部分のリファクタ • 理想は全repository書き換えたいが、今回やりたい機能開発の開発速度が上がるわけではなく、⾃ ⼰満⾜になるかもしれない やらないこと やらないこと

    DDDの正しさを追い求めすぎない • DDDのエッセンスは取り⼊れるが、原理主義にならない • 正しいDDDを導⼊する⽬的でリファクタリングするわけではない
  14. © LayerX Inc. 27 repository層のリファクタ • 集約ルート単位でのやり取りに • usecaseごとに作られていたメソッドの削除 具体的には

    やったこと domain層の導⼊ • 集約に対するビジネスロジックをまとめる • エンティティ、domain serviceの作成
  15. © LayerX Inc. 28 具体的には やったこと repository層のリファクタ • 集約ルート単位でのやり取りに •

    usecaseごとに作られていたメソッドの削除 domain層の導⼊ • 集約に対するビジネスロジックをまとめる • エンティティ、domain serviceの作成
  16. © LayerX Inc. 29 repositoryの設計思想 • 集約内部の変更は必ず集約ルートを経由することで集約内を常に整合性が確保された状態にする • 集約ルートの単位でデータの取得・永続化を行う •

    集約ルート:repositoryは1:1 ◦ テーブル単位ではない • 集約をまたいだ検索が必要な場合、 query serviceで書く • ビジネスロジックを持たない ◦ 指示(指定された引数)に従って CRUDするだけ • repository同士で依存しない repository層のリファクタ
  17. © LayerX Inc. 30 repository層のリファクタ • 既存のモデルをすべて書き出し整理した • 良い集約の範囲を決めるのは難しい •

    やってみて違和感がないか確認したり、試⾏錯誤が必要 • ドメインエキスパートに相談してもいいかも 集約を定義 請求書 (ルート) 請求書ファイル 請求書タグ
  18. © LayerX Inc. 31 repository層のリファクタ • 集約ルートのエンティティをdomainパッケージに作成した • 集約ルート以外は既存の⾃動⽣成されたmodelを使⽤ 集約を定義

    package domain type Invoice struct { *model.Invoice Files []*model.InvoiceFile Tags []*model.InvoiceTag } package model type InvoiceEmbedded struct { Invoice Journals []*Journal Client *Client Files []*Files … }
  19. © LayerX Inc. 33 repository層のリファクタ • 旧repositoryの⼀部、⼤量のメソッドがある • 微妙に違うusecaseに対して違うメソッドが存在し、I/Oもバラバラ •

    ⼤きな変更の際など、全て変更するのも、全てtestを書くのもつらい repositoryの再定義 package domain type InvoiceRepository interface { GetByID(ctx Context, id string) (*model.Invoice, error) GetFileByID(ctx Context, id string) (*model.InvoiceFile, error) UpdateForFooUseCase(ctx Context, value string) error } // 集約の一部を操作するようなrepoは削除 type InvoiceHogeRepository interface { UpdateStatus(ctx Context, status string) error }
  20. © LayerX Inc. 34 repository層のリファクタ • 集約ルート単位でデータのやり取りをする • 基本的にはGet,GetMany,Saveのみ提供(例外はあるが) repositoryの再定義

    package domain type InvoiceRepository interface { Get(ctx Context, id string) (*Invoice, error) GetMany(ctx Context, ids ...string) (*Invoice, error) Save(ctx Context, id string) error }
  21. © LayerX Inc. 35 repository層のリファクタ • 集約外のテーブルを使⽤したい場合 • 複雑な条件の検索が必要な場合別途query serviceを作る

    集約をまたぐ場合 package query type InvoiceQueryService interface { Find(ctx Context, params InvocieFindParams) (domain.Invoices, error) } // 検索条件 type InvocieFindParams { name *string status *model.InvoiceStatus }
  22. © LayerX Inc. 36 具体的には やったこと repository層のリファクタ • 集約ルート単位でのやり取りに •

    usecaseごとに作られていたメソッドの削除 domain層の導⼊ • 集約に対するビジネスロジックをまとめる • エンティティ、domain serviceの作成
  23. © LayerX Inc. 38 エンティティ domain層の導⼊ package serivce func (s

    Invoice) UpdateStatus(ctx context.Cotext, id string, status model.InvocieStatus) error { invoice := s.repo.GetByID(ctx, id) // statusを直接書き換える invoice.Status = status // 集約ルートを経由しないで書き換える invoice.Files[0].Status = hoge // 専用のメソッド return s.repo.UpdateStatus(id, status) } • 古い実装 (極端な例)
  24. © LayerX Inc. 39 エンティティ domain層の導⼊ package serivce func (s

    Invoice) UpdateStatus(ctx context.Cotext, id string, status model.InvocieStatus) error { invoice := s.repo.Get(ctx, id) invoice.UpdateStatus(ctx, status) // 必要ならvalidationとか return s.repo.Save(ctx, invoice) } • リファクタリング後の実装
  25. © LayerX Inc. 40 • 複数のエンティティにまたがる場合や⾃然に表現できない場合 • 多⽤はしない、どうしても必要なときのみ ◦ ドメインモデル貧⾎症にならないように

    • 例えば共通の採番ロジックなど、各エンティティに直接持たせるのが不⾃然な場合 domain service domain層の導⼊
  26. © LayerX Inc. 41 • 採番ロジックの例、実際は採番テーブルを使⽤しており、interfaceがdomain層にある • 他にも、重複チェックなどが考えられる(エンティティ⾃⾝が⾃分が重複しているか知らないから) domain service

    domain層の導⼊ package domain func (s InvoiceService) CreateInvoice(ctx context.Cotext, …) (*domain.Invoice error) { invoice := NewInvoice(...) num = s.numberGenerator.Generate() // なんか処理 return invoice }
  27. © LayerX Inc. 43 Before(再掲) domain層の導⼊ Presentation Tier Service Tier

    Repository Tier Repository S3 Repository Imp xo model REST Handler GraphQL Resolver Connect Usecase proto buf
  28. © LayerX Inc. 44 domain層の導⼊ Presentation Tier Usecase Tier Domain

    Tier Infra Tier Entity Repository Domain Service Repository Imp xo model REST Handler GraphQL Resolver Connect Usecase proto buf S3 usecase model
  29. © LayerX Inc. 46 • スコープを絞り、機能開発とセットでリファクタリングをすることで開発速度を落とさずにリファ クタリングできた ◦ 👍その後の開発では恩恵だけただで受けれる •

    DDDを⼀部取り⼊れ集約を定義し、⼤量にあったrepositoryのメソッドを整理した ◦ 👍その後のrepositoryに対する変更が容易に ◦ 👍 unit testも楽 • サービス層かかれていたビジネスロジックの移植 ◦ 👍 集約ルートのエンティティ経由でしか内部状態が変更されないことが保証されるため、変 更すべき箇所が明確に。不具合対応も楽に まとめ まとめ