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

こんにちは!FlutterやDartを使った開発を楽しんでいますか?

他のプログラミング言語、例えばTypeScriptやRust、Swiftなどで「パターンマッチング」の便利さを知ってしまうと、「あの優雅な書き方をDartでも使いたいな…」って思いますよね。

実は、Dart 3で導入されたパターンマッチング(Pattern Matching)と網羅性チェック(Exhaustiveness Checking)を組み合わせると、オブジェクト指向設計の定番である「コマンドパターン」を、驚くほどクリーンに、そしてボイラープレート(お決まりの退屈なコード)を削ぎ落として実装できるんです。

今回は、コマンドの「引数のバリデーション」と「実行ロジック」をパターンマッチングで美しく統合する実践的なアーキテクチャを、優しく紐解いていきましょう。ここをクリアすれば、あなたのDartコードはワンランク上の洗練されたものになりますよ!

—

そもそも「コマンドパターン」の何が面倒だったのか?

従来のオブジェクト指向におけるコマンドパターンを思い出してみてください。
例えば、CLIツールやエディタのアクションを管理する場合、次のような構造になりがちでした。

1. `Command` という抽象クラス(インターフェース)を作る。
2. `SaveCommand` や `DeleteCommand` といった具象クラスを何個も作る。
3. 各クラスのコンストラクタで引数を受け取り、`validate()` メソッドで型チェックや値の検証を行う。
4. 実行時に `execute()` を呼ぶ。

これだと、「たった一つの小さなコマンドを追加するだけなのに、わざわざ新しいクラスファイルを作らなきゃいけないの?」というボイラープレート地獄に陥ってしまいますよね。

Dart 3の代数的データ型(ADT的アプローチ:シールドクラスとレコード)とパターンマッチングを使えば、これを1つの関数内、あるいは1つのファイルですっきりと表現できるようになります。

—

実装:パターンマッチングでコマンドを統御する

百聞は一見に如かず。実際にコードを見てみましょう。
今回は、「ユーザーの管理コマンド」を題材に、引数のバリデーションと実行を同時に行うクリーンな実装例を作成します。

// 1. コマンドの種類と引数を表現する「シールドクラス(またはsealedな型階層)」
// Dart 3の sealed modifier により、このファイル外で勝手に拡張できなくなります。
sealed class UserCommand {}

// ユーザー作成コマンド(名前と年齢を持つ)
class CreateUser extends UserCommand {
final String name;
final int age;
CreateUser(this.name, this.age);
}

// ユーザー削除コマンド(IDを持つ)
class DeleteUser extends UserCommand {
final String userId;
DeleteUser(this.userId);
}

// パスワード変更コマンド(IDと新パスワードを持つ)
class ChangePassword extends UserCommand {
final String userId;
final String newPassword;
ChangePassword(this.userId, this.newPassword);
}

// 2. コマンドの実行とバリデーションを統合したプロセッサ関数
String handleCommand(UserCommand command) {
// switch文のパターンマッチングを活用!
return switch (command) {
// 【パターン1】CreateUserかつ、年齢が0未満、または名前が空の場合のガード
CreateUser(name: _, age: var a) when a < 0 =>
‘エラー: 年齢には0以上を指定してください (入力値: $a)’,

CreateUser(name: var n, age: var a) when n.trim().isEmpty =>
‘エラー: ユーザー名は空にできません’,

// バリデーションを通過した正常なCreateUser
CreateUser(name: var n, age: var a) =>
‘成功: ユーザー「$n」($a歳)を作成しました。’,

// 【パターン2】DeleteUserの検証と実行
DeleteUser(userId: var id) when id.isEmpty =>
‘エラー: ユーザーIDが空です’,

DeleteUser(userId: var id) =>
‘成功: ユーザーID「$id」を削除しました。’,

// 【パターン3】ChangePasswordの検証と実行
ChangePassword(userId: var id, newPassword: var pwd) when pwd.length < 8 =>
‘エラー: パスワードは8文字以上である必要があります’,

ChangePassword(userId: var id, newPassword: var pwd) =>
‘成功: ユーザーID「$id」のパスワードを変更しました。’,
};
}

