破壊的変更のあるバージョンアップ作業の流れ

こんにちは。Kaneyasuです。
先日飲み会で過去に破壊的変更のあるバージョンアップ作業をなんどかやったことがあると話をしたら、興味を持っていただけました。
私が行ったバージョンアップ作業のうちの一つをこちらの記事にまとめています。
少し古い記事ではあるのですが、参考にはなるかと思います。

https://zenn.dev/satoshi256kbyte/articles/5e587ae730240f

具体的なプログラムの話は、この記事に書かせていただいたので本記事ではバージョンアップ作業の流れの方にフォーカスします。

# 本記事の前提

- 期限が決まっており、それまでにシステム全体をバージョンアップしなければならないと捉えてください。
- 段階的バージョンアップはできません。
- プログラミング言語はPHPをイメージしてください。
- 自動テストはないとします。

# 破壊的変更とは

破壊的変更とは、バージョンアップによって既存の機能やコードが動作しなくなる変更のことです。
プログラミング言語やフレームワークのバージョンアップに伴い、過去の書き方が使えなくなる、関数の入出力形式が大幅に変わる、などが該当します。
私が知っているところだと、CakePHPのバージョン2から3、AngularJSのバージョン1から2、などが該当します。
破壊的変更のある場合、バージョンアップ作業が非常に大変なのでなかなかバージョンアップができないということが起きえます。
そうこうしていると、セキュリティの都合などでいよいよ無理矢理にでもバージョンアップしなければならないと言う状況になることがあります。
過去私はそのような状況において、バージョンアップ作業を爆進したことがあるので、それを踏まえた自分的バージョンアップ作業の流れを書かせていただきます。

# バージョンアップ作業のビジネス的価値

極端な話をします。
バージョンアップ作業は、マイナスをゼロにする作業でしかありません。
技術的には価値があると思いますが、ビジネスの観点で見ると価値を見出すのは難しいでしょう。
これを念頭に、まずはバージョンアップ作業の大きな方針を決める必要があります。

# バージョンアップ作業の大きな方針を決める

バージョンアップ作業の大きな方針としては、2つ考えられます。

- 新しいバージョンの作法に合わせて作り直す
- 既存のコードを極力活かす

エンジニア的には前者を選びたいところですが、私は後者の方をとります。
理由は2つあります。

1つ目は、バージョンアップ作業はビジネス的価値を見出すのが難しいので、できるだけ早くマイナスをゼロまで持っていき、そこからの機能追加から新しいバージョンの作法に最適化していく方がコストパフォーマンスが良いと考えられるからです。

2つ目は、人材確保とプロジェクト管理が非常に難しくなるからです。
バージョンアップ作業は、実は新旧のバージョンの知識が必要です。
新しい知識を習得するだけでも大変なのに、旧の知識も必要となると、人材確保が非常に難しくなります。
バージョンアップ作業はある程度人海戦術になりがちです。
人材確保が非常に難しい状況を作ってしまうと、人海戦術が使えなくなり、八方塞がりになります。
バージョンアップ作業を単純作業に落とし込めば、人海戦術が可能になりますが、落としこみと人海戦術を軌道に乗せるのにアーキテクトやリーダーに多大な負荷が長期間かかり続けます。
バージョンアップ作業におけるアーキテクトやリーダーへの負荷と人海戦術はある程度は致し方ないのですが、できる限り負担を軽減するため、既存のコードを極力活かす方針をとります。

# 既存コードの活かし方

方針を決めたらまずはできるだけ広い範囲をカバーすることを考えます。
私は以下のように既存コードを活かしていきます。

![変更前のコード](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1959433/f1c4f450-39d9-4d34-eefd-c400ccb6e867.png)

例えば、関数`func`があり、それを呼び出している箇所多数あるとします。

破壊的変更により`func`の入出力形式が変わったとします。
この状況で既存コードを活かすためには、以下のような作業を行います。

![入出力が変わっている旧関数を名称変更](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1959433/0d4312b8-d2c5-b1b7-22ea-2aee745f59ad.png)

言語やフレームワークをバージョンアップする前に、既存の`func`とその呼び出し箇所を探し、名称を`legacyFunc`に変更します。

