リファクタリングはできていますか?

手塚悠太氏:それでは、「レガシーな実装を丁寧にリファクタしてモダンな実装にする技術」について、手塚悠太が発表いたします。

自己紹介です。2019年にDeNAに中途入社し、当時はMOV、現在の名前はGOですね、こちらのタクシーアプリの乗務員さん向けアプリケーションを開発していました。またタクシーフードデリバリーサービス「GO Dine」のアプリケーション開発も担当をしていました。こちらのGO Dineは、Flutterを用いています。

その後、2021年8月から『グランブルーファンタジー』推奨ブラウザである「SkyLeap for Android」の開発に携わっています。

さてみなさん、リファクタリングはできていますか? 「機能開発や不具合修正がずっと続いてそんな暇がない」「プロダクトオーナーの理解が得られない」「リファクタリングで新たに不具合を出すのが怖い」などと思っていませんか? 

本日はSkyLeapで実践した内容をもとに、レガシーな実装をモダンな実装にリファクタリングする技術をお伝えいたします。

本セッションの構成です。まず簡単にSkyLeapを紹介しまして、その後SkyLeapが抱えていた課題を紹介します。そしてリファクタリングを進めていくための必要な下準備。その後、実際にリファクタリングをしてSkyLeapはどのようになったのか。最後に、丁寧にリファクタリングをするためのTipsをお伝えします。

『グランブルーファンタジー』推奨ブラウザ「SkyLeap」

『グランブルーファンタジー』推奨ブラウザであるSkyLeapは、『グランブルーファンタジー』を快適に遊べるブラウザで、2019年8月3日にリリースされています。『グランブルーファンタジー』のゲームシステムに合わせて、任意のクエストにすぐアクセスできる「クイックアクセス」や、バトルをしながら編成を試行錯誤できる「2画面分割機能」などを提供しています。

このSkyLeapですが、課題を抱えていました。先ほど、リリースが2019年8月3日と紹介しました。実はこのアプリ、リポジトリが作られたのは2019年6月7日で、なんとリポジトリが作られてから2ヶ月もしないうちに正式リリースされました。

超特急でリリースされたのである程度仕方ない面はあるのですが、やはり技術的負債も多く残っていました。例えばUIスレッドでアクセスされるファイルやデータベース、バイナリデータのパース処理なんかも、UIスレッドで行われていました。

モバイルアプリでは、UIスレッドをブロックしてしまうと画面がフリーズするような動きになってしまうため、UXを大きく損なってしまいます。また、SkyLeapはJavaで開発が開始されており、Kotlin化も道半ばの状態でした。

そのほか、独自のDI処理とJavaとKotlinの混在などで、あちこちで強制アンラップがされていてNull-Unsafetyな状態、またRxにも問題が散見されている状態でした。

これらの技術的負債を解消するにも、まずはKotlinがほしいという結論になりました。当時のSkyLeap開発メンバーはJavaを主力としており、また当時利用を想定していたJavaライブラリとKotlinの互換性を検証する時間もなかった関係で、2019年でもフルJavaで開発されていました。

私がjoinした時点ですでに5割ほどはKotlin化されていましたが、やはり影響範囲が広いところや影響した時のインパクトが大きい部分に関しては、ずっと残り続けていました。どうせ各所リファクタリングするのであればCoroutineやFlowも使いたいので、まずはすべてKotlin化をするという方針になりました。

リファクタリングは、下準備がすべて

リファクタリングは、下準備がすべてと言っても過言ではありません。ここからはリファクタリングをするための下準備について紹介します。

まずは自分自身のプロジェクトを知りましょう。リファクタリングは、時に大きな範囲に影響を与えることがあります。万が一、不具合を引き起こした場合の影響範囲やプロジェクトのリスク許容度を評価します。例えば医療向けソフトウェアなど、プロジェクトによっては人や動物の生命、健康を直接害してしまう可能性があります。

また、金融系ソフトウェアでは他者の財産に大きく影響させてしまう可能性なども考えられます。このような場合は、リスク許容度は低いということになり、慎重にリファクタリングを進める必要が出てくるでしょう。

さらに、将来的な機能追加や売上の拡大などの見込みがなければ、リスクを取ってリファクタリングをする必要はないと判断されることも、時にはあるでしょう。

チームメンバーとの認識がそろっているかも重要です。リファクタリングの重要性の共通認識を作りましょう。

当事者だけ重要性を理解していても、プロダクトオーナーの理解を得られなければ、進めることはできません。SkyLeapには共通認識がありましたが、これがない場合は、まずはその共通認識を作る必要があります。

リファクタリングが必要なコードに目を瞑っていると、いずれ開発速度の低下、プロダクト品質の低下、プロダクトの寿命の低下、属人化の加速などがある、とよく言われます。しかしまれに見落とされるのが、リファクタリングを実施しないで開発がだんだん辛くなってくると、エンジニアの退職リスクが高まるということです。

実際にエンジニアがプロジェクトから離脱するところまで進んでしまうと、負のスパイラルとなって後戻りできなくなってしまいます。リファクタリングの必要性を感じている場合は、チームで議論を進めて共通認識を作るようにしましょう。

リファクタリングがなんとなく不安なのは、リファクタリングの前後で品質の担保に自信がないからと言えます。リファクタリング対象のテストコードがない場合やテストケースが不足している場合は、リファクタリング前に用意し、リファクタリング前後の品質を担保します。リファクタリングに入る前に、テストコードの充実を図っておくのもいいでしょう。

