2024年 Android のリファクタリングにおいて合意を助けた ADR 5選

こんにちは!kintone 開発チームの Android エンジニア、トニオ(@tonionagauzzi)です。
本日は、Android Advent Calendar 2024 の記事として発信します!

私たちは現在、kintone の Android アプリを継続提供し、かつプロダクト価値を高めていくためにリファクタリングを行っています。その際、ADR を起票することでチーム内でリファクタリングの合意形成を取っています。
この記事では、ADR とは何か?という話と、今年書いてよかった ADR を5つ紹介します。

1. ADR とは

ADR (Architecture Decision Records) とは、アーキテクチャに関する重要な意思決定を記録するための文書です。ADR を作成する目的は、開発において適切な技術的選択をできるようにしたり、過去の経緯を知れるようにすることです。ADR は決定の詳細に加えて「なぜその決定が行われたのか」「どういった背景があったのか」を説明する内容になっています。
ADR について詳しく知りたい場合は、ソフトウェアアーキテクチャの基礎の19章を読むことをおすすめします!

私たちが使っている ADR のフォーマットは、次のセクションに分かれます。

  1. タイトル
  2. ステータス
  3. コンテキスト
  4. 決定
  5. 影響
  6. コンプライアンス

1-1. タイトル

簡潔なタイトルを書きます。タイトルが長すぎると要点を掴みにくく、他の ADR と並んだときに見つけにくくなります。ですが、簡潔すぎるのも良くなく、意思決定の内容をイメージできる程度に具体的であるのが望ましいです。決定を把握しやすいよう、「〜について」ではなく「〜する」のような表現を用います。

1-2. ステータス

最新のステータスを書きます。ステータスは 執筆中(Draft)提案中(Proposed)採用(Accepted)不採用(Rejected)廃止済み(Deprecated) から選びます。ステータスは ADR が現在も運用されている決定なのか、過去の意思決定がどのように更新されてきたのかを追跡するために必要です。

1-3. コンテキスト

意思決定が下された背景や文脈について書きます。「何が問題だったのか」「何を解決しようとしていたのか」を明確にし、読み手が決定を理解することを助けます。また、決定に至るプロセスを記録することで、決定に対する信頼を構築し、決定の正当性を示します。

1-4. 決定

意思決定そのものについて書きます。肯定的かつ命令的な文章で、読み手を考慮した簡潔な言葉を用いて書きます。

1-5. 影響

意思決定による良い影響と悪い影響を書きます。選ばなかった別の選択肢についても書く場合があります。詳細に書くことで「どのような影響を想定していたか」を明確にし、読み手が意思決定の妥当性や必要性を理解することを助けます。

1-6. コンプライアンス

決定が守られるための手段を書きます。手段としてレビューを挙げることが多いです。コンプライアンスを書くことで、成果物が決定に基づいて作られることを助け、チームや組織における責任の所在が明確になります。

2. Android のリファクタリングにおいて私たちの合意を助けた ADR 5選

5つ紹介します。ステータスはすべて現時点で「採用(Accepted)」です。

2-1. モジュールの疎結合化によるマルチモジュールのメリットの享受

コンテキスト

モジュール同士が密結合していると、以下のデメリットがある。

  • 保守容易性が低下する(とくに再利用性とテスト容易性)
  • 柔軟性と拡張性が低下する

決定

モジュールの疎結合化を行い、サイクルタイムに影響する前述のデメリットを解決する。方針は以下の通り。

  1. ある機能のドメイン層やデータ層から、他の機能のドメイン層やデータ層を参照しない
    → 依存関係はUI層がDIやViewModelFactoryを使って解決する
  2. 共通モジュールを参照する場合は、極力データ結合とする
  3. 異なるレイヤー間でも外部結合以上の結合度は避ける
    → Repositoryの実体をUseCaseに渡すなどでスタンプ結合や制御結合にはならざるを得ない

影響

メリット

各モジュールの保守容易性、柔軟性、拡張性が上がる。

デメリット

意図せずインスタンス寿命が変わる可能性がある。たとえば、元々参照型で保持していたものをリファクタリングでプリミティブ型で渡すようにした場合、処理のスコープを抜けた瞬間に参照が失われてしまい、意図しない例外発生を招く可能性がある。
コードレビューおよび機能試験で品質担保する。

コンプライアンス

コードレビューにより本決定の遵守を確認する。

2-2. RxJava→Coroutinesによる保守容易性の向上

コンテキスト

RxJavaを使用しており、データの流れが追いづらい、ライフサイクルに合わせて適切に破棄しなければならないなど、保守容易性が低下している。
また、サーバー通信のレスポンスをRxの機能で受けることが前提となっており、レスポンスを受けた後、UIに伝達するロジックのコード量が多くなる傾向がある。

決定

RxJavaを廃止し、Kotlin Coroutinesに書き換えることで、保守容易性を上げる。利益は以下の通り。

  1. Kotlin Coroutinesはデータの流れがRxJavaと比較してわかりやすい
    → suspend関数を使えば、データの待ちが発生する非同期処理も同期処理と同じように短くまとまったコードで書ける
  2. Androidのライフサイクルに合った実装ができる
    → suspend関数をActivityやViewModelのLifecycleScopeで実行することで、画面が破棄されたら処理を止めることが簡単にできる
  3. Composableで非同期処理を扱う際も、標準の変換オペレーターが豊富にあるCoroutinesを使うのが理想的
    androidx.compose.runtime.rxjava3を使いRxからStateなどに変換することはできるが、純Coroutinesの実装よりは認知負荷が上がるため、RxとCoroutinesの併用は推奨しない
  4. Androidエンジニアの多くが持っているスキルのため可読性や学習容易性も上がる

