【実務・中級編】Dart 3の「sealed class」と変数宣言を組み合わせた、網羅的な状態管理の実装 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

Dart 3時代の要塞:`sealed class`と網羅的変数宣言で築く「バグらない」非同期UIアーキテクチャ

コードレビューをしていて、いつまでも `default:` や `else` に `throw StateError(‘Unreachable’)` を書き散らしているエンジニアを見かけると、私はこう問いたくなる。
「おい、君はコンパイラの頭脳を信頼していないのか、それとも自分のタイポ能力を過信しているのか?」と。

Dart 3で導入された `sealed class` は、単なるシンタックスシュガーではない。
これは、「不可能な状態を型システムによってコードから物理的に排除する」ための強力な武器だ。特にWebフロントエンドやFlutterにおける非同期API連携、複雑なUI状態管理において、この機能をどう変数宣言と組み合わせるかで、プロダクトの堅牢性は天と地ほどの差を生む。

今回は、Dartのコンパイル時評価と網羅性チェック(Exhaustiveness Checking)のメカニズムを突き詰め、実務で即座に使える圧倒的に美しいプロダクションコードの設計手法を伝授しよう。

—

なぜ従来の「状態管理」は脆弱なのか?

Webエンジニアがよく犯す過ちは、文字列のunion型や、フラグの組み合わせ(`isLoading`, `hasError`, `data` をバラバラに持つなど)で状態を表現することだ。

// 【アンチパターン】これでは「通信中でかつエラー」という矛盾した状態を型が許容してしまう
class UiState {
final bool isLoading;
final String? errorMessage;
final UserData? data;

UiState({this.isLoading = false, this.errorMessage, this.data});
}

このアプローチの何がクソなのか?
1. 不正な状態の表現: `isLoading = true` かつ `errorMessage = ‘Error’` のような、物理的にあり得ない状態をコンパイラが止められない。
2. 分岐の漏れ: 画面側で `switch` を書く際、新しい状態を追加した瞬間に既存のコードのどこかでハンドリング漏れ(バグ)が起きる。

これをDart 3の `sealed class` とパターンマッチングを駆使し、「変数宣言の段階で状態の全パターンを強制的に網羅させる」設計へと昇華させる。

—

実装:API連携を伴う堅牢な非同期コンポーネント状態

以下のコードを見てほしい。これは、実務の現場でそのまま使える、非同期APIフェッチの状態管理を完全に型安全化したプロダクションコードだ。

import ‘dart:async’;

// 1. sealed classで状態の「宇宙」を定義する
// 同一ライブラリ内でのみサブクラス化可能(コンパイラがすべての派生クラスを把握できる)
sealed class ApiState {
const ApiState();
}

// 状態A: 初期・未発火
class ApiInitial extends ApiState {
const ApiInitial();
}

// 状態B: 通信中(前回のキャッシュを持つ楽観的UI更新にも対応)
class ApiLoading extends ApiState {
final T? cachedData;
const ApiLoading({this.cachedData});
}

// 状態C: 成功
class ApiSuccess extends ApiState {
final T data;
const ApiSuccess(this.data);
}

// 状態D: 失敗
class ApiFailure extends ApiState {
final Object error;
final StackTrace stackTrace;
const ApiFailure(this.error, this.stackTrace);
}

// 2. ドメインモデルの定義
class UserProfile {
final String id;
final String name;
const UserProfile({required this.id, required this.name});
}

// 3. ViewModel / Controller層での非同期ハンドリング
class UserProfileController {
// 状態を変数として保持(immutableなストリームやNotifierを想定)
ApiState _state = const ApiInitial();
ApiState get state => _state;

Future fetchUser(String userId) async {
// 既存データを保持したままロード状態へ遷移
_state = ApiLoading(cachedData: _switchGetData(_state));

try {
// ネットワークレイヤーの模擬
await Future.delayed(const Duration(seconds: 1));

if (userId == ‘error’) {
throw Exception(‘User not found: $userId’);
}

final fetchedUser = UserProfile(id: userId, name: ‘Dart Wizard’);
_state = ApiSuccess(fetchedUser);

} catch (e, st) {
_state = ApiFailure(e, st);
}
}

T? _switchGetData(ApiState currentState) {
// パターンマッチングによる安全な値の抽出
return switch (currentState) {
ApiSuccess(data: final d) => d,
ApiLoading(cachedData: final d) => d,
_ => null,
};
}
}

