モデルを分割した話

はじめに 発生した課題・問い なぜモデルを分けるべきなのか?(2つの理由) 1. 「最新状態(ステート)」と「過去の出来事(ログ)」はライフサイクルが違う 2. 不要なプロパティの持ち回りを防ぎ、責務を分離する 単一責任の原則に立ち返る 「変更理由が複数ある=責任が混在しているサイン」 1箇所でモデルを変換すれば、影響範囲は閉じられる おわりに はじめに 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つにせよ」 という解説を読んでも、イマイチピンと来ていませんでした。しかし今回の設計を通じて、その捉え方がガラッと変わりました。 ...

2026年10月1日 · 1 分 · 奥田 智紘

「状態(フラグ)」を捨てて「事実」で判定する

1. 現場で起きたことと「フラグの違和感」 最初に頭をよぎった設計 感じた違和感 2. どう修正したか? データの定義 判定ロジック 3. この設計から得た学び 「状態(State)」ではなく「データ(事実)」を見る 【追記】二次的なデータでも問題ない例 システム開発をしていて、こんなバグに遭遇したことはありませんか? 「処理が完了したときにフラグを戻し忘れて、次回から処理が走らなくなった」 「例外処理でキャッチした際、リセット処理がスキップされて状態がおかしくなった」 このような「フラグ(状態)の更新・リセット漏れ」は、実装時にどれだけ注意していても、コードの複雑化や将来の機能追加によっていつか必ず発生するバグの温床です。 先日、私が開発しているプロジェクトで、この 「フラグ管理の違和感」を、変数名とデータ設計の見直しだけで根本から解決できた事例 がありました。 今回は、実際の現場で何が起こり、どう修正し、そこからどのような設計の学びを得たのかをまとめます。 1. 現場で起きたことと「フラグの違和感」 開発していたのは、 「ユーザーの安否(バッテリー消費や充電器の抜き差しなどのイベント)を監視し、一定時間(例:48時間)活動が検知できない場合に自動でSOSのSMSを送信する」 という機能です。 このとき、一番気をつけなければいけないのが 「SMSの二重送信(送りすぎ)」 の防止でした。 最初に頭をよぎった設計 「一度SMSを送信したら、次にユーザーがスマホを触る(アクティブになる)までは再送を防止したい」と考えたとき、真っ先に思い浮かびがちなのが以下のようなフラグ管理です。 SMSを送信したら、isSmsSent = true にする。 ユーザーの活動を検知したら、isSmsSent = false にリセットする。 感じた違和感 一見シンプルですが、この「フラグをパタパタ切り替える設計」には、以下のような不穏な未来が目に見えていました。 「いつ、どのタイミングでこのフラグをリセット(あるいは更新)すべきなのか」をあちこちの処理で気にしなければならない。 将来、別のユースケースや例外処理が追加されたとき、 「フラグのリセット漏れによるバグ」 を引き起こす予感がする。 数ヶ月後にコードを見返したとき、「このフラグってどの状態を指してるんだっけ?」と認知のコストがかかる。 「フラグを使わずに、もっと自然な形でキレイに判定できる方法はないだろうか?」と考えました。 2. どう修正したか? フラグで「送信済み / 未送信」という状態を管理する方法は避け、 「最後に活動を検知した時刻(lastActiveTime)」と「実際にSMSを送信したシステム時刻(lastSentTime)」という、2つの「事実(時刻)」を比較する設計 に辿り着きました。 これにより、判定はシンプルな 「不等式(時間の新旧比較)」 へと進化します。 データの定義 データクラスには、フラグではなく「最後に送信した実際のシステム時刻」を持たせます。 data class AlertConfig( val id: String = UUID.randomUUID().toString(), val thresholdHours: Int, val targetContactIds: List<String>, // 💡 この設定で最後にSMSを送信した「実際のシステム時刻」 val lastSentTime: Long? = null ) 判定ロジック 「最終送信時刻が、最終活動検知時刻よりも新しければ、このセッションでは送信済み(ロック)」と判断します。 ...

2026年7月17日 · 1 分 · 奥田 智紘

リファクタリングには二種類ある

リファクタリングには二種類ある ― 構造を変えるものと、概念を変えるもの まず整理:構造と概念の違い 構造の変更 概念の変更 なぜこの区別が重要なのか? なぜ通常は「構造 → 概念」なのか? ① 変更の安全性 ② デバッグのしやすさ ③ 複雑性の増幅 実務での使い分け(私の優先順位) 第1段階:可視化 第2段階:構造整理 第3段階:責務の明確化 第4段階:概念強化 まとめ リファクタリングには二種類ある ― 構造を変えるものと、概念を変えるもの 私は最近の失敗から、大きな学びを得ました。 それは、 リファクタリングには二種類ある ということです。 構造 を変えるリファクタリング 概念 を変えるリファクタリング この区別を意識していなかったことが、バグの原因でした。 この記事では、自分なりに整理した実務的な優先順位と共にまとめます。 まず整理:構造と概念の違い 構造の変更 関数抽出 共通化 重複削除 名前変更 ネスト解消 責務分離 振る舞いは変えない(意味は同じ) これは「コードの形」を整える作業です。 概念の変更 Boolean → sealed class nullable → 非null設計 エラー型明確化 状態遷移の型化 エンティティ再設計 ドメインモデル再定義 責務の再解釈 プログラムが表現する「意味」が変わる これは「コードが何を表現しているか」を変える作業です。 なぜこの区別が重要なのか? 私は「成功」という概念を Boolean で扱っていました。 しかし実際には: 作成成功 更新成功 失敗 という複数の意味がありました。 Boolean に押し込めたことで、 意味の差が見えなくなり、無意識に共通化してしまった のです。 ...

2026年3月4日 · 1 分 · 奥田 智紘