Findyの爆速開発を支えるPull requestの粒度

こんにちは。

Findy で Tech Lead をやらせてもらってる戸田です。

既に皆さんも御存知かと思いますが、弊社では開発生産性の向上に対して非常に力を入れています。

以前公開した↓の記事で、弊社の高い開発生産性を支えている取り組み、技術についてお話させていただきました。

tech.findy.co.jp

ありがたいことに、この記事を多くの方に読んでいただき反響をいただいております。

そこで今回は、↑の記事でも紹介されている「Pull requestの粒度」について更に深堀りしてお話しようと思います。

Pull requestの粒度は、弊社にJOINしたら最初に必ず覚えてもらう最重要テクニックの1つです。

それでは見ていきましょう!

大きなPull request

Pull requestのレビュー依頼が飛んできて確認したら、大きなPull requestでブラウザをそっと閉じた経験がある方は少なくないと思います。

大きなPull requestにおけるデメリットは数多く存在します。

パッと思いつくデメリットを列挙してみます。心当たりがある読者の方もいるのではないでしょうか?

  • topic branchの生存期間が長くなり、結果的にbase branchとの差分が大きくなりconflictの発生確率が高くなる
  • 変更内容が多岐に渡るため、問題発生時の原因特定に時間が掛かってしまう
  • revert時に余計な内容までrevertされてしまう
  • レビューの質が落ちる
    • レビュワーが見るべき範囲が広くなるため、認知負荷が大きくなる
      • あとで見とこ、、、となりがち
        • 結果的にtopic branchの生存期間が長くなる
        • base branchとの差分が大きくなる
        • conflictの発生確率が高くなる
    • 指摘内容が増え、結果的に手戻りが増える
  • etc

このようにデメリットは数多く存在します。システム開発において大きなPull requestの存在は、円滑な開発を阻害する要因となるでしょう。

では「大きなPull request」とは具体的にどんなPull requestのことを指しているのでしょうか?

適切な粒度とは

ここでサイズと粒度の違いを説明する必要が出てきます。

そのPull requestが1つのことに注力できているかどうかが、粒度を語る上で非常に重要なポイントになります。

Pull requestのサイズとは、コードの変更行数や変更ファイル数のことを指します。

しかし、コードの行数や変更ファイル数が多かったとしても、変更内容が一意であれば問題ないはずです。

例えば、関数名を変更して一括置換した場合を例に挙げましょう。変更ファイル数は多くなりますが、変更内容は一意です。

こういったケースの場合、Pull requestの概要欄に一括置換した旨を記載しておけば、変更内容を全て確認せずとも変更後の関数名をレビューし、CIが通ればマージ出来るはずです。

一方、Pull requestの粒度とは、コードの変更内容のことを指します。

例えば、変更ファイル数が少ない一方、データ取得・データ加工・描画処理を同じPull requestで対応した場合を例に挙げて考えてみましょう。

この場合、それぞれの処理は全く異なる内容です。しかし同じPull request内で対応してしまったため、変更内容全てを一度にレビューする必要があり、レビュワーに対する認知負荷が大きくなってしまいます。

極論ですが、たとえ変更行数が1万行を超えていたとしても、変更内容が一意であれば問題は無いと考えています。

逆に変更行数が20行程度の不具合修正の中に「ついでにリファクタ」した内容が含まれていた場合、粒度が大きいと判断しPull requestを分割すべきなのです。

なぜならば、もし不具合修正に失敗していてrevertしようとした際に、ついでにリファクタした内容もrevertされてしまうからです。こういったケースの場合、リファクタをしたPull requestを別で作成します。

つまり本質はコードの変更行数や変更ファイル数ではなく、変更内容そのものにあるということを理解できたかと思います。

粒度が適切であれば、1行だけの修正でも、1万行の修正でも問題ありません。

10のプルリクを1回レビューするよりも、1のプルリクを10回レビューする方が、作成者、レビュワー共に負担が少ないのです。

適切な粒度を維持するために

タスク分解

Pull requestの粒度について完全に理解したので、これからは適切な粒度でPull requestを作るぞ!と思い立っても、すぐに実現させるのは難しいものです。

理解はしているけど、やってみるとやることがどんどん増えていき、結果的にPull requestが肥大化しがちです。

それを解決するのがタスク分解です。概要は↓のページを参照してください。

tech.findy.co.jp

タスク分解に関しても、別の機会で詳細にお話できればと思います。

迷ったら小さく

とは言え、最初のうちは粒度に対して悩むことが出てくると思います。

もっと作り込んでからレビュー依頼を出す?それとも今の段階で出す?でも修正内容が小さすぎないか?などといった葛藤は自分にも経験があります。

そういう時は、より小さい粒度の段階でレビュー依頼を出すことをオススメします。レビューのやり取りの中で粒度に対する議論を行い、そこで認識を合わせればOKです。

Pull requestを大きく作って後から分解するより、小さく作って後から修正内容を追加する方が圧倒的に楽だからです。

