送信確認のメールが届きます。
お問い合わせ内容に応じて、24〜72時間以内に担当者よりご連絡いたします。
送信することで、当社の【プライバシーポリシー】および、Open Reach Techからのメール受信に同意したものとみなします。
プライバシー同意チェックボックスを選択してください。

components..title

components..description

components..title

components..description

送信確認のメールが届きます。
お問い合わせ内容に応じて、24〜72時間以内に担当者よりご連絡いたします。
送信することで、当社の【プライバシーポリシー】および、Open Reach Techからのメール受信に同意したものとみなします。
プライバシー同意チェックボックスを選択してください。

バグの減らし方: if文のネスト禁止

Eucen Stewのプロフィール写真
Eucen StewFull-stack Developer

ORTで導入して効果を得ている「バグの減らし方シリーズ」の第一弾。「if文のネスト禁止ルール」について解説します。

Banner of バグの減らし方: if文のネスト禁止

概要

プログラマーの永遠の課題のひとつは「如何にしてバグを減らすか?」です。

この記事では、弊社で導入している「コーディングスタイルによりバグを減らす方法」から、「if文のネスト禁止」ルールを紹介します。

動機

筆者がSNSで発信した「弊社ではif文のネストが禁止されています」について、大きな賛否がありました。

弊社の開発においては、バグの減少やレビュー効率の改善といった効果を実感しています。しかし、SNSのショートメッセージではそれらを書き尽くせません。

SNSで書き切れない部分について紹介しようと考えたのが、この記事を書く動機でした。

技術選定

弊社は、バックエンド・フロントエンドともにTypeScriptではなくJavaScriptで開発しているので、サンプルコードは基本的にJavaScriptで書いています。

if文のネスト構造は多くのプログラミング言語と共通しているので、バグの減らし方の参考になると思います。

コーディングスタイル

弊社のコーディングスタイルでは、if文のネストが禁止されています。

else if はネストしたifの糖衣構文なので、これも禁止です。

以下のふたつは同じロジックです。

if (alphaCondition) {
  // ...
} else if (betaCondition) {
  // ...
}
if (alphaCondition) {
  // ...
} else {
  if (betaCondition) {
    // ...
  }
}

バグを減らすための指針

バグを減らすのに最も有効なのは、「バグが出る可能性を減らすこと」です。

「家庭内害虫を減らすには、害虫が隠れるスペースをなくせばよい」のと似ています。

バグが潜みやすい構造を排除すれば、バグの可能性は減る道理です。

複雑なロジック

筆者が数年前にSNSで聞いた言葉です。

全てのバグはif文により発生するのではないか

処理分岐の条件やタイミングを間違うからバグになるという理由です。

これを読んで「それはもっともだ」と感じたのを、数年たった今も覚えています。

if文のネスト

if文を巧みに組み合わせた複雑なロジックは、プログラミングの初心者によってよく書かれます。

if文のネストは、それが2段であっても簡単に複雑な構造になります。

重ねた段数により指数関数的に複雑化すると考えればちょうどよいでしょう。

バグの減らし方

先程の「バグを減らすための指針」と「if文をネストさせると複雑化する」を合わせると、以下の考え方が得られます。

if文をネストしなければバグが減る

バグを減らすには、複雑なロジックを書かせないことが重要です。複雑なロジックを書かせないためには、if文のネストを禁止すればよいという理由です。

レビューコストの軽減

if文のネストを禁止するのはいいですが、入社したてのメンバーやジュニアメンバーによりネストしたif文がプルリクエストで提出されることは予想できます。それらをレビューで指示するコストは見過ごせません。

弊社ではこの課題をLintによって解決しています。

弊社は前述の通りJavaScriptで開発しているので、ESLintを使ってif文のネストを禁止しています。

ESLintの指定方法

「if文のネスト禁止」をESLintで指定するのは簡単で、標準のルール内でできます。

// eslint.config.js

export default {
  rules: {
    'no-restricted-syntax': [
      'error',
      {
        selector: 'IfStatement IfStatement',
        message: 'Never use nested-if including else-if',
      },
    ],
  },
}

ネストしたif文をフラットにするテクニック

