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

Dart 3 パターンマッチングと Sealed Class がもたらすゼロコスト抽象化:型安全な有限ステートマシン(FSM)の極限実装

Dart 3における `sealed class` とパターンマッチング(Pattern Matching)の導入は、単なるシンタックスシュガーの追加ではない。これは、コンパイラによる静的解析の領域を劇的に広げ、ランタイムオーバーヘッドをゼロに抑えながら、ドメインの不変条件(Invariants)を型システムに完全に強制するためのパラダイムシフトである。

本稿では、リアクティブシステムや高スループットなネットワークプロトコル処理の根幹をなす「有限ステートマシン(FSM)」を題材に、Dartコンパイラ(CFE: Common Front End)および Dart VM の内部挙動を踏まえながら、極限まで洗練されたクリーンアーキテクチャ上の実装パターンを解剖する。

—

1. コンパイラが見る `sealed class` と網羅性解析(Exhaustiveness Checking)

従来の Dart における状態管理は、抽象クラスの継承と `dynamic` キャスト、あるいは `is` チェックの嵐であった。これらはコンパイル時の安全性を担保できず、ランタイム例外(`TypeError` や `StateError`)の温床となっていた。

Dart 3の `sealed` 修飾子は、そのクラスが同一ライブラリ内でのみ継承可能であることをコンパイラに明示する。これにより、CアクセスやAOT(Ahead-of-Time)コンパイル時において、コンパイラ(cfe / kernel)はサブタイプの閉じた世界(Closed World)を完全に把握できる。

網羅性チェックのメカニズム

パターンマッチングの `switch` 式において、すべてのサブタイプが処理されているかを判定する「網羅性解析(Exhaustiveness Analysis)」は、コンパイル時に行われる。もし新しい状態(例: `Disconnected`)を追加し、ハンドラー側の `switch` に記述し忘れた場合、コンパイラは容赦なくエラーを吐き出す。

この動作原理により、「実行時における想定外の状態遷移」という脆弱性を、数学的かつ構造的に根絶することが可能となる。

—

2. アーキテクチャの設計:FSMのドメインモデル

ここで構築するFSMは、クリーンアーキテクチャの原則に則り、フレームワークやI/Oに依存しない純粋なドメイン層として実装する。

状態(State)とイベント(Event)をそれぞれ `sealed class` で定義し、状態遷移関数を「純粋関数(Pure Function)」として記述する。これにより、イベントループのどのスレッド(Isolate)で実行されても副作用を持たない、スレッドセーフな設計が実現する。

実装コード:型安全なFSMのコアロジック

import ‘dart:async’;

// =====================================================================
// 1. 状態 (State) の定義
// =====================================================================
sealed class ConnectionState {
const ConnectionState();
}

final class Disconnected extends ConnectionState {
const Disconnected();
}

final class Connecting extends ConnectionState {
final int retryCount;
const Connecting(this.retryCount);
}

final class Connected extends ConnectionState {
final String sessionId;
const Connected(this.sessionId);
}

// =====================================================================
// 2. イベント (Event) の定義
// =====================================================================
sealed class ConnectionEvent {
const ConnectionEvent();
}

final class ConnectRequested extends ConnectionEvent {
const ConnectRequested();
}

final class ConnectionEstablished extends ConnectionEvent {
final String sessionId;
const ConnectionEstablished(this.sessionId);
}

final class ConnectionFailed extends ConnectionEvent {
const ConnectionFailed();
}

final class DisconnectRequested extends ConnectionEvent {
const DisconnectRequested();
}

// =====================================================================
// 3. 状態遷移マシンのコアエンジン (Pure FSM)
// =====================================================================
class ConnectionFSM {
ConnectionState _state = const Disconnected();

ConnectionState get currentState => _state;

/// Dart 3 のパターンマッチングを用いた状態遷移関数
/// コンパイлаによる完全な網羅性チェックが強制される
ConnectionState transition(ConnectionState currentState, ConnectionEvent event) {
return switch ((currentState, event)) {
// 切断状態からの接続要求 -> 接続中へ移行 (retry: 0)
(Disconnected(), ConnectRequested()) => const Connecting(0),

// 接続中からの確立イベント -> 確立状態へ移行
(Connecting(retry: var r), ConnectionEstablished(sessionId: var id)) => Connected(id),

// 接続中からの失敗 -> リトライ回数に応じて分岐
(Connecting(retry: var r), ConnectionFailed()) when r < 3 => Connecting(r + 1),
(Connecting(retry: _), ConnectionFailed()) => const Disconnected(),

// 接続済みからの切断要求 -> 切断状態へ移行
(Connected(), DisconnectRequested()) => const Disconnected(),

// どのパターンにもヒットしない不正な遷移は、現在の状態を維持または例外
// ここではドメインのルールとして「無効なイベントは状態を変化させない」
_ => currentState,
};
}

void dispatch(ConnectionEvent event) {
// 状態の不変性を保ちながら更新
_state = transition(_state, event);
}
}

—

3. メモリ最適化とアロケーション戦略

