fbpx

Git リポジトリでの衝突との戦い

この記事は1年以上前に投稿されました。情報が古い可能性がありますので、ご注意ください。

チーム開発では他の開発者が自分と同じファイルの同じ箇所を修正していることはよくあります。最終的に両者の修正をマージする必要があります。多くのバージョン管理システムは同じファイルの修正でも修正箇所が異なれば自動でマージしてくれますが、修正箇所が重なっている場合は衝突したことを開発者に通知し、解決を開発者に委ねます。

多くの場合、衝突するのはほんの数行で、解決はそれほど難しくはありません。

しかし、中には互いの修正が入り乱れすぎてて解決が難しい衝突も発生します。この場合、慣れている開発者でもうまく解決できずに、予想外のバグを引き起こすこともあります。このような経験がある方も多いのではないでしょうか?

このようなやっかいな衝突の解決の頼みの綱は自動テストですが、そのテストすら衝突しまくってしまい、役に立たないこともあります。

やっかいな衝突を確実に解決する方法はありません。やっかいな衝突が起きにくいように開発を進めるべきです。

メインブランチとの剥離を減らす

次の図は Git の機能ブランチの説明でよく見かけます。

この図は現実にあまり合っていません。実際には次の図のようにメインブランチも機能ブランチもコミットされるたびに剥離が大きくなります。

この剥離が大きくなればなるほど衝突の解決が難しくなります。メインブランチからこまめにマージして、この剥離を抑えます。

この方法は一つ一つの衝突を小さくすることで解決の難易度を下げています。やっかいな衝突の確率を軽減できますが、根本的な解決ではありません。

衝突しにくいプログラム設計へ

「またこのファイルが衝突したか...」と思ったことはありませんか? これは修正は一部のファイルに集中することが多いからです。(興味がある方はコミットログからファイル単位の修正回数の分布を取ってみると面白いかもしれません)

この修正が集中しているファイルは、複数の開発者が同時に修正する確率が増えます。

この原因を解消すれば衝突しにくくなります。

衝突しやすい例

衝突しやすい例としてECサイトのショッピングカートがあります。

  • キャンペーン・クーポン・割引・おまけなどの要件がどんどん追加変更されます
  • 合計金額はもちろんの事、ポイントも計算する必要があります
  • カートの商品の価格が変わったら知らせるなどの、利便性のための新機能がどんどん追加されます

これらの大量の処理を一つのカート画面のソースコードファイルに書くとどうなるか... わかりますよね? 特にキャンペーンやクーポンのたぐいはどんどん仕様が追加変更されますので、衝突が頻発するようになります。

余談ですけど、ドメイン駆動設計の勉強会でショッピングカートを題材にしようとしたら何名かが本当に嫌そうにしていました。(多分、経験者でしょう)

開発の初期ではそれほどコードは複雑ではありません。キャンペーンのコードは次のように明確に分かれていて、太郎さんが新しいキャンペーンを追加し、花子さんがキャンペーン B の仕様を変更したとしても、修正箇所が異なるため、やっかいな衝突まで発展することはまれです。

しかし、何度もキャンペーンの追加変更が行われ、下手な共通化が導入されるとコードはどんどん悪化します。

この場合、キャンペーンの追加や変更を太郎さんと花子さんが同時に行うと、解決の難しいやっかいな衝突を引き起こしやすくなります。

衝突を避けるプログラム設計

衝突を避けるためにはファイルを同時に修正する理由をなくせばいいわけです。

もっとも単純なのが、仕様変更を拒否することです。実際には拒否できないと思いますが、開発チームのキャパシティを超えた仕事は最悪セキュリティインシデントにつながる可能性があるため、分量を話し合うことも大事だと思います。

よく使われているのが、衝突しそうな仕様変更を同時にこなさないよう調整する方法です。マネジメントが難しくなるのであまりおすすめはしませんが、どうしても避けられない場合はこの方法を取ります。

一番おすすめなのが、修正する理由の単位でファイルを分けることです。

ショッピングカートの例ではキャンペーンごとの処理をそれぞれクラスや関数に抽出し、別々のファイルにします。もちろん、一つのキャンペーンを複数の開発者で同時に修正すると衝突しますが、そんなことはまれでしょう。

それでも起きるやっかいな衝突

ここまで紹介した対策を行ってもやっかいな衝突をゼロにはできません。次の原因があります。

  • グローバル設定ファイル
  • 構造変更やリファクタリング

グローバル設定ファイル

初期の Java Servlet ではハンドラーはグローバルの設定ファイル web.xml に登録していました。ハンドラーを追加するたびに web.xml をみんなが修正するため、よく衝突していました。ほとんどは簡単に解決できるのですが、まれに他の開発者のハンドラーを消してしまうなどのトラブルが発生していました。

このような頻繁に修正されるグローバル設定ファイルは衝突が起きやすいです。

今ではこのようなフレームワークはまれです。Java Servlet もハンドラーをアノテーションで登録できるようになり、web.xml が衝突することはめったになくなりました。

衝突しやすい傾向のグローバル設定ファイルを利用しているフレームワークの採用は避けます。また、ミニフレームワークを自作することもあると思いますが、衝突しにくいようデザインする必要があります。

この問題は開発の初期の頃は問題にはなりません。開発が進み、コード行数が増えていくに従い、じわりじわりと苦しくなります。この状態から他のフレームワークに乗り換えるのはかなり難しいので、開発に入る前のフレームワークの検討時に評価します。

構造変更やリファクタリング

例えば新しいキャンペーンでは今までにない適用条件が必要になったとします。このとき、構造や API が変更されることが多いです。API の利用側のコードも修正されるため、程度の差はありますが修正が散らばります。構造変更だけでなく、ある程度の規模のリファクタリングも修正が散らばる傾向にあります。

修正が散らばるため、他の修正と衝突しやすくなり、衝突すると解決がやっかいになりがちです。

一般的には、構造変更やリファクタリングが完了するまで干渉する部分の修正を待つ、つまりはマネージメントで回避します。

しかし、修正の完了を待てないことも往々にしてあります。例えばリファクタリングのために数ヶ月の間機能追加や仕様変更ができないとなると許容できないでしょう。その場合はデザインパターンなどを駆使して、旧プログラム設計と新プログラム設計が混在して動作するよう設計します。

どちらの方法を採用するにせよ、開発者へのインパクトが大きいため、モブプログラミングなどで様々な検討をしながら進めたほうが良いでしょう。

最後に

解消が難しい衝突の解決はとても大変です。そのコードを熟知した開発者でも安全に解決することは難しいでしょう。ましてやプロジェクトに入ったばかりの開発者には負担が重いです。既存バグを復活させたり、以前の仕様に戻ってしまったり、予想もつかないバグを引き起こします。

このようなやっかいな衝突の解決は開発者にとって悪夢です。自信をもって解決するためには自分が修正した箇所のテストだけでなく、衝突した他の開発者の修正のテストも必要となります。そのテストのテスト手順書がない場合はどのようにテストすればいいかから始めることになります。ほとんどの場合はそこまでする時間が取れませんので、結果としてテスト不足になり、不安を感じながらの解決になります。

開発者の高い能力を衝突の解決で消費するのはもったいないです。衝突しにくくなるよう工夫しながら開発を進め、このような利益を産まない作業よりも、価値を生み出すための作業に能力を活用しましょう!

新規CTA