ネストしたif文をフラットにするには、以下のテクニックを使います。 これらの方法を使えば、どれだけ複雑なネストであってもフラットにできます。

  1. 早期リターン(ガード節)
  2. 内側のif文を外側に出す
  3. ド・モルガンの法則
  4. メソッドや関数への切り出し

リファクタリングの実例

「指定した年月日の曜日を表示する機能」を例にして、ネストしたif文をフラットにする方法を紹介します。

仕様

  1. 指定した年月日の曜日として表示する文字列を決定する。
  2. 祝日の場合はHolと表示する。ただし、その祝日が日曜の場合はSunと表示する。
  3. 平日は、その曜日をMon~Satで表示する。
  4. 振替休日はSubと表示する。

サンプルコード

ネストしたif文を最大に使って実装した例です。

最深部は8重になっています。

function resolveDayString (date) {
  const baseDay = resolveBaseDayString(date) // Mon ~ Sun

  if (isHoliday(date)) { // (1)
    if (isSunday(date)) {
      return 'Sun'
    } else {
      return 'Hol'
    }
  } else {
    if (isSunday(date)) { // (2)
      return 'Sun'
    } else {
      const oneDayAgoDate = createYesterday(date)

      if (isHoliday(oneDayAgoDate)) { // (3)
        if (isSunday(oneDayAgoDate)) { // (4)
          return 'Sub'
        } else {
          const twoDaysAgoDate = createYesterday(oneDayAgoDate)

          if (isHoliday(twoDaysAgoDate)) { // (5)
            if (isSunday(twoDaysAgoDate)) { // (6)
              return 'Sub'
            } else {
              const threeDaysAgoDate = createYesterday(twoDaysAgoDate)

              if (isHoliday(threeDaysAgoDate)) { // (7)
                if (isSunday(threeDaysAgoDate)) { // (8)
                  return 'Sub'
                } else {
                  return baseDay
                }
              } else {
                return baseDay
              }
            }
          } else {
            return baseDay
          }
        }
      } else {
        return baseDay
      }
    }
  }
}

function resolveBaseDayString (date) {
  // Mon ~ Sunのいずれかを返す
}

function createYesterday (date) {
  // dateの一日前のdateを生成して返す
}

function isHoliday (date) {
  // 祝日リストを保持して、dateが祝日ならtrue, それ以外はfalseを返す
}

function isSunday (date) {
  // dateが日曜ならtrue, それ以外はfalseを返す
}

(5)~(8)の分岐は、2026年05月3日〜5日の三連休の初日が日曜で6日が振替休日になるのを判定するためのネストです。

機能の切り出しによるリファクタリング

関数切り出し

サンプルコード内の(3)~(8)のif文がやってることは、「振替休日かどうかの判定」です。

それらの分岐はまとめて isSubstituteHoliday() 関数を定義して切り出せます。isSubstituteHoliday() の中身は、後ほどリファクタリングします。

function isSubstituteHoliday (date) {
  // dateが振替休日ならtrue、それ以外ならfalseを返す
}

isSubstituteHoliday() を使ってリファクタリングすると以下のようになります。だいぶスッキリしました。

function resolveDayString (date) {
  const baseDay = resolveBaseDayString(date) // Mon ~ Sun

  if (isHoliday(date)) { // (1)
    if (isSunday(date)) {
      return 'Sun'
    } else {
      return 'Hol'
    }
  } else {
    if (isSunday(date)) { // (2)
      return 'Sun'
    } else {
      if (isSubstituteHoliday(date)) { // (3)
        return 'Sub'
      } else {
        return baseDay
      }
    }
  }
}

内側のif文を外側に出すリファクタリング

ロジックをよくみると、日曜日の場合は常にSunになることがわかります。

