コードレビュー:その `switch` 式、将来のアップデートでアプリケーションをクラッシュさせますよ
プロダクションコードのレビューをしていて、最も背筋が凍る瞬間のひとつが、外部パッケージの進化やAPI仕様の変更を見据えていない `switch` 式に遭遇したときだ。
Dart 3で導入されたパターンマッチングと `switch` 式は、コードを劇的に美しくし、型安全性を高めてくれた。特に `sealed class`(網羅性チェック:Exhaustiveness Checking)の恩恵により、開発者が状態のハンドリングを漏らすと、コンパイラが容赦なくビルドを止めてくれる。
しかし、「外部パッケージで定義された `sealed class`」や「将来的に値が増えることが分かっているオープンなユニオン型」を扱うとき、この強力な網羅性チェックが、逆にアプリケーションを沈める爆弾に化けることを知っているだろうか?
今回は、DartコンパイラとVMの挙動、そして実務におけるコンポーネント設計の観点から、網羅性チェックの「罠」を回避し、堅牢性を担保するベストプラクティスを叩き込む。
—
なぜ外部の `sealed class` に対して網羅性チェックをしてはいけないのか?
Dartの `sealed` 修飾子は、「そのライブラリの同一ファイル内でのみサブクラス化を許可する」という強力なカプセル化をもたらす。コンパイラは、コンパイル時にすべてのサブクラスを完全に把握できるため、`switch` 式で `default` ケースを書かなくても「網羅されている」とみなす。
ここで問題になるのが、外部パッケージ(Pub.devの依存ライブラリ)が提供する `sealed class` だ。
// 外部パッケージ ( 例: a_remote_package ) が提供する API レスポンス状態
sealed class ApiResponse
class Success
class Error
あなたがフロントエンド開発やAPI連携レイヤーで、この `ApiResponse` を受け取り、UIコンポーネントを切り替えるために以下のような `switch` 式を書いたとする。
// ❌ 危険な実装:外部パッケージの sealed class を直接網羅している
Widget buildResponseView(ApiResponse
return switch (response) {
Success(data: final user) => UserProfileWidget(user: user),
Error(message: final msg) => ErrorBanner(message: msg),
// コンパイラはここで「完璧に網羅されている」と判断する
};
}
一見、美しく完璧なコードに見えるだろう。だが、数ヶ月後、その外部パッケージがバージョンアップで `class Loading
パッケージをアップデートした瞬間、あなたのアプリケーションはコンパイルエラーを起こす。運良くCI/CDパイプラインがそれを検知してくれればいいが、依存関係のマイナーアップデート等でこれがすり抜けた場合、最悪のケースでは実行時エラー(`type ‘Loading’ is not a subtype of type ‘ApiResponse’` 等のランタイムクラッシュ)を引き起こす。
外部の変更にアプリケーションのビルドが人質に取られる。これが、外部の `sealed` 型に対する網羅性チェックの最大の落とし穴だ。
—
回避策:防衛的プログラミングと `_` (ワイルドカード) の正しい運用
では、どう設計すべきか?
答えはシンプルだ。「外部の型をそのまま信頼して網羅しようとせず、未知の状態(Unknown / Loading など)へのフォールバックを常に担保する」こと。
Dart 3のパターンマッチングでは、ワイルドカード `_` や名前付きのデフォルト処理を記述できる。しかし、単に `_ => throw UnimplementedError()` や `_ => const SizedBox()` と書くだけでは、保守性が低い。
実務のフロントエンド/API連携において、保守性が高く、かつ型安全性を保つための「プロダクションパターン」を見ていこう。
—
実装例:堅牢なAPIステートハンドラーの構築
以下のコードは、外部APIから受け取る状態を安全に処理し、UIコンポーネントへ受け渡すための設計パターンだ。未知の状態が追加されてもアプリがクラッシュせず、かつ開発時に気づける仕組みを入れている。
import ‘package:flutter/material.dart’;
// — 1. 外部パッケージのモック (変更できないサードパーティ製コード) —
sealed class ExternalApiState {}
class ApiIdle extends ExternalApiState {}
class ApiLoading extends ExternalApiState {}
class ApiSuccess extends ExternalApiState { final String payload; ApiSuccess(this.payload); }
class ApiFailure extends ExternalApiState { final Object error; ApiFailure(this.error); }
// ※将来、ここに `ApiMaintenance` などが追加される可能性があるとする
// — 2. アプリケーション層での安全なハンドリング —
/// UIコンポーネントを安全にビルドするためのアダプター関数
Widget renderApiStateView(ExternalApiState state) {
return switch (state) {
// 既知の主要な状態をハンドリング
ApiIdle() => const Text(‘待機中…’),
ApiLoading() => const CircularProgressIndicator(),
ApiSuccess(payload: final data) => Text(‘データ: $data’),
ApiFailure(error: final err) => Text(‘エラー発生: $err’),
// 【極めて重要】
// 外部パッケージのアップデートにより予期せぬサブクラスが増えた場合、
// ここに落ちることでアプリ全体のクラッシュ(White Screen of Death)を防ぐ。
// 同時に、ログ出力やフォールバックUIを表示する。
_ => const SafeFallbackWidget(
message: ‘新しいシステム状態を検知しました。アプリを更新してください。’,
),
};
}
/// 予期せぬ状態のためのフォールバック用ウィジェット
class SafeFallbackWidget extends StatelessWidget {
final String message;
const SafeFallbackWidget({super.key, required this.message});
@override
Widget build(BuildContext context) {
// 本番環境では Crashlytics へのレポート送信などをここに挟むべき
return Container(
padding: const EdgeInsets.all(16.0),
color: Colors.orange.shade50,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.warning_amber_rounded, color: Colors.orange, size: 48),
const SizedBox(height: 8),
Text(message, style: const TextStyle(color: Colors.orangepeel)),
],
),
);
}
}
—
アーキテクトからの実践的アドバイス:ドメイン層へのマッピング
上記のコードは直接UI層で処理しているが、大規模なプロダクション開発(Clean Architectureやレイヤードアーキテクチャ)においては、さらに一歩進んだ設計が求められる。
1. Anti-Corruption Layer (腐敗防止層) の導入
外部パッケージの `sealed class` やデータモデルを、そのままドメイン層やUI層に直結させてはならない。データソース層(Repository)の境界で、一度自前で定義したドメインモデル(自前の `sealed class`)へとマッピング(変換)する。
2. 自前のモデルであれば網羅性チェックを積極的に使え
自らコントロールできるドメイン層の `sealed class` であれば、将来の状態追加漏れをコンパイラに検知させるために、あえて `_` デフォルトケースを書かずに完全な網羅性チェックを強制するべきだ。
// 【ドメイン層】自前で定義した sealed 型であれば、網羅性チェックを強制してバグを防ぐ
sealed class DomainLoadState {}
class Initial extends DomainLoadState {}
class Loading extends DomainLoadState {}
class Loaded extends DomainLoadState {}
// この型に対する switch ではあえて `_` を書かず、状態追加漏れをビルド時に検知させる
—
まとめ:Dart 3の力を正しく引き出すために
Dart 3の `switch` 式と網羅性チェックは、言語仕様として極めて洗練されている。しかし、「どのスコープの型に対して網羅性を担保すべきか」という境界線を見誤ると、保守性を下げる諸刃の剣になり得る。
- 外部の型(変更不能):`_` (ワイルドカード) によるフォールバックを必ず用意し、予期せぬ拡張に対して防御的に振る舞う。
- 内部の型(自ドメイン):`_` を排除した厳格な網羅性チェックを活用し、ビジネスロジックの抜け漏れをコンパイル時になくす。
この明確な指針を持ってコードベースを設計すれば、変化に強く、かつ型安全性の恩恵を最大限に受けた美しいプロダクションコードを維持できるはずだ。
次のコードレビューでは、チームメンバーの書いた `switch` 式が「どちらの領域の型を扱っているか」を鋭くチェックしてみてほしい。