// 4. UI層 / レンダリング関数における「網羅的変数宣言(パターンマッチング)」
String renderUiView(ApiState state) {
// Dart 3の真骨頂:switch式による網羅性チェック (Exhaustiveness Check)
// ここで ApiFailure などのハンドリングを書き忘れると、コンパイルエラーになる。
// 「default:」や「else」に逃げる必要は二度とない。

return switch (state) {
ApiInitial() => ‘画面を読み込んでいます…’,
ApiLoading(cachedData: final cached) => cached != null
? ‘バックグラウンド更新中… (表示中: ${cached.name})’
: ‘ローディング中…’,
ApiSuccess(data: final user) => ‘ようこそ、${user.name}さん!’,
ApiFailure(error: final err) => ‘エラーが発生しました: $err’,
};
}

void main() async {
final controller = UserProfileController();

print(‘— 初期状態 —‘);
print(renderUiView(controller.state));

print(‘\n— データ取得実行 (成功ケース) —‘);
await controller.fetchUser(‘123’);
print(renderUiView(controller.state));

print(‘\n— データ取得実行 (失敗ケース) —‘);
await controller.fetchUser(‘error’);
print(renderUiView(controller.state));
}

—

このコードの何が「世界最高峰」なのか?(アーキテクチャの解説)

1. コンパイラによる網羅性の強制(Exhaustive Pattern Matching)

`renderUiView` 内の `switch (state)` に注目してほしい。
もし将来、プロダクトの仕様変更で `ApiMaintenance(String message)` という新しい状態クラスを `sealed class ApiState` の下に追加したとする。

その瞬間、DartのAOT/JITコンパイラは「おい、`ApiMaintenance` のハンドリングが抜けているぞ」とビルドを即座に落とす。開発者がうっかりUIの表示抜け(白画面バグ)を起こす余地が、言語仕様のレベルで完全に断絶されているのだ。

2. 不正な状態の物理的排除(Type Safety)

`ApiLoading` は `cachedData` を持ち、`ApiSuccess` は必ず `data` を持つ。
従来のOOPならサブクラスごとにキャスト(`as ApiSuccess`)が必要だったが、Dart 3のプロパティパターンマッチング(`ApiSuccess(data: final user)`)により、安全かつ簡潔に型がキャストされた状態で変数にバインドされる。
「データがないのに `data` プロパティにアクセスしてヌルポ」という悲劇はもう起きない。

3. パフォーマンスとメモリ効率の最適化

Dartの `sealed class` は、同一ライブラリ内でしかサブクラスを持てないことがコンパイル時に保証される。
これにより、Dart VMのCFA(Class Hierarchy Analysis)やAOTコンパイラのデバチャリング(Devirtualization)およびインライン展開が極めて効率的に働く。不要な仮想メソッドテーブル(vtable)のルックアップが最適化され、実行時のオーバーヘッドが極限まで削減される。

—

現場のシニアとしてのアドバイス

WebフロントエンドやFlutterで状態管理を書くとき、`dynamic` や何でも許容する汎用的なマップ(`Map`)に逃げないでほしい。
型は「足かせ」ではなく、「爆速でバグのないコードを書くための最強のナビゲーター」だ。

`sealed class` と `switch` 式を組み合わせた変数・状態宣言をマスターすれば、コードレビューで「この条件分岐、漏れてませんか?」という不毛な指摘をする必要すらなくなる。コンパイラに仕事はすべて押し付け、我々エンジニアはもっと本質的なビジネスロジックの設計に集中しよう。

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