内側にあるif文を外側に出すテクニックを紹介します。

  1. 内側の条件で分岐する、空のif-else文を記述する。
    if (isSunday(date)) {
      // (A)
    } else {
      // (B)
    }
    
  2. リファクタリングしたいif文のブロック全体を、上記の(A)と(B)にコピーする。
    if (isSunday(date)) { // <--- ✅️
      if (isHoliday(date)) {
        if (isSunday(date)) { // <--- 🔥
          return 'Sun'
        } else {
          return 'Hol'
        }
      } else {
        if (isSunday(date)) { // <--- 🔥
          return 'Sun'
        } else {
          if (isSubstituteHoliday(date)) {
            return 'Sub'
          } else {
            return baseDay
          }
        }
      }
    } else {
      if (isHoliday(date)) {
        if (isSunday(date)) { // <--- 🔥
          return 'Sun'
        } else {
          return 'Hol'
        }
      } else {
        if (isSunday(date)) { // <--- 🔥
          return 'Sun'
        } else {
          if (isSubstituteHoliday(date)) {
            return 'Sub'
          } else {
            return baseDay
          }
        }
      }
    }
    
  3. 🔥で指定した箇所のif文の結果は、✅️のif文によって固定になるので、thenかelseのどちらかを残して消す。
    if (isSunday(date)) { // <--- ✅️
      if (isHoliday(date)) {
        // if (isSunday(date)) { // <--- true
          return 'Sun'
        // } else {
        //   return 'Hol'
        // }
      } else {
        // if (isSunday(date)) { // <--- true
          return 'Sun'
        // } else {
        //   if (isSubstituteHoliday(date)) {
        //     return 'Sub'
        //   } else {
        //     return baseDay
        //   }
        // }
      }
    } else {
      if (isHoliday(date)) {
        // if (isSunday(date)) { // <--- false
        //   return 'Sun'
        // } else {
          return 'Hol'
        // }
      } else {
        // if (isSunday(date)) { // <--- false
        //   return 'Sun'
        // } else {
          if (isSubstituteHoliday(date)) {
            return 'Sub'
          } else {
            return baseDay
          }
        // }
      }
    }
    
  4. 整頓する
    if (isSunday(date)) {
      if (isHoliday(date)) {
        return 'Sun' // <--- 👀
      } else {
        return 'Sun' // <--- 👀
      }
    } else {
      if (isHoliday(date)) {
        return 'Hol'
      } else {
        if (isSubstituteHoliday(date)) {
          return 'Sub'
        } else {
          return baseDay
        }
      }
    }
    
  5. 2箇所の👀で返す値が同一なので、そのif文は除去できる。
    if (isSunday(date)) {
      return 'Sun'
    } else {
      if (isHoliday(date)) {
        return 'Hol'
      } else {
        if (isSubstituteHoliday(date)) {
          return 'Sub'
        } else {
          return baseDay
        }
      }
    }
    

早期リターンによるリファクタリング

早期リターンを使って余分なネストを削ります。

function resolveDayString (date) {
  const baseDay = resolveBaseDayString(date) // Mon ~ Sun

  if (isSunday(date)) {
    return 'Sun'
  }

  if (isHoliday(date)) {
    return 'Hol'
  } else {
    if (isSubstituteHoliday(date)) {
      return 'Sub'
    } else {
      return baseDay
    }
  }
}
function resolveDayString (date) {
  const baseDay = resolveBaseDayString(date) // Mon ~ Sun

  if (isSunday(date)) {
    return 'Sun'
  }

  if (isHoliday(date)) {
    return 'Hol'
  }

  if (isSubstituteHoliday(date)) {
    return 'Sub'
  } else {
    return baseDay
  }
}
function resolveDayString (date) {
  const baseDay = resolveBaseDayString(date) // Mon ~ Sun

  if (isSunday(date)) {
    return 'Sun'
  }

  if (isHoliday(date)) {
    return 'Hol'
  }

  if (isSubstituteHoliday(date)) {
    return 'Sub'
  }

  return baseDay
}

この時点で、resolveDayString() がフラット化されました。 冒頭でbaseDayを代入する意味がなくなったので整頓しています。

function resolveDayString (date) {
  if (isSunday(date)) {
    return 'Sun'
  }

  if (isHoliday(date)) {
    return 'Hol'
  }

  if (isSubstituteHoliday(date)) {
    return 'Sub'
  }

  return resolveBaseDayString(date) // Mon ~ Sun
}

isSubstituteHoliday() のリファクタリング

以下は、isSubstituteHoliday() の想定コードです。

