3 ヶ月掛けて 1 人で Android アプリをリプレイスした話
最近、現場で 3 ヶ月掛けてアプリのリプレイスを 1 人で行いました。
下記はプルリク時の修正ファイル数の画像です。
どのようなきっかけでリプレイスを行い、問題点は何だったか、具体的に何をやって結果どうなったかなどをまとめます。
ソースコード構成
コア機能を実装したモジュールがあり、各社ごとにアプリを作成し、各アプリがそのモジュールを参照する構成となっております。
リプレイス対象としてはモジュールと、ある会社向けにリリースしていたアプリの 2 つになります。
なぜリプレイスを行ったのか
きっかけはとある会社から直近で大幅な機能追加をしてほしいと要望があったことです。
ところが現行アプリは機能追加を想定した作りになっておらず、このまま追加するのが困難だと予想されました。
また、今後もさらに機能追加を行っていくとのことで、このままでは間違いなく立ち行かなくなることが容易に想像できました。
1 人で行ったのは私がチームの中で 1 番 Android の知見があったのと、単純に人手が足りなかったためです。
問題点
独自アーキテクチャ
アーキテクチャには MVC や MVVM などありますが、そのどれでもない独自のアーキテクチャとなっていました。
もちろん深い理由があって独自にしているのなら仕方ないですが、特に独自にしないといけない理由は無いようでした。
また、独自アーキテクチャなのでなぜその実装なのか実装者に聞かないとわからなく、完全に属人化してしまっていました。
グローバル変数、グローバルメソッドの多さ
使えるグローバル変数やグローバルメソッドが多く、各クラスの依存関係がカオスな状態でした。
また、どこからアクセスされても大丈夫なように過剰なパラメータチェックがあり、仕様が複雑化する要因となっていました。
Fat View
View の中にビジネスロジックが書かれている箇所が多々あり、Fat View になっていました。
テストコードが実装できない
上述した問題があったため、とてもテストコードが実装できる状態ではありませんでした。
代わりに別途 DB を無理やり書き換えるテストクラスを作ったり、ボタンを押して各機能を試せるデバッグアプリがありました。
ですが、DB を無理やり書き換えたりデバッグアプリで試したとしてもテストとしては怪しいです。
非同期処理に Thread や AsyncTask が使われている
言語は全て Kotlin でしたが、非同期処理に Thread や AsyncTask が使われていました。
Thread は Coroutine と比べてパフォーマンスが悪いですし、AsyncTask は Android 11 以降で非推奨となるので、 Coroutine に置き換える必要がありました。
やったこと
アーキテクチャの大幅見直し
アーキテクチャを MVVM に変更することとしました。
理由はアプリ アーキテクチャ ガイドで推奨されていたためです。
推奨されているアーキテクチャを使うことで、Android Jetpack をフル活用して効率よく開発ができると考えました。
そして MVVM 適用後の全体図を作成し、チーム全員で話し合った結果、これで行こうとなりました。
実装
肝となる実装です。
実装に関してはトライアンドエラーで、上述した全体図をベースに実装しては動かして調整を繰り返しました。
同時に上述した問題点も潰していきました。
ただし、いきなり古いコードを全削除して書き直すのではなく、古いコードは基本的に残しておくことで影響範囲を極力抑えるようにしました。
そして、機能追加が必要なアプリのみ、新しいコードを参照するように書き換えました。
実装にあたって、主に下記のライブラリを使用しました。
テストコードの実装
実装で DI を導入するなどしてテスト可能な状態になったので、テストコードも実装しました。
過去に作っていたテストクラスやデバッグアプリで行っていた内容をテストコードで実装し、品質をある程度担保できるようにしました。
また、CI のためにビルドとテストコードを実行するためのシェルスクリプトを作成しました。
テストコードの実装にあたって、主に MockK を使用しました。
リプレイス後のソースについて説明会を適宜実施
上述した通り、私 1 人でリプレイスを行っていたため、他メンバーは私が具体的にどうリプレイスしたか把握していません。
そのため説明会を適宜実施しました。
使用したライブラリの説明や各クラスの役割についての説明などを行いました。
各機能のシーケンス図を作成
ソースコードだけでは他メンバーが把握するのに情報不足なため、別途各機能のシーケンス図を作成しました。
現場では図を書くのにLucidchartを使用しており、手書きするのが煩わしかったのでマークアップを使用して作成しました。
PlantUMLのような感覚で使えるので、慣れればサクッと作成できます。
良かった点
機能追加がしやすくなった
UI やビジネスロジックが分離されることで、どこに何を実装するべきかが明瞭になりました。
また、新しく機能を実装する際にも他の機能の実装を見様見真似で実装できるようになりました。
仕様がシンプルになり、把握しやすくなった
無駄な仕様を省くことで仕様がシンプルになりました。
また、データの流れが View -> ViewModel -> Repository -> API or DB という 1 本の形となって仕様が把握しやすいと他メンバーからも言われるようになりました。
パフォーマンス向上によりメモリリークしなくなった
非同期処理を全部 Coroutine に置き換えたことで、今まで長時間稼働させていたときにメモリリークするケースがあったのを解消できました。
Android Jetpack の知識が増えた
今まで Android Jetpack を使ったことがあまりなかったのですが、導入から一通りやってみることで Android Jetpack の知識や便利さを実感できました。
こんなに便利なのに何で今まで使ってこなかったんだろうって思いました。
今後の開発で使う機会があればバンバン使っていきたいです。
課題
他メンバーへの教育
説明会を適宜実施したり、各機能のシーケンス図を作成したりしましたが、完璧に理解できているかというとそうではないと思っています。
ここからは他メンバーにバグを修正してもらうなどして実際に触れてもらい、コードレビューの中で教育していけたらと考えています。
古いコードをいつ削除するか
影響範囲を極力抑えるため、古いコードは基本的に残しましたが、いずれは他のアプリも新しいコードを参照するようにして古いコードを削除していく必要があります。
まずはリプレイスしたアプリに関して全機能テストを行っていき、バグを全部潰したタイミングで他のアプリも同じようにリプレイスしていくことを考えています。
もちろん直近でリリースしなければならないアプリについてはリリース優先でリプレイスせず、タイミングを見る必要があります。
また、「どのコードが削除対象なのか分からない」という指摘があったので、別途削除対象のコードを一覧化して共有しました。
CI/CD ツールの導入
CI 用にシェルスクリプトを作成しましたが、いちいち手動で実行しないといけない分面倒です。
CI/CD ツールを是非導入したいのですが、会社の都合上できていない状況なので打診してみます。
まとめ
現場で 3 ヶ月掛けてアプリのリプレイスを 1 人で行った話をまとめました。
Android Jetpack や Coroutine などのライブラリを駆使することで、無駄なコードが減り、効率の良い開発やパフォーマンス向上などの恩恵を受けることができました。
また、リプレイス後の内容を他メンバーにキャッチアップしてもらうための工夫も重要です。
まだ課題も残っているので、そこを早く潰していきたいです。
本来こういう経験はなかなかできないので、かなり勉強になりました。
Discussion