その「ネストされた if 文」は負債だ:Dart 3 論理演算パターンでバリデーションを再定義する
コードレビューで私が最も頻繁に突き返すのは、深くネストされた `if-else` のジャングルだ。
「入力値が空でなければ、かつ長さが8文字以上で、かつ数値を含み……」といったバリデーションロジックを書く際、多くのエンジニアは無意識に手続き的な条件分岐を積み上げてしまう。しかし、そのコードは実行時に Dart VM がどのように分岐予測を行い、AOT コンパイラがどう最適化するかを無視した、極めて「硬い」設計だ。
Dart 3 の登場により、我々は「論理演算パターン(Logical Patterns)」という強力な武器を手に入れた。これを使えば、複雑なバリデーションは「手続き」から「宣言」へと昇華される。今回は、実務で即戦力となる「堅牢かつ美しいバリデーションロジック」の構築手法を伝授しよう。
—
なぜ `if` を重ねるのが「悪」なのか
まず、以下の「典型的なダメなコード」を見てほしい。
// 典型的な「手続き型」バリデーション
String? validatePassword(String? value) {
if (value != null) {
if (value.isNotEmpty) {
if (value.length >= 8) {
if (RegExp(r’\d’).hasMatch(value)) {
return null; // 正常
} else {
return ‘数値を含めてください’;
}
} else {
return ‘8文字以上必要です’;
}
}
}
return ‘必須入力です’;
}
このコードの何が問題か。
1. 認知負荷が高い: どの `else` がどの `if` に対応しているか、脳内でスタックを積む必要がある。
2. 拡張性がゼロ: 新しい条件(例:記号の追加)が増えるたびに、ネストはさらに深くなる。
3. コンパイラの最適化を活かせない: 逐次的な `if` は、実行時の分岐予測(Branch Prediction)に依存し、パイプラインのストールを招きやすい。
—
Dart 3 論理演算パターンによる「宣言的」解決
Dart 3 の `switch` 式と `&&` (Logical-and), `||` (Logical-or) パターンを組み合わせることで、上記のロジックは以下のようにフラット化できる。
プロダクション級の洗練されたコード例
/// バリデーション結果を表現する列挙型(より堅牢な設計のために)
enum ValidationError { none, empty, tooShort, noDigit }
/// Dart 3 のパターンマッチングを駆使したバリデーター
ValidationError validateSecureInput(String? value) {
// switch文の中で直接論理演算パターンを展開する
return switch (value) {
null || ” => ValidationError.empty,
String s && ({Length: < 8}) => ValidationError.tooShort,
String s && (var s when !s.contains(RegExp(r’\d’))) => ValidationError.noDigit,
_ => ValidationError.none,
};
}
// 拡張プロパティを利用してパターンをさらに読みやすくする(テクニカルリードの小技)
extension on String {
int get Length => length;
}
// 実行例
void main() {
final results = [
validateSecureInput(null), // ValidationError.empty
validateSecureInput(“abc”), // ValidationError.tooShort
validateSecureInput(“abcdefgh”), // ValidationError.noDigit
validateSecureInput(“password123”), // ValidationError.none
];
for (var res in results) {
print(‘Validation Result: ${res.name}’);
}
}
アーキテクトの視点:なぜこれが「優れている」のか
1. ガード節としての `&&` パターンの効率
上記の `String s && ({Length: < 8})` という記述を見てほしい。これは単なるシンタックスシュガーではない。Dart のパターンマッチングは、構造の分解(Destructuring)と条件の適用をアトミックに行う。
コンパイラは `value` が `String` であることを確認した瞬間に、その内部プロパティへのアクセスを型安全に保証する。実行時には、不必要なキャストや null チェックをバイパスし、最短経路で評価を行う。
2. `||` による「Or-pattern」の合流
`null || ”` という記述は、複数の無効な状態を一つのパスに集約している。これを手続き的に書くと `if (v == null || v.isEmpty)` となるが、パターンマッチングでは「これらはいずれも同じ『空の状態』である」というドメインの定義として機能する。
3. 網羅性チェック(Exhaustiveness Checking)
もし戻り値を `String?` ではなく `ValidationError` のような列挙型、あるいは `sealed class` にした場合、Dart 3 のコンパイラは `switch` が全てのケースをカバーしているかを厳格にチェックする。
「バリデーション漏れ」という、実行時まで気づけない致命的なバグをコンパイル時エラーに変えることができるのだ。
—
実務への応用:複雑な API レスポンスのバリデーション
Web フロントエンドにおいて、API から返ってくる不確定な JSON データの検証ほど神経を使うものはない。ここでも論理演算パターンが火を吹く。
sealed class ApiResponse {}
class Success extends ApiResponse { final String data; Success(this.data); }
class Failure extends ApiResponse { final int code; Failure(this.code); }
String handleResponse(ApiResponse response) {
return switch (response) {
// 成功かつデータが空でない場合
Success(data: var d) && (var s when d.isNotEmpty) => ‘Success: $d’,
// 失敗かつ、特定のエラーコード(404 or 500)の場合
Failure(code: 404 || 500) => ‘Critical Server Error’,
// それ以外の失敗
Failure(code: var c) => ‘Error occurred with code: $c’,
// 網羅性チェックにより、全てのケースをカバーしないとコンパイルエラーになる
_ => ‘Unknown State’,
};
}
パフォーマンス上の注意点
Dart VM において、パターンマッチングは非常に高度に最適化されている。特に `sealed class` と組み合わせた `switch` 式は、内部的に Jump Table へとコンパイルされる可能性があり、大量の `if-else` を評価するよりも高速に動作する場合が多い。
ただし、`when` 節(ガード句)の中で重い計算(例:巨大なリストの走査や複雑な正規表現のループ)を行うのは避けるべきだ。`when` はパターンマッチングの一部として評価されるため、マッチングの効率を阻害する可能性がある。重い処理はあらかじめ計算しておくか、パターン自体をシンプルに保つのが鉄則だ。
—
結論:コードの「格」を上げろ
`if` 文をこねくり回すのは、初心者の仕事だ。
我々プロフェッショナルは、「データがどうあるべきか」という宣言に集中すべきだ。
Dart 3 の論理演算パターンを使いこなすことは、単にコードを短くすることではない。それは、データの構造を正しく定義し、コンパイラの力を最大限に引き出し、後からプロジェクトに加わるメンバーに対して「このロジックに曖昧さはない」と宣言することに他ならない。
次にバリデーションを書くときは、その `if` を消し、`switch` と `&&` を手に取れ。それが、伝説的なアーキテクトへの第一歩だ。