こんにちは。テクニカルリードの私だ。
コードレビューをしていると、未だに「オブジェクト指向デザインパターン」の教条主義に囚われたコードに出くわすことがある。GoF(Gang of Four)の『デザインパターン』が出版されてから何十年も経つ。特に「状態パターン(State Pattern)」を考えてみてほしい。
非同期API通信や画面のライフサイクル管理など、フロントエンド開発において「状態(State)」の管理は避けて通れない。しかし、古典的なStateパターンを実装するために、抽象状態クラスを作り、具象状態クラスを量産し、`context.setState(NextState())` のようなボイラープレートを書き散らしていないか?
「クラスの数=保守コスト」であるWebフロントエンド開発において、これは悪夢だ。状態が5つ、イベントが4つあるだけで、クラスファイルの山ができあがり、処理の全体像を追うためにIDEのタブが何枚も必要になる。
Dart 3の登場により、この状況は完全に過去のものとなった。
Language Version 3.0で導入された `sealed class`(網羅的クラス) と `switch` 式(Pattern Matching) を使えば、オブジェクト指向の多態性(Polymorphism)を維持したまま、手続き的かつ宣言的に状態をコントロールできる。しかも、コンパイル時に網羅性(Exhaustiveness)が保証されるため、未知の状態やハンドリング漏れによるランタイムエラーは物理的に不可能になる。
今回は、実務の現場ですぐに応用できる、堅牢かつ美しいプロダクションコードを通じて、その極意を伝授しよう。
—
1. なぜ「古典的Stateパターン」はスケールしないのか?
まず、何が問題なのかを整理する。
従来のStateパターンでは、状態ごとにクラスを定義する。
// 【アンチパターン】古典的なクラスベースの状態パターン
abstract class FetchState {
void handle(OrderContext context);
}
class InitialState implements FetchState {
@override
void handle(OrderContext context) { / … / }
}
class LoadingState implements FetchState {
@override
void handle(OrderContext context) { / … / }
}
class SuccessState implements FetchState {
final Data data;
SuccessState(this.data);
@override
void handle(OrderContext context) { / … / }
}
// 失敗状態、リトライ状態、タイムアウト状態…クラスが無限に増える
このアプローチの最大の欠点は、「関心の分散」だ。あるイベントが発生した時に、全状態がどう反応するかを俯瞰したい場合、それぞれの具象クラスを行き来しなければならない。これではCognitive Load(認知負荷)が高すぎる。
—
2. Dart 3 `sealed` + `switch` による状態管理のパラダイムシフト
Dart 3の `sealed class` は、同一ライブラリ内でのみサブクラス化を許可する。これにより、コンパイラ(Dart VM / AOTコンパイラ)は「この型のバリエーションはこれ以上増えない」と断定できる。
結果として、`switch` 式を用いたパターンマッチングにおいて、すべてのケースを網羅しているかをコンパイル時に検証(Exhaustiveness Checking)できるようになるのだ。
では、実際のプロダクションコードを見ていこう。非同期API連携を行う堅牢なコンポーネントの状態管理モデルだ。
プロダクションコード例:非同期API連携の完全な状態モデル
import ‘package:flutter/foundation.dart’;
// 1. sealed classによる有限の状態定義
// 同一ライブラリ(ファイル)内でのみ拡張可能。外部から勝手なサブクラスを作らせない。
sealed class ApiState
const ApiState();
}
class ApiIdle
const ApiIdle();
}
class ApiLoading
const ApiLoading();
}
class ApiSuccess
final T data;
const ApiSuccess(this.data);
}
class ApiFailure
final Object error;
final StackTrace stackTrace;
const ApiFailure(this.error, this.stackTrace);
}
// 2. ビジネスロジック / ViewModel層
class OrderViewModel extends ChangeNotifier {
ApiState
ApiState
// 非同期API連携のシミュレーション
Future
_state = const ApiLoading();
notifyListeners();
try {
// ネットワーク遅延のシミュレーション
await Future.delayed(const Duration(seconds: 2));
if (orderId == ‘error’) {
throw Exception(‘Failed to fetch order: $orderId’);
}
const resultData = ‘Order #1049: MacBook Pro M3 Max’;
_state = const ApiSuccess(resultData);
} catch (e, st) {
_state = ApiFailure(e, st);
} finally {
notifyListeners();
}
}
// 3. パターンマッチングによるUI描画ロジックの極限まで洗練された表現
// switch式(Expression)を使用している点に注目せよ。文(Statement)ではないため値路を返す。
String buildUiMessage() {
return switch (_state) {
ApiIdle() => ‘注文IDを入力して検索してください。’,
ApiLoading() => ‘データを取得中…’,
ApiSuccess(data: final d) => ‘取得成功: $d’,
ApiFailure(error: final err) => ‘エラーが発生しました: $err’,
};
}
}
> 💡 リードエンジニアの解説:`switch` 式の魔力
> 上記の `buildUiMessage()` 内の `switch` は、従来の `switch-case` 文とは異なり、式(Expression)である。各アーム(分岐)がそのまま値を返すため、不必要な一時変数を排除できる。
> さらに、もし将来 `ApiState` に `ApiTimeout` という新しい状態を追加し忘れた場合、コンパイラが即座にエラーを吐く。これにより、「新しい状態を追加したのにUI側でハンドリングし忘れてフリーズする」というWebフロントエンド特有のバグが構造的に根絶される。
—
3. パフォーマンスとコンパイル時の最適化
「こんなに高度なパターンマッチングを多用すると、実行時パフォーマンス(JOT/AOTコンパイル後の速度)に悪影響があるのではないか?」という懸念を持つ鋭いエンジニアもいるだろう。
結論から言うと、心配無用だ。
1. ジャンプテーブル(Jump Table)への最適化:
Dart VMやAOTコンパイラ(dart2native)は、`sealed class` のサブクラスが有限であることを知っているため、`switch` 式を効率的なジャンプテーブルやインラインキャッシュにコンパイルする。`instanceof` のチェイン(`if-else` の連続)よりも遥かに高速にディスパッチされる。
2. アロケーションの最小化:
`const` コンストラクタを付与した状態クラス(`ApiIdle`, `ApiLoading` など)は、Dartの定数プール上で共有されるため、ガベージコレクション(GC)のプレッシャーをゼロに近づけられる。状態変化のたびに無駄なヒープメモリを消費しない設計が極めて容易になる。
—
4. コードレビューで使えるチェックリスト
チームメンバーがこのアプローチを正しく導入できているか、次の基準でコードレビューを行うとよい。
- [ ] 状態クラスは `sealed class` になっているか? (`abstract class` や通常の `class` になっていないか)
- [ ] 状態クラスのコンストラクタは `const` になっているか? (不必要なオブジェクト生成を避けているか)
- [ ] `switch` 文ではなく `switch` 式を使っているか? (値を直接返すことでボイラープレートを削れているか)
- [ ] ガード節やオブジェクトパターンを活用しているか? (例: `ApiSuccess(data: final d) when d.isNotEmpty` のような高度な絞り込み)
—
結びにかえて
デザインパターンとは、コードを複雑にするための免罪符ではない。
「複雑性を隠蔽し、変更に強く、機械的に正しいコードを書くためのツール」でなければ意味がない。
Dart 3の `sealed class` とパターンマッチングを使いこなせば、かつての重厚長大だったStateパターンは、数行の美しく堅牢な宣言的コードへと昇華する。
今日のレビューから、無駄なクラスファイルの山を削除し、コンパイラの型安全性を最大限に活かしたアーキテクチャへとアップデートしてほしい。君たちのコードベースがより洗練されることを期待している。