はじめに
Androidアプリ(Kotlin + Jetpack DataStore)で見守りチェック機能を実装していた際、バックグラウンドでの判定用データと、画面に表示する「チェック履歴」の持ち方について設計を見直す機会がありました。
最初は「既存の判定用モデルをそのままリスト化して履歴として保存すればいいのでは?」と考えていたのですが、モデルを分離(SafetyData と MonitoringHistoryLog)する設計を検討・議論する中で、「単一責任の原則」をどう実務に活かすかについての腹落ちが得られたのでブログにまとめます。
発生した課題・問い
見守りアプリにおいて、以下の2つのデータを管理する必要がありました。
- システムの最新ステート:前回のバッテリー残量、前回のチェック時刻、現在機能が有効かどうか等(次回チェック時の差分判定に使用)
- 時系列の履歴ログ:いつチェックが走り、活動が検知されたか(履歴画面でタイムライン表示に使用)
当初は、1の最新ステートを表す SafetyData をそのまま List<SafetyData> としてDataStoreに保存すれば、クラスも増えずシンプルで良いのではないかと考えました。
しかし、このアプローチにはいくつかの懸念点がありました。
なぜモデルを分けるべきなのか?(2つの理由)
議論を通じて整理された「モデルを分けるべき理由」は以下の通りです。
1. 「最新状態(ステート)」と「過去の出来事(ログ)」はライフサイクルが違う
- ステート(
SafetyData): 次回のチェック判定まで保持されればよく、常に新しい値で上書きされるデータ(生存期間:短)。 - ログ(
MonitoringHistoryLog): 過去の記録として蓄積され、ユーザーに見せるためのデータ(生存期間:長)。
寿命(ライフサイクル)や保存方式(上書き vs 追記)が異なるデータを1つのモデルに詰め込むと、データの読み書きや更新処理が無駄に複雑化します。
2. 不要なプロパティの持ち回りを防ぎ、責務を分離する
SafetyData には isMonitoringEnabled(機能全体のON/OFF設定)などの設定項目も含まれています。これを履歴として毎件保存すると、データサイズが無駄に肥大化するだけでなく、「履歴画面を表示したいだけなのに、無関係な設定データまで持ち回る」 ことになります。
専用の軽量モデル MonitoringHistoryLog を用意することで、データサイズを削減し、履歴画面側が不要なプロパティを気にしなくて済むようになります。
// 1. システム内部の最新判定用モデル(ステート)
data class SafetyData(
val lastBatteryLevel: Int?,
val lastActiveTime: Long?,
val isMonitoringEnabled: Boolean, // 履歴ログには不要な設定データも含まれる
// ...
)
// 2. 履歴画面・記録用の専用モデル(ログ)
@Serializable
data class MonitoringHistoryLog(
val id: String = UUID.randomUUID().toString(),
val executedAt: Long, // 実行時刻
val isActivityDetected: Boolean // 活動検知があったか
)
単一責任の原則に立ち返る
これまでプログラミングの本などで 「単一責任の原則=クラスを変更する理由を1つにせよ」 という解説を読んでも、イマイチピンと来ていませんでした。しかし今回の設計を通じて、その捉え方がガラッと変わりました。
「変更理由が複数ある=責任が混在しているサイン」
「変更理由を絶対に1つにしなければならない」という絶対ルールとして覚えるよりも、「変更理由が複数見つかるなら、責任が混在している可能性が高いから、もっと細かく分割できるかも?」と気づくためのセンサーとして捉える方がずっと実践的で覚えやすいと感じました。
今回のケースで言えば:
- 「履歴画面の表示項目を変えたいとき」
- 「バックグラウンドの判定ロジックや保持方法を変えたいとき」
という2つの変更理由が1つのモデル(SafetyData)に同居しそうになった時点で、「あ、これは責任が混ざっているな」とセンサーが働き、モデルを分ける判断ができました。
1箇所でモデルを変換すれば、影響範囲は閉じられる
UseCase(またはMapper)という「変換所」を1箇所設けて、生のデータから「ホーム画面用(ステート)」と「履歴画面用(ログ)」にモデルを変換して渡してあげる。
そうすることで、「変換所から先のコードは、自分が使わないプロパティのことを考える必要がなくなる」 という大きなメリットが生まれます。
- 履歴画面の改修:
MonitoringHistoryLogだけを触ればよく、判定ロジックを壊すリスクはゼロ。 - 判定ロジックの改修:
SafetyDataや UseCase の中身だけを触ればよく、画面側を壊すリスクはゼロ。
おわりに
設計における「モデルの分離」は、単にファイルを増やす作業ではなく、「責任の混在に気づき、影響範囲を安全に閉じ込めるための手段」 だと改めて実感しました。
「単一責任の原則」を完璧なルールではなく「設計の違和感を検知するセンサー」として手元に持っておきながら、今後の開発でも影響範囲が明確で保守しやすいコードを目指していきたいと思います。