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

Dart 3の真骨頂:Switch式で「有限ステートマシン」をエレガントに構築する

こんにちは。Dartの深淵を覗き込んでいる皆さん、ようこそ。

今回は、多くの開発者が「なんとなく」使いがちな`switch`文を、Dart 3で導入された強力な「パターンマッチング」と「switch式」を駆使して、堅牢な有限ステートマシン(FSM)へと昇華させるテクニックを伝授します。

if-elseのネスト地獄から抜け出し、コンパイラが「網羅性(Exhaustiveness)」を保証してくれる設計を手に入れましょう。

—

1. なぜ「Switch式」がFSMに最適なのか?

従来のFSM実装では、巨大なクラス構造や、無数のif文が必要でした。しかし、Dart 3のswitch式は違います。

  • 網羅性チェック: 状態を一つでも定義し忘れると、コンパイラが即座にエラーを吐いて教えてくれます。
  • データとロジックの分離: 状態を「データ(Enum/Sealed Class)」として定義し、ロジックを「パターンマッチング」で記述するため、非常にクリーンです。

まずは、シンプルな「注文ステート」を例に見てみましょう。

—

2. 実装:堅牢なステートマシンを構築する

まずは状態を定義します。Dart 3では`sealed class`を使うことで、「このクラスを継承できるのは同じファイル内のこれらだけ」と限定し、switchでの完全網羅を強制できます。

// 状態の定義
sealed class OrderState {}

class Pending extends OrderState {}
class Paid extends OrderState {}
class Shipped extends OrderState {}
class Delivered extends OrderState {}

// 状態遷移を管理する関数
OrderState nextState(OrderState current) => switch (current) {
Pending() => Paid(),
Paid() => Shipped(),
Shipped() => Delivered(),
// もしDeliveredの状態を書き忘れると、ここでコンパイルエラー!
Delivered() => Delivered(),
};

ここがポイント!

  • `sealed class`: 状態の「型」を定義します。これにより、switch文は「この型の派生クラスをすべて網羅しているか?」を計算時に判断できます。
  • `switch (current)`: これは文(statement)ではなく式(expression)です。戻り値として直接次の状態を返せるため、コードが劇的に短くなります。

—

3. パターンマッチングで「条件付き遷移」を実現する

実務では、単に状態が変わるだけでなく、「価格が足りているか?」といった条件分岐が絡みますよね。ここでもDartのパターンマッチングが輝きます。

sealed class OrderState {}
class Pending extends OrderState { final int amount; Pending(this.amount); }
class Paid extends OrderState {}

// 遷移ロジックに「ガード」を加える
OrderState process(OrderState current) => switch (current) {
// Pendingかつamountが1000以上の時だけPaidへ
Pending(amount: >= 1000) => Paid(),
// それ以外のPendingはそのまま
Pending() => current,
Paid() => Paid(),
};

陥りやすいエラー:網羅性の欠如

もし `Pending()` のガード条件を書き漏らしたり、新しい状態クラスを追加したのにswitch側を更新しなかった場合、Dartコンパイラは「非網羅的なswitch式です」と警告します。これがバグを未然に防ぐ「Dartの守護神」の力です。

—

4. なぜこれが「クリーンアーキテクチャ」なのか

このアプローチは、「状態というデータ」と「遷移という振る舞い」を明確に切り離しています。

1. 単一責任の原則: 遷移ロジックがひとつのswitch式に集約されるため、コードの追跡が容易になります。
2. 型安全性: `sealed class`とパターンマッチングにより、実行時に「予期せぬ状態」が発生する余地をコンパイルレベルで排除できます。
3. 可読性: 状態遷移図をそのままコードに落とし込んでいるかのような直感的な構造になります。

—

5. 初学者がつまずきやすいポイントと攻略法

  • 「なぜ `default` を使ってはいけないの?」
  • 初学者はつい `default` を書いて網羅性チェックを回避しがちです。しかし、FSMにおいて`default`を使うことは「異常系を隠蔽する」ことに他なりません。網羅性チェックをエラーにさせることこそが、Dart流のデバッグ手法です。
  • 「switch文とswitch式、どっちを使うべき?」
  • 戻り値を返すなら `switch (…)` 式を。副作用(ログ出力やDB更新など)のみを行う場合は `switch (…) { case …: … }` 文を使いましょう。

—

最後に:Dartを掌握するということ

Dartの美しいところは、「コンパイラが開発者の思考を補助してくれる」点にあります。

今回紹介したFSMの書き方は、複雑なビジネスロジックを抱えるFlutterアプリにおいても、非常に高い保守性を発揮します。ぜひ、次回の開発で「`if-else`を消し去る」ことから始めてみてください。

ここをクリアすれば、あなたはもうDartの基本をマスターしたも同然です。さあ、次はどんな複雑な状態遷移を、このエレガントな手法で解き明かしますか?

—
Dartのコードは常に「読みやすさ」と「堅牢さ」の狭間にあります。そのバランスを制御できるようになった時、あなたはDartエンジニアとして一段上のステージに立っているはずです。

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