【実務・中級編】Dart 3のパターンマッチングで「状態パターン(State Pattern)」を置き換える – Dart コア文法・オブジェクト指向・Null安全解析バイブル

こんにちは。テクニカルリードの私だ。

コードレビューをしていると、未だに「オブジェクト指向デザインパターン」の教条主義に囚われたコードに出くわすことがある。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 extends ApiState {
const ApiIdle();
}

class ApiLoading extends ApiState {
const ApiLoading();
}

class ApiSuccess extends ApiState {
final T data;
const ApiSuccess(this.data);
}

class ApiFailure extends ApiState {
final Object error;
final StackTrace stackTrace;
const ApiFailure(this.error, this.stackTrace);
}

// 2. ビジネスロジック / ViewModel層
class OrderViewModel extends ChangeNotifier {
ApiState _state = const ApiState.idle(); // ※実際にはコンストラクتور定義による
ApiState get state => _state;

// 非同期API連携のシミュレーション
Future fetchOrderData(String orderId) async {
_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パターンは、数行の美しく堅牢な宣言的コードへと昇華する。
今日のレビューから、無駄なクラスファイルの山を削除し、コンパイラの型安全性を最大限に活かしたアーキテクチャへとアップデートしてほしい。君たちのコードベースがより洗練されることを期待している。

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