Dart 3パターンマッチングで昇華させる、型安全でボイラープレートなきコマンドパターン
コードレビューをしていて、最も辟易するのは「不必要なクラス階層の乱立」だ。
特にフロントエンドのコンポーネント設計や、複雑な非同期API連携を伴うクライアントサイドのアーキテクチャにおいて、典型的な「GoFのコマンドパターン」を愚直に実装したコードによく遭遇する。
`Command` インターフェースを作り、具象クラスを数十個生やし、それぞれのクラスにコンストラクタインジェクションで依存を渡し、実行時まで型安全性が担保されないパラメータの型キャスト地獄に陥る――。
待ってほしい。我々が使っているのはDart 3だ。
Dart 3で導入された網羅的パターンマッチング(Exhaustive Pattern Matching)と代数データ型(ADT: Algebraic Data Types)の表現力を手に入れた今、そのオールドスクールなボイラープレートの山は完全に負債でしかない。
今回は、コマンドの実行と引数のバリデーション、そして型安全なディスパッチをDart 3のパターンマッチングで完全に統合し、ランタイムエラーの余地をコンパイル時にねじ伏せるクリーンなコマンドアーキテクチャを伝授しよう。
—
なぜ従来のコマンドパターンは実務で破綻するのか?
従来のオブジェクト指向的なコマンドパターンは、ポリморфиズムに依存しすぎている。
// 良くあるボイラープレートの極み(非推奨)
abstract class Command {
Future
}
class UpdateUserCommand implements Command {
final Map
UpdateUserCommand(this.payload);
@override
Future
// 実行時まで payload の型や必須キーの存在が分からない
final userId = payload[‘id’] as String?;
if (userId == null) throw ArgumentError(‘Missing id’);
// …
}
}
このアプローチの問題点は明白だ。
1. 実行時エラーの温床: `Map
2. 凝集度の低下: 「バリデーションのロジック」と「コマンドの実行ロジック」が別ファイルに散らばりがちになり、機能追加のたびに変更漏れが発生する。
3. 認知負荷の高さ: 単一の小さなアクションを起こすためだけに、何枚ものファイルを往復させられる。
Dart 3の `sealed` クラスとパターンマッチングを使えば、これらの問題は一瞬で氷解する。
—
実装:パターンマッチングで統合されたコマンドアーキテクチャ
百聞は一見にしかず。フロントエンドの状態管理や複雑なUIアクション、非同期API連携を想定したプロダクション品質のコードを見てほしい。このままコピー&ペーストして、手元のプロジェクトで挙動を脳内トレースしてほしい。
import ‘dart:async’;
// =================================================================
// 1. コマンドとペイロードを統合した代数データ型(ADT)の定義
// =================================================================
sealed class UiCommand {
const UiCommand();
}
/// ユーザープロフィール更新コマンド
class UpdateProfileCommand extends UiCommand {
final String userId;
final String newName;
final int age;
const UpdateProfileCommand({
required this.userId,
required this.newName,
required this.age,
});
}
/// 設定トグルコマンド
class ToggleFeatureFlagCommand extends UiCommand {
final String flagKey;
final bool enable;
const ToggleFeatureFlagCommand({
required this.flagKey,
required this.enable,
});
}
/// 機密性の高いデータ削除コマンド(追加の認証トークンを要求)
class DeleteAccountCommand extends UiCommand {
final String userId;
final String confirmationToken;
const DeleteAccountCommand({
required this.userId,
required this.confirmationToken,
});
}
// =================================================================
// 2. バリデーション結果の定義(Result型パターン)
// =================================================================
sealed class CommandValidationResult {
const CommandValidationResult();
}
class Valid extends CommandValidationResult {
const Valid();
}
class Invalid extends CommandValidationResult {
final String reason;
const Invalid(this.reason);
}
// =================================================================
// 3. コマンドハンドラー(バリデーションと実行の統合ディスパッチ)
// =================================================================
class CommandDispatcher {
/// コマンドのバリデーションをパターンマッチングで静的に強制
CommandValidationResult validate(UiCommand command) {
return switch (command) {
UpdateProfileCommand(:var newName, :var age) =>
newName.trim().isEmpty
? const Invalid(‘Name cannot be empty’)
: age < 0
? const Invalid('Age cannot be negative')
: const Valid(),
ToggleFeatureFlagCommand(:var flagKey) =>
flagKey.isEmpty
? const Invalid(‘Flag key is required’)
: const Valid(),
// DeleteAccountCommand はパターンマッチングの網羅性により、
// ここでハンドリングを忘れるとコンパイルエラーになる(最強の安全性)
DeleteAccountCommand(:var confirmationToken) =>
confirmationToken.length < 10
? const Invalid('Invalid security token')
: const Valid(),
};
}
/// コマンドの実行(非同期API連携のシミュレーション)
Future
// 1. 実行前のバリデーション
switch (validate(command)) {
case Invalid(:var reason):
print(‘❌ Validation Failed: $reason’);
return;
case Valid():
print(‘✅ Validation Passed.’);
}
// 2. パターンマッチングによるディスパッチと実行
// Dart VMは、この switch 式を最適化し、安全かつ高速な分岐コードを生成する。
await switch (command) {
UpdateProfileCommand(:var userId, :var newName, :var age) =>
_apiUpdateProfile(userId, newName, age),
ToggleFeatureFlagCommand(:var flagKey, :var enable) =>
_apiToggleFlag(flagKey, enable),
DeleteAccountCommand(:var userId, :var confirmationToken) =>
_apiDeleteAccount(userId, confirmationToken),
};
}
// — API 連携モック —
Future
await Future.delayed(const Duration(milliseconds: 100));
print(‘API: Updated profile for $id -> Name: $name, Age: $age’);
}
Future
await Future.delayed(const Duration(milliseconds: 100));
print(‘API: Flag $key set to $enable’);
}
Future
await Future.delayed(const Duration(milliseconds: 100));
print(‘API: Account $id deleted securely.’);
}
}
// =================================================================
// 4. エグゼキューション・エントリポイント
// =================================================================
void main() async {
final dispatcher = CommandDispatcher();
// ケースA: 正常系コマンド
print(‘— Case A: Valid Commands —‘);
await dispatcher.dispatch(
const UpdateProfileCommand(userId: ‘usr_001’, newName: ‘Alice’, age: 30),
);
// ケースB: バリデーションエラー系(名前が空)
print(‘\n— Case B: Invalid Commands (Empty Name) —‘);
await dispatcher.dispatch(
const UpdateProfileCommand(userId: ‘usr_001’, newName: ‘ ‘, age: 30),
);
// ケースC: 別種のコマンド
print(‘\n— Case C: Different Command Type —‘);
await dispatcher.dispatch(
const ToggleFeatureFlagCommand(flagKey: ‘dark_mode’, enable: true),
);
}
—
チーフアーキテクトが解説する:この設計の圧倒的な優位性
1. `sealed` クラスによる「網羅性の強制(Exhaustiveness)」
もし将来、新しいコマンド(例: `ResetPasswordCommand`)を追加したとする。
従来のOOPアプローチや、貧弱な型システムを持つ言語であれば、ハンドラー側(ディスパッチ処理やバリデーション)の修正漏れは実行時エラー(NotImplementedErrorなど)として爆発していた。
しかし、Dart 3の `sealed` 修飾子と `switch` 式の組み合わせにより、すべての具象コマンドに対するハンドリングが書かれていない場合、コンパイラがビルドを即座に拒絶する。
人的ミスによるバグの温床を、コンパイル時検査で完全にゼロにできるのだ。
2. パターン変数バインディング(Pattern Variable Binding)の美学
`UpdateProfileCommand(:var newName, :var age)` の記述を見てほしい。
これはオブジェクトからプロパティを「抽出」しながらローカル変数にバインドしている。わざわざ `command.newName` と書く必要がなく、スコープ内ですっきりと展開できる。コードの視認性が劇的に向上し、余計なボイラープレートが排除される。
3. パフォーマンスとDart VMの最適化
「こんなにモダンな書き方をすると、実行時パフォーマンスにオーバーヘッドがあるのではないか?」と懸念するシニアエンジニアもいるかもしれない。
安心してほしい。Dart VMのAOT/JITコンパイラは、`sealed` クラスに対する網羅的な `switch` 式を、内部的に非常に効率的なジャンプテーブルやインラインキャッシュに最適化する。
リフレクションを一切使わず、静的な型情報に基づいた高速なディスパッチが行われるため、パフォーマンス上の懸念は皆無である。
—
実務で運用する際の注意点
1. ファイル肥大化のコントロール:
コマンドの種類が増えすぎると、単一の `UiCommand` 定義ファイルや `CommandDispatcher` が巨大化する。ドメイン境界(ユーザー管理、設定管理、課金管理など)ごとに `sealed` クラスを適切に分割し、それぞれにディスパッチロジックを持たせるのがクリーンアーキテクチャの観点からも望ましい。
2. イミュータビリティの徹底:
コマンドオブジェクトは、必ず `const` コンストラクタを持ち、すべてのフィールドを `final`(イミュータブル)にすること。これにより、非同期処理のキューに積んだ際の状態競合や意図しない副作用を完全に遮断できる。
—
総括
Dart 3のパターンマッチングは、単なる「シンタックスシュガー」ではない。
それは、我々アーキテクトが「バグの入り込む余地のない堅牢なソフトウェア構造」をコードとして表現するための強力な武器である。
ボイラープレートを捨て、パターンマッチングを掌握せよ。お前のコードベースは、もっと美しく、もっと強靭になるはずだ。