Boolean に潰された「成功」 ― リファクタリングで気づいた抽象化の罠
はじめに
ViewModel の保存処理をリファクタリングしていたとき、 私は一度コードを壊しました。
原因は単純な「うっかりミス」に見えました。
しかし振り返ってみると、問題はもっと深いところにありました。
それは、
「成功」という概念を Boolean に潰してしまったこと
でした。
この記事では、その過程と学びを書きます。
元のコード
保存処理は、新規作成と更新で分岐していました。
private fun saveData() {
viewModelScope.launch {
val isExistingItem: Boolean = (取得)
if (destId == 0) {
// 新規作成
val resultSuccess = directDebitDefRepo.createDestination(...)
if (resultSuccess) {
// フォームの初期化
_formInputState.update { FormInputState() }
showSuccess()
} else {
showFailure()
}
} else {
// 更新
val resultSuccess = directDebitDefRepo.updateDestination(...)
if (resultSuccess) {
showSuccess()
} else {
showFailure()
}
}
}
}
ポイントはここです。
- create 成功 → フォーム初期化あり
- update 成功 → フォーム初期化なし
つまり、
create成功 ≠ update成功
でした。
リファクタリングでやったこと
私は「成功時の処理はほぼ同じ」と考え、共通化しました。
しかしその結果、
- create のときだけ必要だったフォーム初期化を消してしまいました。
そして動作が壊れました。
最初は「うっかり」だと思った
私はこう思いました。
create と update の違いを吸収しすぎた、単なるミスだ。
でも本当にそうでしょうか?
本当の原因
問題はここでした。
true = 成功
false = 失敗
Boolean は 2つの状態 です。
しかし実際のドメインはこうでした。
- CreatedSuccess
- UpdatedSuccess
- Failure
3つの状態があったものを、 Boolean に圧縮していました。
その瞬間に、
CreatedSuccess と UpdatedSuccess が同一化した
のです。
そして「成功は同じ」と思い込んでしまった。
抽象化の粒度が粗すぎた
今回の失敗はロジックの問題ではありませんでした。
抽象化の粒度の問題でした。
「成功」という言葉を、技術的成功(DB操作成功)として扱ってしまった。
しかし UI から見ると、
- 新規作成成功
- 更新成功
は意味が違う。
このズレがバグを生みました。
型で守る設計
もし最初からこうしていたらどうだったでしょうか。
sealed interface SaveResult {
data object Created : SaveResult
data object Updated : SaveResult
data object Failed : SaveResult
}
そして:
when (result) {
Created -> {
_formInputState.update { FormInputState() }
showSuccess()
}
Updated -> {
showSuccess()
}
Failed -> showFailure()
}
この設計では、
Created と Updated を無意識に同一視することはできません。
型が設計を強制します。
型が未来の自分を守ります。
学び
今回の経験から得たことは2つです。
- Boolean は情報を潰す
- ドメインの意味をそのまま型に表すと事故が減る
終わりに
今回のバグは、小さなものでした。
しかし、
抽象化は便利だが、意味を削りすぎると危険
という重要な気づきを得ました。
リファクタリングはコードを綺麗にする作業ではありません。
ドメインの意味を正しく表現し直す作業です。
そしてときどき、 型が私たちを守ってくれます。