この記事はhey Advent Calendar 2021 の17日目の記事のうちの1つです。
はじめに
こんにちは。Androidエンジニアをしている@n_seki_です。STORES 決済 のAndroidアプリ/SDKの開発をしています。先月で入社4年目に入りました。
振り返ると、この3年間いろいろな設計変更・リファクタリングを行ってきたなーと感じています。そこでこの記事では、これまで行ってきた設計の改善対応から3つピックアップして、STORES 決済 Androidアプリの設計・実装がどのように変わってきたのかの歴史を振り返ってみます。
おしながき
ContentProvider
からRoom
への移行- Repositoryパターンへの移行
- クレジットカード決済処理の設計改善
大きめだったこれら3つの対応をダイジェストで振り返ってみます。
ContentProviderからRoomへの移行
データの永続化で利用していたContentProviderからRoomへの移行対応です。
Android Developersにも記載されているように、自アプリでのみ利用し、かつ特殊な要件もない場面においては ContentProvider
ではなく他のAPIの利用が推奨されます。
STORES 決済 においても Room
で十分に要件が満たせるので移行作業を行なっていました。
当時の状況
STORES 決済 アプリでは通信の結果を永続化することで、例えばオフライン時にも売上の一覧が参照できるなどユーザビリティを考慮した設計になっています。
私がヘイ*1にジョインしたとき、永続化処理では ContentProvider
をゴリゴリに使っている状況でした。間もなくして同僚がRoom
で全テーブルの定義を書いてくれたり、一部の機能ではRoom
を使うようになりました。しかし依然として「永続化処理といえば ContentProvider
」な状況が続いていました。
実装上はContentProvider
をラップして使いやすいようになっていたため、機能追加では大きなペインはありませんでしたが、
- バグがあったときなどに
ContentProvider
まわりの実装を読んだり修正したりするコストが大きい - 一部処理で
ContentProvider
へのアクセスがUIスレッドから行われていた
という課題がありました。
やったこと
Room
に移行しました。上述のとおり、テーブル定義(@Entity
が付いたクラス)は出揃っている状態だったのでDao
を書いて、ContentProvider
のクエリを1つ1つ置き換えて行きました。
当時テーブルのPrimary Keyに設定されていたカラムの都合で OnConflictStrategy
の指定でハマったりしましたが、それ以外は大きな問題なく対応できたと記憶しています。
Dao
に対してUnitTestも書いたりとRoom
全般の知識を強化でき、個人としても良い経験となりました。
余談ですが、このとき Room
に触れたおかげもあり Room
は好きなライブラリの1つになっています。
その後
Room
移行できたおかげでKotlin Coroutines Flowなども積極的に使えるようになり、できることの幅が広がりました。以前は一覧系の画面ではLoaderが使われていましたが、Room
と Kotlin Coroutines Flow、 LiveData で書き換えたりしました。
またRoom
になったことで、上述したI/OをUIスレッドで行なっている箇所の対応も比較的容易になりました。
......しかし実はまだ一部の参照系の処理でContentProvider
が残っているため、はやくRoom
に完全移行しないとな、とソワソワしているところです。
Repositoryパターンへの移行
以前書かせていただいたSTORES 決済 開発事例紹介 〜決済サービスを支えるAndroidアプリの裏側〜にて、アプリの設計について「データ層ではRepositoryパターンを用いてAPI通信、永続化などの処理を隠ぺい」と記載していましたが、最初からこのような綺麗な設計だったわけでなく、わりと最近になって設計変更がなされたものでした。
当時の状況
Repository
, UseCase
という名の付いたクラスは存在していましたが、データの取得と永続化についてはこのような責務になっていました。
クラス | 責務 |
---|---|
Repository | 通信ライブラリ Retrofit を実行してレスポンスを返す |
UseCase | Repositoryから受け取ったレスポンスを永続化する |
つまりRepositoryは「API通信、永続化などの処理を隠ぺい」していない設計となっていました。
「Repositoryパターン」という命名から想起される設計とは異なっており、当時モヤモヤしていたことを覚えています。また、これからジョインするメンバーが混乱する懸念も考えられました。
やったこと
まず最初に、Recommended app architectureで紹介されているパターンによって既存のRepositoryの1つを書き換え「Repositoryパターンって本来こういう形だと思うのだけど......」とPullRequestを作成しチーム内でレビューをしました。合意が取れたので、すべてのRepositoryをRepositoryパターンで再実装、という流れでした。
対応自体は機械的なものだったので、とくに躓くことなく進めることができました。最初に小さいPullRequestでチームが同じ方向を向けた、ということも良かったのかなと思っています。
その後
「Repositoryパターン」と聞いて誰もが想像する設計となり、チーム開発がやりやすくなったと感じています。個人的には、やってよかった設計変更 No.1 です。
STORES 決済 開発事例紹介 〜決済サービスを支えるAndroidアプリの裏側〜で紹介したデータ層の設計はこのような背景で生まれたのでした。
クレジットカード決済処理の設計改善
クレジットカード決済は STOERS 決済 おけるコア機能です。当時から課題があったこと、また将来的な機能追加を見据えて設計を変更する判断をしました。安定性が特に求められる機能であるため、可能な限りUnitTestを書きながら、少しずつ対応を進めていきました。
当時の状況
当時、クレジットカード決済の処理ではBroadcastでAPI通信の結果を飛ばし、Activity
が持つ決済状態を更新していく.....という設計で実装されていました。
この設計が改善すべき課題であることはチームの共通認識だったので、まずは具体的なペインポイントを挙げて、設計を考えることにしました。
当時挙げられた課題
チームでは以下のような課題を感じていました。
Broadcast
がどこから飛んでくるのか処理が追いにくいBundle
にデータを詰めることになるが、キャストに起因する実行時エラーが容易に発生しうる- 状態管理行をなっている
Activity
が肥大化している(UIの責務を逸脱している)
やったこと
チーム内で検討した結果、Flux を参考にした設計で書き換えることにしました。
データのやり取りには Broadcast
ではなくRxJava
を利用することになりました。今でしたら Kotlin Coroutines Flow という選択肢もありますが、対応当時はリリースされて間もない頃だったため、安定性を考慮してRxJava
を選択した背景があります。
また、Flux
を参考にしたのは「状態を保持する」「状態を変化させる」「状態を通知する」役割を明確に分離したいという意図がありました。
データ層からUIまで手を入れる大掛かりな設計変更でしたが、p-rを分けて小さく進めていきました。
設計変更やテストの過程でクレジットカード決済の仕様の理解はかなり深まりました。実装の過程で既存処理の問題点にも気が付けたりしたのも良かったです。
その後
Broadcast
から脱却してデータの流れを整えたことで、決済周りの機能改善・追加も格段にやりやすくなったと感じています。
さらにBundle
から、通常のKotlinのdata classで型安全にデータをやりとりできるようになって平和が訪れました。
実は(別の処理の)設計の都合で、当時やりたかったことの半分しかできていない状態なので、今後は設計を一歩進めて、インタフェースもRxJava
ではなくKotlin Coroutinesに置き換えたいなと思っています。
さいごに
3つの設計変更・リファクタリングをダイジェストで振り返ってみました。今振り返っても「どれもやってよかった対応だった」と感じています。
歴史のあるアプリにおいては、設計は一朝一夕で綺麗になるものではなく、長い時間の中での改善の積み重ねだと考えています。 改善したいポイントや課題はまだまだあるので、機能開発の傍らでコツコツと改善を継続していきたいです。
ヘイでは複数のサービスでAndroidエンジニアを積極的に募集しています。ヘイでのAndroid開発を一緒に盛り上げていきましょう!
*1:私が入社したのはコイニーという会社なのですが、気がつくとヘイが弊社になっていました