【実務・中級編】Dartの「パターンマッチング」で実装するコマンドパターンのクリーンな実装 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

Dart 3 パターンマッチングで解き放つ「コマンドパターン」の真実:クラス爆発からの脱却

Dartが言語レベルでパターンマッチングとレコード型を導入したことで、我々アーキテクトが長年苦しんできた「オブジェクト指向の過剰な設計」に終止符を打つ時が来た。

多くの現場で、コマンドパターンを実装するために「コマンドごとにクラスを作成し、`execute()`メソッドを定義する」という古典的な手法が踏襲されている。しかし、それはボイラープレートの山を築き、型階層を不必要に複雑化させ、結果としてコードの可読性と保守性を著しく損なう。

今日は、DartのコンパイラとVMがどのようにパターンを最適化し、それを活用して「いかに美しく、堅牢なコマンドシステムを構築するか」を伝授しよう。

—

なぜ「クラスによるコマンドパターン」は非効率なのか

従来の設計:

abstract class Command { void execute(); }
class AddUserCommand extends Command { … }
class DeleteUserCommand extends Command { … }

このアプローチは、コマンドが増えるたびにファイル数が増大し、ロジックが分散する。さらに、各コマンドが状態を持つ必要がない場合、これはメモリ上の無駄なメタデータと、不要な仮想関数呼び出し(vtable参照)を発生させるだけだ。

我々が求めるのは、「型」ではなく「データ」にロジックを寄せる設計である。

—

Dart 3:レコードとパターンマッチングによる「データ駆動コマンド」

Dart 3以降、コマンドは「クラス」ではなく「データ(レコード)」として定義する。そして、その処理は `switch` 式の網羅性チェック(Exhaustiveness Checking)に委ねる。これが最もモダンで、かつコンパイラが最も効率的に最適化できる経路だ。

実装例:プロダクション級のコマンドハンドラ

// 1. コマンドをレコード型で定義。明示的なクラス定義は不要。
sealed class UICommand {
const UICommand._();

factory UICommand.addUser(String name) = AddUser;
factory UICommand.deleteUser(int id) = DeleteUser;
factory UICommand.updateTheme(bool isDark) = UpdateTheme;
}

// 内部用クラス(シールドクラスでスコープを制御)
class AddUser extends UICommand { final String name; AddUser(this.name) : super._(); }
class DeleteUser extends UICommand { final int id; DeleteUser(this.id) : super._(); }
class UpdateTheme extends UICommand { final bool isDark; UpdateTheme(this.isDark) : super._(); }

// 2. コマンドを処理する核心ロジック
// コンパイラが網羅性をチェックするため、case漏れはビルド時に弾かれる。
void handleCommand(UICommand command) {
switch (command) {
case AddUser(name: final n):
print(‘Adding user: $n’);
case DeleteUser(id: final id):
print(‘Deleting user: $id’);
case UpdateTheme(isDark: final dark):
print(‘Theme set to: ${dark ? ‘Dark’ : ‘Light’}’);
}
}

—

アーキテクトの視点:なぜこれがパフォーマンスに優れるのか

1. 静的解析の最大化: `switch` 式によるパターンマッチングは、コンパイラにとって「どの分岐が実行されるか」が明白である。クラスの継承ツリーを辿る動的なディスパッチに比べ、ジャンプテーブルや単純な分岐命令に最適化されやすい。
2. メモリ効率: `AddUser(String name)` のような単純なデータ保持は、クラスインスタンスのオーバーヘッドよりも軽量に扱える(将来的には `inline class` や `value class` の導入でさらに最適化される予定だ)。
3. 網羅性チェック: `sealed` クラスとパターンマッチングを組み合わせることで、「コマンドを追加したのにハンドラ側で処理を書き忘れる」というバグがコンパイル時に100%防げる。これはユニットテストよりも強力な「型による防壁」だ。

—

実務での応用:非同期API連携のクリーンな設計

フロントエンドのステート管理(BlocやRiverpodなど)において、イベントをこのパターンで定義すれば、UIから送られてくるコマンドと、Repository層への橋渡しが劇的にクリーンになる。

// 非同期処理の結果をレコードで受け取り、パターンで分解する
Future processAction(UICommand command) async {
final result = await executeApi(command);

// レコードパターンによる分解
switch (result) {
case (status: 200, data: final json):
print(‘Success: $json’);
case (status: 401, :_):
print(‘Unauthorized!’);
case (status: final s, message: final msg):
print(‘Error $s: $msg’);
}
}

結論

「サブクラスを大量に作る」という古い教義から脱却せよ。Dartのパワーは、強力な型システムと、それを活用した宣言的なパターンマッチングにある。

1. コマンドは「データ(レコード)」として定義する。
2. ロジックは `switch` 式による網羅的パターンマッチングで一箇所に集約する。
3. `sealed` クラスを使い、型の安全性をコンパイル時に担保する。

この設計を取り入れるだけで、あなたのプロダクションコードは劇的に短くなり、バグ発生率は低下し、何より「読みやすいコード」へと昇華されるはずだ。さあ、今すぐ不要なクラスファイルを削除し、現代的なDartの設計にリファクタリングを始めよう。

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