【実務・中級編】Dartの「switch式」で実装する、有限ステートマシン(FSM)のクリーンアーキテクチャ – Dart コア文法・オブジェクト指向・Null安全解析バイブル

Dart 3の真髄:Switch式で極める「型安全な有限ステートマシン」の構築術

Dart 3の登場により、我々が長年悩まされてきた「状態管理の複雑性」は過去のものとなりました。特にパターンマッチングとswitch式の融合は、単なる構文糖衣ではありません。コンパイラが全ての枝分かれを静的に検証できるようになったことで、「状態遷移の網羅性」をコンパイル時に保証するという、極めて堅牢なエンジニアリングが可能になったのです。

今回は、実務のフロントエンド開発で避けて通れない「非同期API連携を伴う複雑なUI状態管理」を題材に、バグを排除し、保守性を最大化する有限ステートマシン(FSM)の設計術を伝授します。

—

なぜ従来のif-elseやenum管理は「罪」なのか

多くの現場で散見される `if (state == State.loading)` や、無機質な `switch` 文による副作用の散乱。これらは以下の理由でエンジニアの生産性を削ぎ落とします。

1. 網羅性の欠如: 新しい状態を追加した際、すべての分岐を修正したかコンパイラは教えてくれない。
2. 型推論の断絶: 状態ごとの固有データ(例: エラー時のメッセージ、成功時のデータ)を無理やりクラスプロパティに押し込めてしまい、`null` チェック地獄に陥る。
3. 可読性の低下: 状態遷移のロジックがコンポーネントのUIコードと密結合し、テストが困難になる。

Dart 3では、「代数的データ型(ADT)」と「switch式」を用いることで、これらを根本から解決できます。

—

実装:宣言的ステートマシンアーキテクチャ

まずは、API通信の状態を定義するモデルから始めます。`sealed class` を使うのがポイントです。これにより、コンパイラは「この階層下にはこれ以外のサブクラスは存在しない」と確信し、switch式での網羅性チェックを強制できます。

// 状態の定義:sealed classで閉じることで、外部からの継承を禁止し網羅性を確保
sealed class ApiState {
const ApiState();
}

class Initial extends ApiState { const Initial(); }
class Loading extends ApiState { const Loading(); }
class Success extends ApiState { final T data; const Success(this.data); }
class Failure extends ApiState { final Object error; final StackTrace stackTrace; const Failure(this.error, this.stackTrace); }

コンポーネントを駆動する「純粋な遷移関数」

次に、状態を変換するロジックをクラスの外に切り出します。これが「ビジネスロジックの純粋性」を担保します。

// 状態遷移ロジック:switch式による網羅的な処理
// コンパイラが「FailureやInitialのケースが漏れている」と警告を出してくれるため、バグが入り込む余地がない
Widget buildStateView(ApiState state) => switch (state) {
Initial() => const Center(child: Text(“準備完了”)),
Loading() => const CircularProgressIndicator(),
Success(data: final data) => Text(“データ取得成功: $data”),
Failure(error: final e) => Text(“エラー発生: ${e.toString()}”),
};

—

パフォーマンスとアーキテクチャの極意

1. なぜ「式(Expression)」であるべきか

`switch` 文(Statement)は値を返しません。副作用を伴う命令を記述するのに適していますが、`switch` 式(Expression)は「値を返す」ことを前提とします。これにより、UIのレンダリング関数自体を純粋関数として記述できるようになり、Flutterの `build` メソッド内での再代入や予期せぬ副作用を排除できます。

2. コンパイル時最適化の恩恵

Dart VMは、`sealed class` を使ったパターンマッチングを高度に最適化します。`switch` 内の各ブランチは、コンパイル時に効率的なジャンプテーブル(あるいは最適化された条件分岐)に変換されるため、`if-else` の連鎖よりも実行時コストが低くなります。

3. 非同期API連携での活用

実務では、このFSMを `Notifier` や `Bloc` と組み合わせます。

class DataNotifier extends Notifier> {
@override
ApiState build() => const Initial();

Future fetchData() async {
state = const Loading();
try {
final data = await api.fetch();
state = Success(data);
} catch (e, stack) {
state = Failure(e, stack);
}
}
}

—

結論:堅牢なコードは「型」に語らせる

私がコードレビューで最も重視するのは「テストの数」ではなく「コンパイラが拒絶してくれるコードになっているか」です。

今回紹介したFSM設計は、以下の3つの強みを提供します。

  • 網羅的(Exhaustive): 新しい状態を追加した瞬間、全てのswitch式でエラーが発生するため、実装漏れが物理的に不可能になる。
  • 安全(Type-safe): `Success` 型のブランチ内でのみ `data` プロパティにアクセスできるため、Null安全の恩恵を最大化できる。
  • 疎結合: UI層は「どの状態で何を表示するか」という宣言に集中でき、ビジネスロジックと完全に分離される。

Dart 3の力を過小評価してはいけません。言語機能が提供する「型による制約」を最大限に活用し、テストコードを書く前に「コンパイラが正しいと言ってくれるコード」を設計することこそ、真のシニアエンジニアの嗜みです。

明日からの開発で、ぜひ `if-else` を `switch` 式に書き換えるところから始めてみてください。その瞬間に、あなたのコードから「未定義の状態」というバグが一つ、また一つと消えていくはずです。

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