🐛

else ifが複数回出てきたら考えること

2023/12/13に公開

こんにちは!アルダグラムでエンジニアをしている@sohichiroです。

本記事は株式会社アルダグラム Advent calendar 2023 13日目の記事です。

前日は、@kageyama さんによる、SQLインジェクションの話でした。

セキュリティはいつも万全にしておく必要があるので、大変ためになる記事でした。

さて、隠れ関西民として関東地方に生息している私にとっては、翔んで埼玉〜琵琶湖より愛をこめて〜は、いろいろ込み上げてくるものがありました。確かに兵庫県は神戸と芦屋だけが飛び抜けて都会指数が高いですよ😂(私、兵庫県の田舎の方出身)。関西弁を勉強したいなら、格好の教材だと思いました。

else ifが複数回出てくることのつらみ

さて、else ifです。

else if。プログラマーであれば、一度は書いたことあると思います。

言語によっては、elsif(Ruby)、elif(Python)といった書き方もあったりします。

本稿で掲示しているコードは、私が最も馴染みのあるswiftで書いています。

まずはこちらから。

とび太[1]で満たされているか、田舎、、違った、否か。

if とび太で満たされている {
    print("そこは、滋賀")
}

特定の条件が満たされた場合にのみ実行する条件分岐を追加する構文、それが、if文です。

それに対して、if文の条件がfalseの場合に、if文の代わりに実行できる句を追加できるのがelse文です。

if とび太で満たされている {
    print("そこは、滋賀")
} else {
    print("そこは、滋賀やない")
}

だいぶ思想が入ったコードですが、サンプルコードなのでとりあえず話を進めます。

ifとelseは、安定する気がするんですよ、AかBかだから。

二者択一で収まっていることは結構あると思います。

例えば、こんな感じに

let dinnerMakable = false

let stomach: String
if dinnerMakable {
    stomach = "full"
} else {
    stomach = "empty"
}

晩御飯を作ることができれば、お腹は満たされる、作れなければ、お腹は空腹になる。

うん。

確かにこれ以上は、ロジック的な変更は、なさそうです[2]

では、処理結果が複数あるようなケースを考えてみましょう。

例えば、こんな感じ。

let dinner: String

let madeCurryYesterday = false
let foundDeliciousMeat = true

if madeCurryYesterday {
    dinner = "curry"
} else if foundDeliciousMeat {
    dinner = "yakiniku"
} else {
    dinner = "nothing"
}

昨日カレーを作っていれば、晩御飯は、カレー(二日目のカレーは美味しいですもんね)、美味しそうなお肉を見つければ、晩御飯は焼肉、それ以外は、晩御飯は、なし。

最後はelseで閉める、みたいなコードよくあると思うんです。

なんですが、else ifで条件繋げ出すと何故か3個4個と謎に条件が増えていくんですよ(私調べ)。

理由は謎なんですが。。

そこで、そのコードって本当にそれでいいんですか?安全なんですか??っていうのが本稿の趣旨です。

めっちゃマサカリ飛んできそうで怖いんですが。。

上記の例で言うと、あらかじめ条件を考慮できていない(整理できていない)ことも不安になる要因とは思いますが、まず、madeCurryYesterdayと、foundDeliciousMeatの関連性がなく、にもかかわらず、各々dinnerの状態に口を出してきています。

else ifで条件を追加していってる時点で、扱わないといけない条件を全て考慮できていますか?と言うところがelse ifと最後elseで閉めるコードの一番怖いところです。

またこのパターンになると、今はそんな感じですが、きっと、

let smellsEel = true

のようなフラグが追加されるケースもあるんじゃないかと思います。

そうすると、、サンプルコードは、以下のようになると思います。

let dinner: String

let madeCurryYesterdayFlg = false
let foundDeliciousMeatFlg = false
let smellsEelFlg = true

if madeCurryYesterdayFlg {
    dinner = "curry"
} else if foundDeliciousMeatFlg {
    dinner = "yakiniku"
} else if smellsEelFlg {
    dinner = "unaginokabayaki"
} else {
    dinner = "nothing"
}

プロダクションでは、今まで想定もしなかったような要件が追加されて、謎のフラグが増える、と言うことはよくあることだと思っています。

このようなif else if文(造語)が続いていくコードの恐ろしいところは、以下の三点かなと思います。

  • else if文を入れる箇所を見極める必要があり、コードの修正を誤ってしまう可能性がある
  • 全ての条件を網羅できているのかが、分かりにくい
  • 扱わないといけない条件が増えた場合でも、とりあえずはelse句の処理がコンパイルエラーなく実施されるので、対応もれに気づくことが難しい

