STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

STORES 決済 Androidアプリの設計改善の歴史

f:id:n-seki:20211215163613j:plain

この記事は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を書きながら、少しずつ対応を進めていきました。

当時の状況

当時、クレジットカード決済の処理ではBroadcastAPI通信の結果を飛ばし、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開発を一緒に盛り上げていきましょう!

hello.hey.jp

*1:私が入社したのはコイニーという会社なのですが、気がつくとヘイが弊社になっていました