高スループットなシステムにおいて、GC(ガベージコレクション)のプレッシャーは最大のボトルネックである。Dart VMのジェネレーショナルGCにおいて、短命なオブジェクト(Young Generation)の過剰な生成はマイナーGCの頻度を上げ、レイテンシのスパイク(Jank)を引き起こす。

上記のコードで注目すべきは、すべての状態およびイベントクラスに `const` コンストラクタが付与されている点である。

カノニカライゼーション(Canonicalization)の恩恵

DartのAOT/JITコンパイラは、`const` でインスタンス化されたオブジェクトを定数プール(Constant Pool)にカノニカライズ(一意化)する。
つまり、`const Disconnected()` や `const ConnectionFailed()` といった頻繁に使用されるステート・イベントオブジェクトは、実行時にヒープメモリを再アロケーションせず、単一のインスタンス参照を使い回す。これにより、FSMが毎秒何万回ものイベントを処理しても、GCヒープアロケーションは実質的にゼロに抑えられる。

—

4. イベントループの厳密なキュー消費と非同期FSMの統合

実世界のアーキテクチャでは、FSMは同期的に動作するだけでなく、非同期イベント(ネットワークI/O、タイマーなど)の流入をイベントループ経由で順次処理する必要がある。

Dartの非同期モデルは単一スレッドのイベントループ(Event Loop)上で動作するため、適切に `StreamController` と結びつけることで、競合状態(Race Condition)のない安全なイベントパイプラインを構築できる。

クリーンアーキテクチャに基づくリアクティブFSMの構築

class ReactiveConnectionManager {
final ConnectionFSM _fsm = ConnectionFSM();

// 外部への状態公開用(Broadcast Stream)
final _stateController = StreamController.broadcast();
Stream get stateStream => _stateController.stream;

// イベント流入用 Sink
final _eventController = StreamController();

ReactiveConnectionManager() {
// イベントループのマイクロタスクキューを汚染しないよう、
// イベントストリームを逐次処理(Sequential Processing)する
_eventController.stream.listen(_handleEvent);
}

void _handleEvent(ConnectionEvent event) {
// FSMにイベントをディスパッチし、遷移後の状態を取得
_fsm.dispatch(event);

// 状態の変更をオブザーバー群に通知
_stateController.add(_fsm.currentState);
}

// 外部からのイベントエントリポイント
void sendEvent(ConnectionEvent event) {
if (!_eventController.isClosed) {
_eventController.add(event);
}
}

void dispose() {
_eventController.close();
_stateController.close();
}
}

イベントループの挙動に関する低レイヤ考察

`_eventController.stream.listen(_handleEvent)` は、内部的に非同期のイベントキューからイベントを1つずつ取り出し、同期的にコールバックを実行する。
Dartのイベントループは非プリミティブ(Non-preemptive)であるため、`_handleEvent` 内の処理がブロックされない限り、マルチスレッドのようなロック機構(Mutex / Semaphore)を意識することなく、状態の整合性が完全に保証される。これが、Dartにおけるシングルスレッド・イベント駆動アーキテクチャの最大の強みである。

—

5. 実行と検証:実用コードの動作確認

最後に、構築したReactive FSMを駆動し、状態が厳密に遷移する様子を検証するエントリポイントを示す。

main() async {
final manager = ReactiveConnectionManager();

// 状態の変化を監視するロガー
manager.stateStream.listen((state) {
switch (state) {
case Disconnected():
print(‘[FSM] Status: Disconnected’);
case Connecting(retry: var r):
print(‘[FSM] Status: Connecting… (Retry count: $r)’);
case Connected(sessionId: var id):
print(‘[FSM] Status: Connected! Session ID: $id’);
}
});

// イベントの投入(シミュレーション)
print(‘— 接続要求を送信 —‘);
manager.sendEvent(const ConnectRequested());

// 非同期の遅延を挟んで接続成功イベントを投入
await Future.delayed(const Duration(milliseconds: 100));
print(‘— 接続成功イベントを送信 —‘);
manager.sendEvent(const ConnectionEstablished(‘SESSION_XYZ_789’));

await Future.delayed(const Duration(milliseconds: 100));
print(‘— 切断要求を送信 —‘);
manager.sendEvent(const DisconnectRequested());

// クリーンアップ
await Future.delayed(const Duration(milliseconds: 100));
manager.dispose();
}

期待されるコンソール出力

[FSM] Status: Connecting… (Retry count: 0)
— 接続要求を送信 —
— 接続成功イベントを送信 —
[FSM] Status: Connected! Session ID: SESSION_XYZ_789
— 切断要求を送信 —
[FSM] Status: Disconnected

—

6. 総括

Dart 3 の `sealed class` とパターンマッチングは、単なる記述性の向上に留まらない。コンパイル時の静的型安全性を極限まで高め、`const` によるカノニカライゼーションでランタイムのメモリ・アロケーションコストを消去する、「モダン言語における最高峰のゼロコスト抽象化の具現化」である。

アーキテクチャの境界線において、ドメインの不変条件をコンパイラに強制させ、ランタイムエラーの余地を完全に排除すること。それこそが、シニアエンジニアおよびアーキテクトが目指すべき、美しく堅牢なソフトウェア設計の極みである。

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