【実務・中級編】Dartの「switch式」で「Result型」の実装を簡略化する – Dart コア文法・オブジェクト指向・Null安全解析バイブル

コードレビューを始めていく。
今日のテーマは、非同期API連携やフロントエンドの状態管理において避けて通れない「エラーハンドリングの堅牢化」だ。

お前たちが普段書いているコードを思い出してほしい。
`try-catch`を幾重にも重ね、`onError`のコールバック地獄に陥り、`if (res.hasError)` のようなフラグ判定の網羅性漏れでプロダクション環境を盛大に落とした経験はないか?

Dart 3以降、我々は強力な武器を手に入れた。「switch式(Switch Expressions)」と「パターンマッチング(Pattern Matching)」だ。これらを `Result型` と組み合わせることで、エラー処理は「場当たり的な例外処理」から「コンパイル時に保証された堅牢な制御フロー」へと昇華する。

今回は、実務の最前線で即座に使える、美しくかつ妥協のない設計パターンを授けよう。

—

なぜ従来の `try-catch` とフラグメンテーションは破綻するのか?

まず、なぜ従来のやり方がスケールしないのかをDart VMの挙動も含めて理解しておこう。

// 【アンチパターン】よくある脆弱なコード
Future fetchProfile(String id) async {
try {
final response = await apiClient.get(‘/users/$id’);
if (response.statusCode == 200) {
return UserProfile.fromJson(response.data);
} else {
throw ServerException(‘Failed with ${response.statusCode}’);
}
} catch (e) {
// ここで何が起きているか?
// 1. 投げられた例外の型が網羅されている保証はない。
// 2. 呼び出し元は「どんなエラーが起き得るか」シグネチャから一切読み取れない。
// 3. 非同期境界を越えた例外伝播は、スタックトレースのコストやパフォーマンスの劣化を招く。
}
}

例外(Exception)は、文字通り「例外的な事象」に使うべきだ。ビジネスロジック上の「失敗(サーバーエラー、バリデーションエラー、ネットワーク切断)」は、例外として投げるのではなく、「値(First-class Value)」として型システムに組み込むべきである。

ここで登場するのが `Result型` だ。

—

Dart 3で魅せる:型安全な `Result` と `switch式` の融合

まずは、代数データ型(ADT: Algebraic Data Types)の概念をDartのシールトクラス(sealed class)で表現した、プロダクション品質の `Result` 定義を見てほしい。

// result.dart
import ‘package:meta/meta.dart’;

@immutable
sealed class Result {
const Result();

// 成功値へのマッピングや共通処理を記述可能だが、
// 今回の主役は外側の「switch式による網羅的分解」である。
}

final class Success extends Result {
const Success(this.value);
final T value;
}

final class Failure extends Result {
const Failure(this.error);
final E error;
}

ポイントは `sealed class` を使っている点だ。Dartコンパイラは、`sealed` が付与されたクラスのサブタイプを完全に把握する。つまり、後述する `switch式` で全パターンを網羅していない場合、コンパイルエラー(Dead code / Non-exhaustive switch)を吐き出す。人間の注意力に頼るコードは、ここで完全に駆逐される。

—

実践:APIクライアントとUIコンポーネントでの完全網羅ハンドリング

では、実際のフロントエンドやAPI連携の現場を想定したコードを書こう。
ユーザーデータを取得し、その結果に応じてUIの描画ステートを切り替えるシーンだ。

// domain/models/api_error.dart
sealed class ApiError {
const ApiError();
}
class NetworkError extends ApiError { const NetworkError(); }
class ServerError extends ApiError { const ServerError(this.code); final int code; }
class ParseError extends ApiError { const ParseError(this.message); final String message; }

// data/repositories/user_repository.dart
Future> fetchUserProfile(String userId) async {
try {
// 疑似的なネットワークフェッチ
final response = await http.get(Uri.parse(‘/api/v1/users/$userId’));

if (response.statusCode != 200) {
return Failure(ServerError(response.statusCode));
}

final json = jsonDecode(response.body);
final profile = UserProfile.fromJson(json);
return Success(profile);

} on FormatException catch (e) {
return Failure(ParseError(e.message));
} catch (_) {
return const Failure(NetworkError());
}
}

呼び出し側:switch式による極限まで美しいハンドリング

ここからが本題だ。取得した `Result` を、UIコンポーネントやBLoC/StateNotifier内でどう処理するか。if-elseのネストや冗長なコードは一切不要。Dart 3の `switch式` がすべてを解決する。

// ui/widgets/user_profile_view.dart
String renderUserScreen(Result result) {
// switchは「文(Statement)」ではなく「式(Expression)」として機能する
return switch (result) {
// 1. 成功パターンの分解
Success(value: final profile) => ‘ようこそ、${profile.name}さん!’,

// 2. 失敗パターンの網羅的分解(ガード節や構造化パターンも使える)
Failure(error: NetworkError()) => ‘ネットワーク接続を確認してください。’,
Failure(error: ServerError(code: 404)) => ‘指定されたユーザーは見つかりませんでした。’,
Failure(error: ServerError(code: final c)) => ‘サーバーエラーが発生しました(コード: $c)。’,
Failure(error: ParseError(message: final msg)) => ‘データの解析に失敗しました: $msg’,
};
}

どうだ? このコードの美しさと強靭さが分かるか。

1. 網羅性の保証: もし `ApiError` に新しいサブタイプ(例: `UnauthorizedError`)を追加した瞬間、上記の `switch式` は即座にコンパイルエラーになる。「エラーハンドリングを書き忘れて本番でクラッシュする」というバグが、物理的に発生しなくなるのだ。
2. 圧倒的な可読性: データの構造(構造化パターン)に直接マッチングするため、無駄なキャストや `is` 演算子による型チェックがコードベースから完全に消え去る。

—

アーキテクチャ視点:パフォーマンスとアロケーションに関する警告

チーフアーキテクトとして、パフォーマンスに関する実務的な注意点も伝えておく。

`Result` 型を導入する際、すべてのメソッドチェーンで `Success` や `Failure` のインスタンスをアロケート(生成)することになる。高頻度で実行されるループ内や、60fps/120fpsが要求されるFlutterのビルドフレームワーク内で過剰にオブジェクトを生成すると、GC(ガベージコレクション)のプレッシャーが増大する。

対策として、以下の原則を守れ:

  • `const` コンストラクタの徹底:

先ほどの `Result` の実装を見てほしい。すべて `const` コンストラクタにしている。特にパラメータを持たないエラー(例: `NetworkError()`)は、可能な限り `const` で定数化し、ヒープアロケーションをゼロに抑えよ。

  • Hot Pathでの使い所を見極める:

毎フレーム実行されるアニメーションの計算ロジックなどで `Result型` を使うのは愚策だ。しかし、「非同期API連携」「画面遷移」「フォームバリデーション」といった、アプリケーションの境界領域(IOバウンドな処理)においては、パフォーマンスの損失よりも、型安全性と保守性のメリットが圧倒的に勝る。適材適所で使え。

—

まとめ:明日からお前のチームでやるべきこと

1. 例外(Exception)をビジネスロジックの制御に使うのを今すぐやめよ。
2. ドメイン層の戻り値に `Result` を採用せよ。
3. `if (result is Success)` のようなダサいコードは捨て去り、`switch式` によるパターンマッチングでコードを書き換えよ。

このパターンをチームに導入すれば、コードレビューでのエラーハンドリングに関する指摘はゼロになる。コンパイラを最高のレビュワーとして働かせるのだ。

さて、次のチケットに取りかかるとしよう。綺麗なコードを期待している。

タイトルとURLをコピーしました