影響

メリット

保守容易性が上がる。

デメリット

意図せずデータフローが変わる可能性がある。たとえば、RxJavaのDisposableで非同期処理の破棄をしていたところをCoroutinesのCoroutineScopeに変えたときに破棄されるタイミングが変わり、意図しない例外発生を招く可能性がある。
コードレビューおよび機能試験で品質担保する。

コンプライアンス

コードレビューにより本決定の遵守を確認する。

2-3. URLを扱う際はCBMURLではなくStringやUriを使う

コンテキスト

CBMURLとは、サイボウズ社内で使われているユーティリティクラスで、URLをベース URI、スキーム、パス、およびクエリに構造化して操作する機能を提供する。
CBMURLを使うとURLを扱いやすくなるが、以下のようにインスタンス作成毎にNullチェックが必要で、新しいメンバーがCBMURLの仕様に慣れる必要がある。

CBMURL.create(url = url)?.let { cbmUrl ->
    // URLを扱う処理
}

決定

URLの表現には、Stringもしくはandroid.net.Uriを用いる。
ユーザー入力や動的に生成されるURLの処理、画面間のURL受け渡しにはStringを用いる。
URLからホストやスキーム、パス、クエリを取り出したり、URLを組み立てたりする際にはandroid.net.Uriを用いる。

url.toUri() // URLを扱う処理

代替案

Android開発において、他にURLを保持する方法にはjava.net.URLとjava.net.URIがある。
それぞれのクラスは異なる特徴を持ち、どれを使用するかは用途によるが、Android SDKを用いるモジュールであえてjava.net.*を選ぶ理由はないと考える。

コンプライアンス

コードレビューにより本決定の遵守を確認する。

2-4. 画面固有のエラーハンドリング

コンテキスト

アプリ内で発生したエラーが、Model経由でMainActivityで処理されていた。詳細は「2-5. アプリ全体のエラーハンドリングの廃止」のコンテキストを参照。
そのような実装になった理由の1つとして、特定の画面でのみ起きる以下のような処理の取り扱い方法が決まっていなかった。

  • 通知一覧画面で通知の取得に失敗した
  • ファイルプレビュー画面でファイルの取得に失敗した

決定

特定の画面でのみ発生するエラーは、その画面で独自にエラーハンドリングとエラー表現をする。

  1. UseCaseでエラーを捕捉し、呼び出しにエラーを伝える(Resultパターンなど)
  2. ViewModelでエラーが起きたことをUI状態に表現する
  3. UI側でエラー状態を受け取った場合、適切なエラー表現を表示する

コンプライアンス

コードレビューにより本決定の遵守を確認する。

2-5. アプリ全体のエラーハンドリングの廃止

コンテキスト

画面固有でないエラーが30か所以上で発生する。画面固有でないエラーはModelにイベントが飛び、MainActivityが処理している。この仕組みのまま開発を続けると、保守容易性が低下する。

  1. エラー処理がMainActivityにあると、再利用性とテスト容易性が低下する
  2. Modelを使う前提なので他アプリに処理を移植しづらく、再利用性が低下する

決定

「2-4. 画面固有のエラーハンドリング」に従い、複数の画面で発生するエラーも画面別にエラー処理を実装する。
MainActivityでのエラー処理、Modelは廃止する。

コンプライアンス

コードレビューにより本決定の遵守を確認する。

3. 課題

いかがだったでしょうか。一見、しっかり決めてリファクタリングに臨んだように感じますが、これらのADRには課題もあります。

たとえば、コンプライアンスの確認手段がレビューしかないことです。レビューは強力ですが、人力なので違反の見落としや作業時間の増加がしばしば問題となります。もし、アーキテクチャ適応度関数などに基づいたアーキテクチャ決定違反の自動検出ツールを導入することができれば、より低コストかつ安全にコンプライアンスを遵守できるかもしれません。

また、2-4や2-5はチーム内では説明するまでもなく既知の問題だったため、影響や代替案の記述を省略しましたが、今後チームに新しく来た人がADRを読むことを考えれば、きちんと影響や代替案を書き残しておくべきという意見もあります。

最適なADRは取り扱う内容の性質や状況によるので、どれが正解とは言い切れませんが、私たちは常に学びながら開発プロセスを改善しています。ADRを用いた意思決定について、引き続き探究を続けていきたいと思います!

4. まとめ

kintone Android のチーム開発では、1つ1つの意思決定をきちんと合意して先に進むことを大事にしています。
ADR は、迷いやすい意思決定をスムーズに進めたり、チーム全体で共通理解を深めることを助けてくれました。ADR が無かったら、「なぜそのような決定をするのか(したのか)?」という話し合いにもっと時間がかかっていたと思います。
また、過去の決定を参照可能にしたことで、似たような意思決定の速度を上げたり、新しいメンバーのオンボーディングもスムーズになったと感じています。

今年はモジュールの疎結合化、RxからCoroutinesへの書き換え、URLを扱う方法、エラーハンドリングの方法についてADRを書いて合意形成を行いました。
来年も引き続き、プロダクトの価値を高める意思決定を重ねていきたいと思います!