まずは小さすぎても良いので小さく作り、そこから肉付けしていくようなイメージでPull requestを作成していくと良いでしょう。

レビューを最優先にする

Pull requestの粒度が小さくなった場合、レビュー依頼に対して最優先で取り組む必要があります。

なぜならば、マージしないと次の修正に着手出来ない場合に、レビューがボトルネックになり結果的に開発スピードが遅くなってしまうからです。

自分の作業を一時中断して、10分程度レビューして自分の作業に戻るとコンテキストスイッチを戻すのが非常に難しいと思います。

でも心配しなくてよいです。Pull requestの粒度が適切であれば、レビュワーが確認する内容も小さくなるためレビューに掛かる時間が短縮されます。

そのため、自分の作業を一時中断することになったとしても、すぐまた自分の作業に戻ることができるようになります。

弊社では1つのPull requestのレビューに掛ける時間はほんの数分程度となっています。1分以内で終わることもあります。

粒度が適切なPull requestが当たり前となれば、レビューを最優先にする習慣、文化が組織に根付くはずです。

CI高速化

粒度が小さくなってくると、当然ながら作成されるPull requestの数が増えます。つまりCIの実行回数も比例して増えていきます。

CIの実行回数が増えた状態で実行速度が遅いと、逆に開発効率が落ちてしまいます。CIが遅いからPull requestの粒度も大きくなってしまうといったケースも見たことがあります。こうなってしまうと本末転倒です。

そこでCIの高速化も同時に進めた方が良いでしょう。詳細はこの記事では割愛しますが、↓の別記事を参考にしてみてください。

tech.findy.co.jp

feature flag運用

Pull requestの粒度は適切にしたいが、本番環境には影響を出したくない。というパターンはfeature flagを使うと良いでしょう。

ローカル環境でのみ実行されるようなコードにしておいて、その状態を維持しつつbase branchにマージし続けます。本番公開OKのタイミングでfeature flagを解除し、本番環境に反映します。

feature flagの運用方法はSaaSを使う、ライブラリを使うなどの方法がありますが、今回は一番カンタンに導入できる方法の、コード上にフラグを埋め込んで切り替える手法を紹介します。

実際にコードの例を見てみましょう。

export const SampleComponent = () => {
  const isEnabledNewLabel = process.env.FEATURE_NEW_LABEL === 'true';

  return (
    <div>
      {isEnabledNewLabel && <span>NEW</span>}
      <span>Sample</span>
    </div>
  );
};

環境変数に埋め込まれている FEATURE_NEW_LABEL の値によって、 NEW の表示を切り替える単純なcomponentです。

この仕組みにより、ローカル環境の環境変数をtrue、本番環境の環境変数をfalseにすることで、本番環境に影響を与えずに新機能の開発を進めることが可能になります。

開発が完了して本番環境に反映する際には、本番環境の環境変数をtrueにすることで新機能を公開できます。何かしらの問題が発生した場合、本番環境の環境変数をfalseに戻すことで、新機能を非公開に変更可能です。

本番環境で問題が起きなければ、環境変数のフラグとコード上の分岐を削除します。

この手法はfeature flagの管理が煩雑になったり、コード内の分岐やテストケースが増えるといったデメリットもありますが、Pull requestの粒度を適切に維持するためには非常に有効な手段です。

本番環境に影響を出さずに、Pull requestの粒度を適切に維持し続け、スピード感を持って開発する手段の1つとして活用してみてください。

topic branch運用

様々な事情によってfeature flagを使えない、使いたくない場合の手段として、topic branch運用を紹介します。

topic branch運用

base branchからtopic branchを切って、そこから更にdevelop branchを切ります。develop branchからtopic branchへのPull requestを作成し、そこで粒度を維持しつつレビューをします。

本番反映OKのタイミングでtopic branchからbase branchへのPull requestを作成し、マージします。このタイミングでのレビューは必要最低限でOKです。なぜなら、develop branchからtopic branchへのPull requestでレビューしているからです。

この手法により、base branchにはリリースのタイミングまで変更内容が反映されません。そのため本番環境への影響を与えず、Pull requestの粒度を適切に維持し続けることが可能になります。

定期的にbase branchからtopic branchへ変更をmergeしておくのがポイントです。topic branchの生存期間が長くなるので、base branchとの差分が大きくなりconflictが発生しやすくなるからです。最低でも1日1回はbase branchの修正内容を取り込んでおくと良いでしょう。

まとめ

いかがでしたでしょうか?

粒度を適切に維持することで、レビュワー、レビュイーの両者に対して優しいPull requestを作成し続けることができます。

弊社では最重要テクニックの1つとしていますが、比較的カンタンに覚えることができ、コツさえ掴めば誰でも実践できる内容です。

是非、皆さんも試してみてください。

現在、ファインディでは一緒に働くメンバーを募集中です。

興味がある方はこちらから ↓ herp.careers