void main() {
// テストケースを流し込んでみましょう
var commands = [
CreateUser(‘Alice’, 25), // 正常系
CreateUser(‘Bob’, -5), // 異常系(年齢不正)
DeleteUser(‘usr_123’), // 正常系
ChangePassword(‘usr_123′, ’12’), // 異常系(パスワード短すぎ)
];

for (var cmd in commands) {
print(handleCommand(cmd));
}
}

実行結果

成功: ユーザー「Alice」(25歳)を作成しました。
エラー: 年齢には0以上を指定してください (入力値: -5)
成功: ユーザーID「usr_123」を削除しました。
エラー: パスワードは8文字以上である必要があります

どうですか?この美しさ!
クラスごとにファイルを分ける必要はなく、`switch` 式(Switch Expression)と `when` 節(ガード条件)を組み合わせることで、「バリデーション」と「実行ロジック」が一目瞭然のひとつのフローに集約されていますよね。

—

ここがスゴい!Dart VMとコンパイラを唸らせるポイント

「ただの `if-else` の書き換えじゃないの?」と思ったそこのあなた。実は、Dart 3のこのアプローチには、言語の根幹に関わる強力なメリットがあります。

1. 網羅性チェック(Exhaustiveness Checking)の恩恵

`sealed class` を使っているため、もし将来 `UpdateEmail` という新しいコマンドを追加し忘れた場合、コンパイラが「おい、`UpdateEmail` のパターンが網羅されてないぞ!」とコンパイルエラーを出して教えてくれます。
従来の `if-else` だと、実行時(Runtime)までバグに気づきにくいですが、Dartのコンパイラがビルド時に検知してくれるため、堅牢性が圧倒的に高まります。

2. スマートキャストの連動

パターンマッチング(`CreateUser(name: var n, age: var a)`)の構文によって、マッチした瞬間にそれぞれのプロパティが安全に抽出され、ローカル変数にバインドされます。型安全性が保たれたまま、直感的に値を取り出せるのは非常にモダンですね。

—

陥りやすい文法エラーと注意点

初学者の開発者の方が、このパターンを実装する際によくハマるポイントをいくつかシェアしておきますね。

① `switch` 文(Statement)と `switch` 式(Expression)の混同

今回のコードでは、`return switch (command) { … };` という「式」の形を使っています。
式であるため、各パターンの末尾はカンマ `,` で区切り、全体をセミコロン `;` で終わらせる必要があります。
これを通常の `switch (command) { case …: … }` の文の書き方とごちゃ混ぜにしてしまうと、コンパイルエラーになるので注意してください。

② ガード条件(`when`)の書き順

パターンマッチングを行う際、「より厳しい条件(エラー系)」を上に書き、「一般的な条件(正常系)」を下に書くのが鉄則です。
Dartのパターンマッチングは上から順に評価されるため、先に正常系をマッチさせてしまうと、異常系がキャッチされなくなってしまいます。

—

まとめ

今回は、Dart 3のパターンマッチングとシールドクラスを駆使して、コマンドパターンのボイラープレートを劇的に削減するクリーンなアーキテクチャをご紹介しました。

  • `sealed class` でコマンドの種類を型安全に制限する。
  • `switch` 式 と パターンマッチング でバリデーションと処理を一体化する。
  • 網羅性チェック で将来の仕様変更時のバグをコンパイル時に防ぐ。

この手法をマスターすれば、複雑な状態遷移やユーザーアクションの処理が、驚くほど見通しが良くなりますよ。
ぜひ、あなたの次のFlutterアプリやDartコンソールアプリケーションで試してみてくださいね。ここをクリアしたあなたなら、もうDartのモダンな機能は使いこなせています!バッチリマスターです!

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