またリファクタリングの対象は、できるだけ細かくチケットを切って管理します。チケットには、そのチケットでやることとクローズできる条件を具体的に記載します。

例えばExampleRepositoryをKotlin化するというチケットであれば、やることにExampleRepositoryのテストコードを書く、必要なテストケースがなければそれも書く、IDEで自動変換をする、自動変換されたコードを最適化する、テストコードがPassすることを確認すると書き、その上で完了の条件としてやることがすべてできていることpull requestがマージされていることといった具体的な内容を記載するようにします。

こうすることで、チケットを開いたあとに「結局このチケットは何をすればいいんだっけ?」というオーバーヘッドを限りなく減らします。

これで下準備は完了です。最後にプランニングを行って、リファクタリングを実施しましょう。影響範囲が小さいものに関しては、スプリントに乗せて順次実行してもよいでしょう。

影響範囲が大きいものは、できるだけほかの作業を止めておきたいところです。ほかの大きな修正と大きなリファクタリングがコンフリクトしてしまうと、そのコンフリクトの解消ですら、不具合を呼んでしまう可能性があります。

そして、リファクタリングを実施したあとのQA計画やリリース計画も、このタイミングで行います。

SkyLeapがリファクタリングされてモダンに生まれ変わった結果

さて、そんなSkyLeapがリファクタリングされてモダンに生まれ変わった結果を見ていきましょう。生まれ変わったと言いましたが、言い過ぎました。まだまだ直したいところは山ほどあるので、生まれ変わりつつある姿を見ていきます。

まずKotlin化に関しては、100パーセント完遂できています。SkyLeapでは、2020年の初頭でKotlin化するという決意のチケットが切られていました。実際に動きがあったのは2020年下期からで、2021年の下期に向けて主に独立したクラスなど、影響範囲が比較的小さい部分、もしくは限定的な部分に関して、機能開発と同時並行でKotlin化が行われていました。

そして今年度上期には機能開発をほぼほぼ止めて、ActivityやViewModelといったUIレイヤーや、それに密結合したテストが書きにくいクラスたちを一気に進めています。最終的には、このぐらいの差分量となりました。

Kotlin化以外では、主に現代のAndroidアプリケーション開発におけるデファクトスタンダードを採り入れていくように、リファクタリングを行っています。DIを独自で実装されていたものからDagger Hiltを導入したり、RepositoryやViewModelをCoroutineやFlowを使う構成に変更したり、UiStateを導入したり、Google Guavaを除去したり、Ktlintでフォーマットを保つなどを行なっています。

リポジトリなどはまだまだ改善の余地はありますし、そのうちUIに関してはJetpack Compose化もしていきたいという気持ちもいっぱいなので、まだまだリファクタリングは続いていきそうです。

丁寧にリファクタリングをするためのTips

さて最後になりましたが、丁寧にリファクタリングをするためのTipsをお伝えします。リファクタリングは、できるだけ影響を小さく抑えて行います。SampleRepositoryをリファクタリングするというこの例ですが、SampleRepository2つのメソッドのうち、1つだけをリファクタリングする例となります。

SampleRepositoryはインターフェイス化を行なって、古い実装はImpl、新しい実装をImpl2として作っています。インターフェイス化は必須ではありませんが、DIをしている場合は切っておくと差し替えるだけで済むので、よりやりやすくなると思います。

新しい実装のほうは古い実装に依存し、リファクタリングをするメソッドのみ実装を行います。それ以外の実装は、古い実装をそのまま呼び出します。こうすることで、影響範囲をリファクタリングしたメソッドのみに限定できます。

Kotlinのようにclass delegationのような機能がある場合は、そちらを用いてもよりシンプルになっていいと思います。

こちらは、t_wadaさんのツイートの引用です。これまでも何度かお伝えしていますが、t_wadaさんのツイートにあるように、影響範囲の小さいリファクタリングというものは定常作業に組み込んで絶え間なく実施していくといいでしょう。

SkyLeapは、2週間のスプリントでスクラムを組んでいます。スプリントレビューとレトロスペクティブの翌日の1日を改善の日に割り当てており、そこで細かなリファクタリングや改善を行う日として、スプリントの中に予約を行っています。また、必要に応じてスプリントの中に組み込んでしまうこともあります。

古い実装のAPIを削除できない場合は、@Deprecatedを付けて、今後呼び出されないようにしましょう。KotlinのDeprecatedアノテーションは高機能なので、任意のメッセージを設定するだけでなく、IDEの自動置き換えの機能で新しい実装に置き換えを行うように指示できます。

またDeprecationLevelの設定も可能で、単純に警告を出す状態のほかコンパイルエラーにさせたり古い実装をコード補完機能の対象から隠したりできます。どのやり方でもかまいませんが、今後新しい実装に気づかずに古い実装を使ってしまった、ということがないようにしましょう。

目の前のリファクタリングだけに集中しよう

最後に、リファクタリングをする時は、目の前のリファクタリングに集中するようにしましょう。コードを直していると、周辺の実装も気になってきて「あっちもこっちも直したい」と思うようになります。

しかし範囲を広げてしまうと、影響範囲も無限に広がってしまいますし、時間も無限に足りなくなります。グッと我慢してチケット化だけを行って、機をうかがいましょう。SkyLeapでも、Kotlin化のほかにもっとリファクタリングしたい部分がまだまだたくさんあります。優先順序を見失わず、改めて計画を立てて実施するようにしましょう。ここまで来れば、リファクタリングを計画的に効率よく実施できる下地が作られているはずです。

以上で発表を終わります。ご清聴ありがとうございました。