システム開発をしていて、こんなバグに遭遇したことはありませんか?

  • 「処理が完了したときにフラグを戻し忘れて、次回から処理が走らなくなった」
  • 「例外処理でキャッチした際、リセット処理がスキップされて状態がおかしくなった」

このような「フラグ(状態)の更新・リセット漏れ」は、実装時にどれだけ注意していても、コードの複雑化や将来の機能追加によっていつか必ず発生するバグの温床です。

先日、私が開発しているプロジェクトで、この 「フラグ管理の違和感」を、変数名とデータ設計の見直しだけで根本から解決できた事例 がありました。

今回は、実際の現場で何が起こり、どう修正し、そこからどのような設計の学びを得たのかをまとめます。

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
)

判定ロジック

「最終送信時刻が、最終活動検知時刻よりも新しければ、このセッションでは送信済み(ロック)」と判断します。

// 💡 最終送信時刻(lastSentTime)が、最終活動時刻(latestActiveTime)より新しければ、この最終活動時刻に対するSMSは送信済み
val hasSentForCurrentActive = lastSentTime >= latestActiveTime

// 48時間などの閾値を超えた場合、かつ、SMS未送信の場合
if (elapsedTime >= thresholdMillis && !hasSentForCurrentActive) {
    // SMS送信処理を実行...
    
    // 送信した「その瞬間のシステム時刻」をそのまま保存する(フラグ管理ではないため、追加のリセット処理は不要)
    saveLastSentTime(currentTime)
}

この修正により、以下のメリットが生まれました。

  • lastSentTime は「最後に送信した時刻」という 事実のみを表す名前 になり、迷う余地がなくなった。
  • ユーザーが活動を再開して latestActiveTime が新しく(現在の時刻に)更新されると、自動的に lastSentTime >= latestActiveTimefalse になるため、「送信済み状態をリセットするための処理(フラグのクリア)」が不要になった。

3. この設計から得た学び

今回のリファクタリングを通じて、今後に活かせる一般的な考え方をまとめます。

「状態(State)」ではなく「データ(事実)」を見る

  • 「送信済み / 未送信」というフラグは、人間がプログラムを制御するために後から作った 二次的な状態(State) です。
  • 一方で、「最後に活動検知した時刻」「最後に送信した時刻」は、システム内で実際に発生した揺るぎない 一次的な事実(Fact) です。

何かを制御しようとして「フラグを1つ追加しよう」と思いついたときは、一歩立ち止まるようにします。

「このフラグは、すでにシステム内にある他の『事実(データ)』の組み合わせや比較だけで、動的に導き出せないだろうか?」

事実同士を比較する設計にできれば、状態をパタパタと書き換える必要がなくなり、データの一貫性が保たれます。

この考え方は、 イベントソーシング とも呼ばれる考え方です。