![名称変更した旧関数で新しい関数の入出力を変換](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1959433/166bad7b-d4ba-116b-637e-dbe7c0ec26bd.png)

変更した後に、言語やフレームワークをバージョンアップします。
`legacyFunc`の中で、新しい`func`を呼び出すようにし、出力を旧い形式に変換するようにします。
`legacyFunc`に`@deprecated`アノテーションをつけ、現時点では使っても良いが、新規のコードは使わないように警告が出るようにします。


これで、呼び出し元のコードを変更せずに済みます。
あとは、`legacyFunc`は入出力形式を変換してるので、変換のオーバーヘッドがなるべく小さくなるようチューニングを行います。

# 正規表現による置換

`func`から`legacyFunc`への変換など、既存コードの機械的な変換はVSCodeなどの強力なエディタを使うと効率的です。
VSCode を使えば、改行を含む複数行の置換や、正規表現を活用した高度な変換も可能です。

https://code.visualstudio.com/docs/editor/codebasics#_case-changing-in-regex-replace

頑張れば、関数の第1引数と第2引数を入れ替えるようなこともできます。
このやり方で、大多数のコードを変換していきます。

このあたりまでの作業は、シニアクラスのエンジニアが極少人数で行った方が効率的です。
正規表現による置換は、プロジェクトの中で急いで広めないといけない知識でもないですし、人へ説明する手間などを省いた方が良いでしょう。

# 静的解析ツールの導入と各種コメントのメンテナンス

正規表現による置換などを駆使し、ある程度の変換が済んだら静的解析ツールを導入します。
そして、静的解析ツールが廃止された構文や入出力の齟齬が拾えるように各種コメントやアノテーションをメンテナンスしていきます。
最低限の記述を行うように指示すれば、ここは人海戦術で対応できるでしょう。

この作業、一見遠回りに見えますが、これが最も効率的だと考えています。
破壊的変更を伴うバージョンアップにおいては、バージョンアップが正確に行われたかを人力でチェックし切るのは不可能です。
PHPのような動かしてコードが通らないと真の意味ではチェックできない言語であればなおさらです。
ツールの力を活用するのに全力を尽くすことをお勧めします。

メソッドの引数・戻り値の指定や、アノテーションを追加してまわるぐらいなら、みんなでやれば短期間で終わります。

PHPStanなどの静的解析ツールには、ルールレベルが設定できます。

https://phpstan.org/user-guide/rule-levels

迷うところですが、私はルールレベルは最大にするのをお勧めします。
私自身、ルールレベルは最大以外を選ぶ妥当な理由が見つかりませんでした。
中途半なルールレベルを選んでしまうと、なぜそのルールレベルを選んだのか問われると、答えに窮することでしょう。
ルールレベルを最大にして、HITしすぎてどうにもならないケースを一つ一つ無視リストに追加していくのがベターだと思います。

# 特殊なコードのフォロー

メソッドの引数・戻り値の指定や、アノテーションの追加を頑張ってもらっている間、アーキテクトやリーダーは特殊なコードのフォローを行うのが良いでしょう。
よくあるのは、SQLをハードコーディングしている箇所です。
言語やフレームワークの方で、クエリビルダやORMを使うことが推奨されていても、サブクエリが必要などの理由でSQLがハードコーディングされていることは多々あります。
このような特殊なコードを放置すると、心配で手が止まってしまう人が現れので早めにフォローを行いましょう。

特殊なコードはビジネスロジックが難しい箇所でよく見られる現象です。
従って特殊なコードがある箇所＝ビジネス的に重要なロジックの可能性があります。
放置すると心配で手が止まってしまう人が現れるのにはこういう側面もあります。
こういう観点でも早めにフォローを行うことが大事と思います。

# まとめ

私の考える破壊的変更のあるバージョンアップ作業は以下の通りです。

1. 既存コードを活かすよう入出力形式を変換する関数を作成する
2. 正規表現による置換などを駆使し、既存コードを変換する
3. 静的解析ツールを導入し、各種コメントやアノテーションをメンテナンスする
4. 静的解析ツールでチェック
5. 3と4を繰り返しながら、傍で特殊なコードのフォローを行う
6. 最後は手動テストなどで、丁寧に確認＆修正

以上です。
みなさまの作業の参考になれば幸いです。