function isSubstituteHoliday (date) {
  const oneDayAgoDate = createYesterday(date)

  if (isHoliday(oneDayAgoDate)) {
    if (isSunday(oneDayAgoDate)) {
      return true
    } else {
      const twoDaysAgoDate = createYesterday(oneDayAgoDate)

      if (isHoliday(twoDaysAgoDate)) {
        if (isSunday(twoDaysAgoDate)) {
          return true
        } else {
          const threeDaysAgoDate = createYesterday(twoDaysAgoDate)

          if (isHoliday(threeDaysAgoDate)) {
            if (isSunday(threeDaysAgoDate)) {
              return true
            } else {
              return false
            }
          } else {
            return false
          }
        }
      } else {
        return false
      }
    }
  } else {
    return false
  }
}

このコードは再帰構造を持っているので、再帰処理でリライトしてみます。

function isSubstituteHoliday (date) {
  const oneDayAgoDate = createYesterday(date)

  if (isHoliday(oneDayAgoDate)) {
    if (isSunday(oneDayAgoDate)) {
      return true
    } else {
      return isSubstituteHoliday(oneDayAgoDate)
    }
  } else {
    return false
  }
}

早期リターンを使って書き換えます。この時点でフラットになりました。

function isSubstituteHoliday (date) {
  const oneDayAgoDate = createYesterday(date)

  if (!isHoliday(oneDayAgoDate)) {
    return false
  }

  if (isSunday(oneDayAgoDate)) {
    return true
  }

  return isSubstituteHoliday(oneDayAgoDate)
}

想定していたのは2026年5月3日〜5日の三連休の初日が日曜になったケースでした。

将来の日本で法律が変わって「5月3日〜8日までの6連休」になった場合でも、再帰処理で記述した isSubstituteHoliday() は対応可能です。

ド・モルガンの法則の実例

先程のリファクタリング例ではド・モルガンの法則を使うケースがなかったので、以下の例で紹介します。

ド・モルガンの法則

ド・モルガンの法則は、言葉にするとシンプルです。

論理式全体の否定は、各オペランドを否定してANDとORを相互に入れ替えた式に等しい

以下は、「AND演算を含む式全体の否定」をOR演算に変換する操作です。

操作
AND演算を含む式全体の否定!(!alpha && beta)
ド・モルガンの法則を適用!(!alpha) || !beta
整頓alpha || !beta

「OR演算を含む式全体の否定」をAND演算で表現するときも同様です。

操作
OR演算を含む式全体の否定!(!gamma || delta)
ド・モルガンの法則を適用!(!gamma) && !delta
整頓gamma && !delta

コード例

先程のリファクタリング例とは異なり、抽象化したコード例を使います。

function sampleFunction (alpha, beta, gamma) {
  if (!alpha && beta) {
    if (gamma) {
      return 100
    } else {
      return 200
    }
  } else {
    return 300
  }
}

早期リターンによるリファクタリング

早期リターンは、then/elseブロックの内、分岐のない側を先に判定する手法です。この例ではelse側に分岐がないので、早期リターンの対象になります。

最初のif文を単純に否定条件とすると、以下の条件が複雑化します。このままだとレビュワーの認知コストが大きく、低品質なコードと評価せざるを得ません。

function sampleFunction (alpha, beta, gamma) {
  if (!(!alpha && beta)) { // <--- 👀
    return 300
  }

  if (gamma) {
    return 100
  } else {
    return 200
  }
}

こういうケースで使われるのが、ド・モルガンの法則です。

ド・モルガンの法則によるリファクタリング

✅️ の条件式がシンプルになりました。

function sampleFunction (alpha, beta, gamma) {
  if (alpha || !beta) { // <--- ✅️
    return 300
  }

  if (gamma) {
    return 100
  } else {
    return 200
  }
}

if文のネストはこの時点で解消されていますが、早期リターンを使ってelseを排除しておきます。

function sampleFunction (alpha, beta, gamma) {
  if (alpha || !beta) { // <--- ✅️
    return 300
  }

  if (gamma) {
    return 100
  }

  return 200
}

まとめ

筆者が23年にわたってプログラミングのコードを書いてきた経験から、どれだけ複雑にネストしたif文であってもフラットにできると結論しています。

もしも「これはフラットにできないだろう」と思われるネスト構造を見つけた場合は、最寄りのStewEucenまでご連絡くださいな。