簡単な例であれば、即座に分かるよ、、と言えると思いますが、人は間違えてしまうのです。おとなはウソつきではないのです。間違いをするだけなのです。

二つ目のものは本当に難しくて、全ての事象を、もれなくダフリなく考慮できているかを判断することは、単発のフラグが連続して存在しているだけだと本当に難しいです。それぞれの条件に関連性がないと、難易度はさらに増します。

三つ目も今はサンプルコードしかないので見つけるのは容易ですが、コードベースが大きくなるにつれ、見つける難易度は上がり、人間の目では、見逃してしまうことも多々あると思います。

それ、多分ケースがどんどん増えてくるケースだから、enum切って、全ての事象をカバーすることを考えた方がいいよ

上記のように、else ifで条件が二つ続いてしまった場合、私は、enumでまとめてしまえないかを検討します。

まず、一連のif else if文で決定したい対象=分岐の判断材料を検討します。

この場合は、dinnerに何を入れるか?と言うことなので、if else if文の中で使われている条件文は全て、dinnerに何を入れるかの判断材料となっています。

そのため、以下のようなenumを定義すると思います。

// 晩御飯を何にするか?
enum DinnerCondition:String, CaseIteratable {
    case madeCurryYesterday // カレーを昨日作った
    case foundDeliciousMeat // 美味しそうなお肉を見つけた
    case smellsEel      // うなぎの匂いがした
}

フラグ単体では別々の事象だったものが、enumにまとめたことで、考慮されている条件が見通せるようになりました。

これであれば、帰宅の道すがら、美味しそうな焼き鳥屋さんを見つけた時にも容易に判断材料が追加できそうです。

次に、どの条件が満たされているかを確認するロジックを組みます。

if else if文は、いずれかの句一つだけが実行されるロジックになっています。

ですので、enumの定義された値のうち一つだけ満たされているものがあるはずです。

条件が満たされた判断材料を見つけてくれば良いので、、

let madeCurryYesterdayFlg = false
let foundDeliciousMeatFlg = false
let smellsEelFlg = true

let dinner: String

let condition = DinnerCondition.allCases.first { condition in
    switch condition {
    case .madeCurryYesterday:
	return madeCurryYesterdayFlg
    case .foundDeliciousMeat:
        return foundDeliciousMeatFlg
    case .smellsEel:
        return smellsEelFlg
    }
}

if let condition = condition {
  dinner = condition.rawValue
} else {
  dinner = "nothing"
}

と言った感じに、enumをCaseIterableに適応させておいた上で、全てのケースのコレクションから、判断条件が満たされた場合にのみtrueとなるような処理を、全ての判断条件で、もれなく定義できるように、switch文を使って、判断条件が満たされているかを判断するロジックを定義します。

swiftの場合、switch文においては、全てのケースを網羅する必要があり、網羅されていない場合は、コンパイルできないため、機械的に判断条件の抜けを見つけることができます[3]

上記のように、enumとswitch文を使うことで、if else if文で条件を繋げていくより、コンパイラの助けを得て、条件に漏れがないかを確認しやすくできた、と思っています。

まとめ

本稿においては、else ifで句を繋げていくケースにおいては、抜け漏れが発生しやすく、危険なので、判断条件をenumにまとめたうえで、switchを利用して評価することで、考慮もれを防ぎやすくする書き方を提案しました。

本稿での提案は、switch文が、全ての条件を網羅できていなければ、コンパイルエラーになるswiftの言語仕様に依っているので、他の言語においても適用できる、と言うわけではないのですが、判断材料を1箇所にまとめる、と言う考え方は、他の言語にも流用できるのではないかと思っています。

未来がどうなるかは分かりませんが、どのようなことになっても不具合を起こさないような安全なコードを書いてきたいですね。

脚注
  1. 翔んで埼玉におけるとび太の活躍は凄かったですね。 ↩︎

  2. 三項演算子で書いた方が綺麗になりそうなサンプルですが、とりあえずはif elseに焦点を絞っています。 ↩︎

  3. switch文においては、明示的に対処されていないケースをカバーするdefaultケースを利用することができますが、defaultケースを利用すると、ケースの網羅もれが発生するので、私は極力利用しないようにしています。 ↩︎

アルダグラム Tech Blog

Discussion