新たな価値を提供し続けるためには、コードの健全化が必要です。アプリは一度リリースして終わりではなく、新機能の開発を行っていきますが、新機能の開発を続けるには、コードの健全化も進める必要があります。
新しいOSへの対応や増えていく機能との兼ね合い、新しく出てきたより良い設計思想の取り込みなど、「適切な状態」というものは常に変化し続けるため、コードの健全化はどのソフトウェアにも必要です。
これを怠ると開発効率が悪くなっていき、新たな価値を提供できなくなってしまいます。Yahoo!天気アプリでは、新規開発と並行して技術的負債を返済していく、コード健全化の取り組みを行っています。
約3年前と現在のアプリを比較します。違いは見た目だけではありません。長らく抜本的なリファクタリングが行われていなかったこと、Androidのノウハウがあまりない時代の設計を引きずっていたことなどから、技術的負債が多数残っていました。より良い設計思想を取り込むなど、さまざまな改善を通して約3年でほぼ全てのコードを書き換え、数多くの技術的負債を返済してきました。
Yahoo!天気アプリの改善で行ってきたことは、特別なことではなく、当たり前だと思われるようなことです。 当たり前のことを当たり前に実施していくことが、非常に重要だと考えています。
アプリ開発者が遭遇する困難
ここからは、アプリが遭遇する困難と、それに対する健全化の取り組みを「仕様とデザイン」「フレームワークと内部設計」「アプリの寿命」の三つの観点で紹介します。
仕様とデザイン
初めて開発者目線でYahoo!天気アプリをじっくりと見た際に、少し気になる部分がありました。そこはかとなく感じる、「Androidらしくない」印象です。
ネイティブアプリのUI開発では、スイッチやチェックボックス、ラジオボタンなど、標準のUIコンポーネントが提供されており、これらを組み合わせてアプリを製作します。また、画面構成の仕方など、ある程度の方針がプラットフォーマーから提示されています。Androidの場合は、Googleのマテリアルデザインが挙げられます。
このような標準のコンポーネントを使わず、独自実装などによって標準から外れた実装をしてしまうとAndroidらしくないという印象につながってしまいます。
標準から外れる原因の1つに、開発者の知識不足が挙げられます。Android開発では非常に多くの知識が要求されますが、最初期には、Androidアプリ専門のエンジニアはほとんどおらず、当時はオフィシャルのドキュメントがないなど、Android SDKに対する理解が浅かった、UIコンポーネントの存在を知らなかった、知っているけど適切な使い方がわからなかったという事情もあったと思います。Android開発では、これらを適切に利用する知識やデザインガイドラインに対する知識も必要です。これらについては常に勉強し続け、新しいものを取り込み続けることで解決できます。
日本でシェアが大きいiPhone
そして、1つの原因が「iPhone」の存在です。少し話題がそれますが、Androidエンジニアの機嫌を損ねる指示があります。「iPhoneと同じにしておいて」です。Androidエンジニアの方には共感いただけると思います。
ただ、日本におけるAndroidとiOSのシェアは、iOSの方が大きいのが現状です。シェアの大きいiOSが優先されるでしょうし、仕様やデザインを考える人間がiPhoneしか知らない場合もあります。そのため、各種仕様がiPhoneを基準に決められてしまうことはよくあります。
「iPhoneみたいなUIを作るのは大変かもしれないけれども、それはそれで良いのでは」と思われるかもしれません。まず、作るのが大変であるということ自体が良くないことなのですが、ユーザー目線で見ても良くないところがあります。
世の中には非常に多くのアプリがあり、Android版、iOS版、両方に同じアプリが提供されるのは珍しいことではありません。サービスを開発する側の視点に立つと、どうしても両OSのアプリを見比べてしまいます。
「同一サービスの同一アプリなのだから、同じであるべきだ」と考えるかもしれません。しかし、ユーザーの目線で考えると、iOSとAndroid両方を持っていて、同じアプリを入れて使っているユーザーは多くないでしょうから、デバイスをまたいだ共通性はほとんど見られることはありません。1つのOSの中のほかのアプリ、特にOS付属のオフィシャルアプリと行き来しながら使うため、それらとの基本的な操作性やUIの配置といったルールが統一されていないと、ユーザーにとって使いにくいものになってしまいます。
ゲームなど、独自の世界観を作り込み、没入感を演出することが重要なアプリといった例外はあるでしょうが、サービスとしてのブランドカラーなどは統一しつつも、ベースとなるUIルールはOSと親和性の高いものにしていくことが最も良い方法の1つだと思います。
標準のコンポーネントを素直に使った、エンジニアにとっても製作しやすいアプリが最も健全な状態の1つだと思います。開発コストを低く抑えることができますし、OS側のアップデートに自然と追従できて、メンテナンスコストを削減できます。広く使われているものであるため、ユーザビリティにも優れていて、アクセシビリティにも自然に対応できます。このように、素直な実装は、低コストに完成度の高いアプリにつながり、それだけで大きなメリットがあります。
要するに、「AndroidアプリはAndroidアプリらしくあるべきである」ということです。 OSとの親和性は、仕様やデザインと時にぶつかる場合もありますが、どちらかだけを優先していては良いアプリにはなりません。 関係者と話し合いその意図を確認し、よりOSと親和性の高い仕様をエンジニアからも提案する。こうすることで、ユーザーにとって使いやすく、メンテナンスコストも低く、品質の高い仕様を作ることができます。
健全なコードであるには、健全な仕様とデザインであることが必要です。デザインガイドラインやライブラリを積極的に導入し、適切な使い方を知る。OSと親和性の高い素直な実装で高い完成度を実現する。仕様やデザインの意図を共有し、OSと親和性の高いコードをエンジニアから提案していく。そのようなことを行い、健全な仕様に基づく健全なコードへと改善していきます。
フレームワークと内部設計
Androidアプリ開発にはさまざまな難しさがありますが、その1つにどれほど多機能なアプリであっても、1つのパッケージとして実装する必要がある点が挙げられます。作り方次第では、あらゆる機能を混ぜて実装できてしまいます。
そこでよく発生するのが、誤った抽象化・共通化といった問題です。特にAndroidの画面を構成するコンポーネントであるActivityやFragmentに、共通機能やよく使われる機能を全部実装してしまったBaseActivity、BaseFragmentというものが作られてしまいがちです。
ActivityやFragmentを拡張することが絶対悪だというわけではありませんが、その結果として、簡単には引き離せない技術的負債になりかねません。 Yahoo!天気アプリにもBaseActivityやBaseFragmentがあり、これをひとつひとつ引きはなし、完全に削除するまで2年ほどかかりました。
ライフサイクルとスレッド
Androidの画面には独特のライフサイクルがあり、これを適切に扱う必要があります。例えば、画面が回転するとインスタンスが再生成されるため、この状態の引き継ぎや、インスタンス間の接続をやり直すなどの手続きが必要です。
スレッドの観点では、UIコンポーネントはメインスレッド以外で操作してはならず、メインスレッドでネットワーク処理を行ってはならないというルールがあります。そのため、多くのアプリが行う、ネットワークAPIにアクセスし、その結果を画面に表示するという処理はスレッド間のデータの受け渡しが必要になります。いずれも、Androidをよく知らない人からするとハードルが高いところです。
サポート範囲の広さ
Androidでは、OSのバージョンごとに使用できるAPIが変化し、それをAPIレベルという数値で表現します。OSバージョンでは、必ずメジャーバージョンが変わるわけではありませんが、メジャーバージョンが共通でもAPIレベルでは異なるため、エンジニアが気にするべきバージョンはむしろAPIレベルです。
現在Yahoo!天気アプリでは、Android7から最新のAndroid13まで、APIレベルでいうと24から33までサポートしています(2022年11月時点)。実に10種類のOSバージョンに対応する必要があります。
さらに言えば、Androidはさまざまなメーカーが多様な端末をリリースしているため、画面の大きさなど、ハードウェアのバリエーションが非常に多い傾向にあります。各メーカーはOSに対しても独自の機能を盛り込むことがあるため、同一のOSバージョンであってもメーカーによって挙動が異なるなどの問題に対処していく必要があります。
これらの問題に対しては、「使えるものは使う」、そして「型にはめる」ことで対処できます。
使えるものは使う
Android界隈では、OSバージョンやデバイス間の差異をうまく対処し、ライフサイクルやスレッドを簡単に扱えるようにしてくれるものなど、非常に優れたOSSが数多く提供されています。すでにある仕組みは積極的に導入し、車輪の再発明をせずに済みます。
また、開発言語面にも大きな変化がありました。従来ほぼ100%「Java」で開発されていたコードは、現在ほぼ100%「Kotlin」に書き換わっています。Google I/O 2017で公式の開発言語としてサポートが発表され、Google I/O 2019では「Kotlinファースト」が発表されたように、非常に優れた開発言語です。優れた言語仕様と豊富な標準ライブラリによって、安全かつ簡単に記述できるようになり、一気にコードの品質を上げることができました。
型にはめる
もう1つ「型にはめる」とはアーキテクチャを導入することです。レイヤードアーキテクチャを参考にしたアーキテクチャを導入しています。アーキテクチャ導入における最も重要なポイントは、モジュールの整理方法を都度考える必要がないことです。
初めて開発に参加する人も、一定のルールで整理されているコードを見れば、それに合わせて開発することで、自然とモジュールを一定のルールで製作できます。また、各機能のスコープを自然に制限して、局所最適や間違った共通化、過度な複雑性が発生するのを抑制するのにも役立ちます。
アーキテクチャの導入にはコストなど考慮すべきことが多く、リアーキテクチャには全体を書き換えるぐらいの覚悟が必要です。しかし、うまく導入できれば、開発が一気に楽になるはずです。
ここで紹介したのはごく当たり前のことかもしれませんが、使えるものは使ってできる限り楽をしよう、楽しく開発しようとしています。
アプリの寿命
スマホアプリは、最初のリリースからずっと同一のパッケージを更新し続ける必要があります。Yahoo!天気アプリでは、10年以上にわたって更新し続けています。
もちろん、完全に別のアプリを作ってそちらに移行していただくことも可能ですが、移行しないユーザーもかなりの割合で発生するでしょうし、おいそれと取れる手段ではありません。
長期にわたって更新を続ける上で、いくつかの課題があります。
1つは、一度出してしまうと取り返しのつかない点です。例えば外部とのプロトコル。Androidでは「AndroidManifest」というファイルで、外部アプリからの起動方法やウィジェットに関する情報など、そのアプリの外部仕様を定義しています。ここに記述した内容は、アプリ外部から使われるもののため、安直にアップデートで変更してしまうと、正常に動作しなくなってしまう可能性があります。例えば、起動する画面のクラス名を変更すると、ユーザーが作ったショートカットが消えてしまったり、残ったとしてもアプリを正常に起動できないといった問題が発生したりします。
アプリの永続化情報も同様です。アプリの設定などをローカルストレージに保存したものは、アップデートしても変化せず残り続けるため、取り返しがつかない可能性があります。こうして外に出てしまったアプリの仕様は、完全に取り除くことが不可能になってしまう場合があります。
アプリの外に出てしまった過去のアプリの仕様は、現在のアプリのコードからは見えなくなっている場合もあります。連携機能であれば、どこから使われるかという情報は、コードに残らない場合が多いです。
運用が終了しているが、本当に一切使われないのかと確信が持てず、残り続けているコードもあります。過去の永続化情報も同じです。Androidでは、「SharedPreferences」というkey-valueのデータ構造で任意のデータを保存できる仕組みがあり、アプリの設定などはここに保存することが多いです。
適切に管理しないと、今は使われていない過去に書き込んだデータがどのようなキーや型で使われていたのか、といった情報が失われてしまいます。すると新たな読み書き処理の追加は、非常にリスキーになります。新たに追加したキーが実は過去に使われていた場合、テストなどで検出することは不可能ですが、リリース後に昔から使ってくれているユーザーの端末でのみ問題が発生することがあります。
アップデートを繰り返して1つのアプリを長く使ってもらう
このように、アプリの外に出てしまったものは、その処理が削除されても影響を及ぼし続けます。また、アプリはユーザーの環境にインストールして使っていただくという性質上、アップロードされているパッケージを差し替えただけでは変更は反映されず、ユーザーにアップデートを行ってもらう必要があります。
しかし、仕様の変化などを嫌ってアップデートしないユーザーは一定数います。新機能や仕様の変更だけではなくて、バグの修正や、新たに発覚した脆弱性の修正といったものも含まれているため、全てのユーザーにアップデートを実施していただきたいですが、それを強制することも難しいです。
データ構造が変わったのでマイグレーションを行いたいと思っても、アップデートを行ってもらうまではマイグレーションが実行されず、古いデータ構造がいつまでもユーザー環境に残り続ける、という問題があります。
こういった問題に対しては、アプリのコードを見ているだけでは健全化することは難しいです。ログを仕込んで調査したり、関係者への聞き込みをしたりといったアナログな方法で、本当に今使われているのか調査し、使われていないと分かったものは仕様ごと削除してしまいます。
アプリ内の特定の画面を直接呼び出すショートカットなど、機能提供が終了したので、新規には使われないが、過去の名残で使われている可能性が残っているものもあります。こういったものは、全体の整合性のとれる仕様に落とし込みます。
ショートカットを例に挙げると、起動先の画面はすでにサポート終了したものだったという場合は、入り口だけを残して、アプリを通常起動させるにとどまる。別の同等の画面がある場合は、そちらを起動する。そして、古い画面やそれに関連する処理は削除します。ひとつひとつ調査し、謎仕様を取り除いていく地道な作業が求められます。
「終わり」を常に意識する
どれほど熱意を込めて製作した機能であっても、ユーザーに全然使われず廃れてしまうかもしれませんし、非常に人気の高い機能も、数年後には使われなくなっているかもしれません。常に「終わり」を意識する必要があります。
アップデートしないユーザーの存在も考慮した、新旧の仕様への両対応やマイグレーションのサポートには、相当の期間がかかります。マイグレーションは一人のユーザーにとって、一回だけ実行すれば終わる処理ですが、両対応している開発陣にとっては、チェック処理が都度走るなどの無駄が発生します。
こういった一時的な無駄は許容しなければなりませんが、最後の一人まで対応し続けるわけにはいきません。 Yahoo!天気アプリでは、アップデートしないユーザーをサポートする期間の基準として、2年という期間を設定しています。あらかじめこうした基準を設定していれば、都度判断に迷わずに済み、問題を先送りするのを防げます。
ただサポート打ち切りといっても、全く使えなくなるわけではありません。一部の設定値がデフォルト値に戻った程度の影響になるように配慮します。過去の仕様については長期間両対応などを受け入れる処理は残しますが、だからといってアプリの内部設計がそれに引きずられるわけにはいきません。
こうした問題にはアーキテクチャが役立ちます。アプリ内部と外部を分離し、過去の仕様のデータなどは変換のためのレイヤーを設けることで、アプリ内部に過去の仕様の影響を与えないようにしています。こうすることで、アプリ内部を健全に保つことができます。
その後は永続化データの設計や、プロトコルの設計段階で将来マイグレーションできる仕組みを組み込みます。例えば、プロトコルやデータにはバージョン番号を必ず付与することなどが考えられます。
アプリは長い寿命を持っており、更新を続ける必要があります。新しい仕様を考える際は、将来的に負債にならないだろうかと常に考え、負債になりにくい仕様を提案していきます。「今どう作るか」ばかりに焦点を当てて考えずに、将来のメンテナンスコストが高くならないように配慮しています。
コードの健全化として三つの観点から取り組みを紹介しました。コードが健全であるためには、健全な仕様である必要があります。また、使えるものは使って楽をするための苦労を惜しまず、効率良く開発できる環境を模索しています。1つのアプリを長く提供し続けるために行うことは、いずれも特別なことではありませんが、当たり前のことを当たり前に実行することで、コードを健全に保つことができると思います。
アーカイブ動画
こちらの記事のご感想を聞かせください。
- 学びがある
- わかりやすい
- 新しい視点
ご感想ありがとうございました