「新機能作成時に開発ブランチに細かくmergeしていく戦略」について社内勉強会で発表しました

はてなのアプリケーションエンジニアのid:shiba_yu36です。社内技術勉強会で「新機能作成時に開発ブランチに細かくmergeしていく戦略」という発表をしたので、資料を公開します。

speakerdeck.com


以下、簡単に文字でまとめておきます。

戦略

  • ユーザーに新機能が見えないようにする工夫をし、新機能のbranchもどんどん開発ブランチにmergeしていく
    • mergeされたものは随時本番にリリースされるが、ユーザーに見えない工夫をしているので問題なし
  • PRは可能な限り細かくする
  • 機能が完成したら最後にユーザーに新機能を見えるようなPRを作り、mergeしてリリースする


なぜこの戦略を使うのか

いろんな失敗を経験して、この戦略を最近使っている。

  • 失敗パターンその1: 巨大PRパターン
  • 失敗パターンその2: 新機能リリースブランチパターン

失敗パターンその1: 巨大PRパターン

新機能を1ブランチで作って一気にPRするパターン。以下のような困り事がある。

  • 超コンフリクト問題
  • レビューする観点が多すぎて見落としてしまう
    • バグの混入を防ぎにくくなる
  • 指摘が多くなりがちで、レビューされる側が疲弊する
  • レビュー依頼 -> レビュー完了 -> レビュー依頼 -> ...のサイクルが多くなり、レビューする側もレビューされる側も疲弊する
  • mergeしたところ他にmergeした箇所と競合して動かなくなったということが起こる
  • etc...

大体500行の変更を超えてきたあたりで辛くなる印象。

失敗パターンその2: 新機能リリースブランチパターン

新機能のリリース用ブランチを一つ用意して、そこにPRを送るパターン。PRは細かくレビューされ、リリース用ブランチへmergeするため、巨大PRパターンよりましに見える。

しかし、以下の困り事が起こる。

  • 最後のmergeしたところ動かなくなる問題が解消しない
  • 最後にリリースPRを全チェックしないとなんとなく安心できない
  • 他の機能で必要になったメソッドをcherry-pickしまくる

「他の機能で必要になったメソッドをcherry-pickしまくる」が一番つらい。例えば

  • feature1でfind_hoge_blogというメソッドを作る
  • feature2でも同じメソッドを使うことになった
  • find_hoge_blogを作ったcommitをcherry-pickする?
  • しかしそのcommitをcherry-pickするとそのメソッド以外のコードも紛れてくる
  • コピペするしかない???

のような状況が起こる場合がある。

これらの失敗から

これらの失敗から、最初の戦略を用いることになった。


ユーザーに新機能を見えないようにするパターン

最初に掲載した戦略を見ると、「ユーザーに新機能が見えないようにする工夫」という部分ができれば、細かくmergeすることが出来る。このような工夫のパターンは以下のようなものがある。

  • ロジックのメソッドから作る
  • ユーザーに見えない部分から作る
  • エンドポイントを本番では見えないようにする
  • 一部のHTMLだけ本番では隠しておく
  • リニューアルパターン

ロジックのメソッドから作る

  • Logic::Blog->find_public_blogsとか、Logic::Blog->createとかだけ作ってPRする
  • ロジックをユーザーに見えるControllerとかで使わなければ当然影響なし

ただし、次の点に注意。

  • ユースケースを分析しておかないと、実際に使う時に使えないことに気づくことがある
  • 使い方擬似コードを書きつつ、それも合わせてレビューしてもらうと良い

ユーザーに見えない部分から作る

例えば「MySQLのLIKE検索から、Elasticsearchに置き換えて自由なソートも利用可能に」という機能を作ることを考える。これをもう少し分割すると以下の4つになる。

  1. Elasticsearchの環境作り(手元、開発環境、本番)
  2. 検索したいデータのElasticsearchへの同期
  3. 検索クエリでの検索裏側をMySQLからElasticsearchに置き換え
  4. Elasticsearchを使ってソートを実装

このようにすると、3番まではユーザーに影響がないので、いつリリースしても問題ないので、細かくmergeできる。

エンドポイントを本番では見えないようにする

用途

新機能を特定のURLで提供する時に使える。

方法

  • 開発環境ではURLでその機能にアクセスできるように
  • 本番環境ではNot Foundにする
  • 単なるフラグだけで実装可能

コードのイメージは以下の通り。

# /feature1でアクセスできるエンドポイント(Controller)
sub feature1 {
    my ($req, $res) = @_;

    # 開発環境でのみONになるフラグ
    # フラグは環境変数を使ってもいいし、フレームワークの設定などを利用しても良い。
    # TODO(feature1_release): リリース前に消す
    if (!$ENV{CAN_SHOW_NEW_FEATURE}) {
        return $res->not_found;
    }

    ...
}

これでこのURLでアクセスする機能は細かくPRを作って開発できる。

最後に見えるようにするには

完成したら、開発環境でのみONになるフラグを使ったif文を消すだけでユーザーに公開できる。

一部のHTMLだけ本番では隠しておく

用途

あるページの一部に新機能追加する時に使える。

方法

テンプレートエンジンを使っていると簡単。フラグをテンプレートに渡して、特定HTMLを消したりするだけで実現可能。

...

[%- IF can_show_new_feature %]
  <section id="feature1">
    ...
  </section>
[%- END %]

...

もしこの要素に関係するJSを実装したい時も、要素のあるなしでJSを実行するかどうかを決めれば、JSも細かく作ることができる。

最後に見えるようにするには

完成したら、テンプレートのIF文を削除するだけでユーザーに公開できる。

リニューアルパターン

用途

新機能でページがガラッと変わり、特定HTMLを隠すとかでは対処不能な時に利用できる。

方法

  • そのページ用のコントローラとテンプレートのセットをもう1つ用意する
  • 新しいコントローラは本番では見えないように
  • 最後に前のやつとごそっと入れ替える

別URLでアクセスできるコントローラを作り

# /feature1_renewalでアクセスできるエンドポイント
sub feature1_renewal {
    my ($req, $res) = @_;

    # 開発環境でのみONになるフラグ
    # TODO(feature1_release): リリース前に消す
    if (!$ENV{CAN_SHOW_NEW_FEATURE}) {
        return $res->not_found;
    }

    ...
}

feature1_renewal.htmlのようにHTMLファイルも別で作る。

これで、新しいページはユーザーに見えることなく細かく実装できる。

最後に見えるようにするには

  • feature1_renewalメソッドをfeature1メソッドに改名し
  • feature1_renewal.htmlをfeature1.htmlに改名し
  • 対応するCSSやJSなどを移動する

ということをすれば、/feature1でアクセスできるページを一気にリニューアルできる。


まとめ

  • 新機能作成時に開発ブランチに細かくmergeしていく戦略について紹介した
  • ユーザーに見えないようにするパターンを5つ紹介した
  • 実際にはこの5つをうまく組み合わせることで、最後にユーザー向けにリリースするためのPRの変更量を最小にすることができる