こんにちは!Dartの奥深い世界へようこそ。フルスタックエンジニアの先輩として、今日から君をさらに一歩上のDart使いへ導いちゃいますね。
FlutterやDart 3の登場によって、私たちのコードの書き方は劇的に進化しました。その中でも特に胸熱なのが、Dart 3で導入された「パターンマッチング」と「sealed class(網羅的クラス)」のコンビネーションです。
今回は、オブジェクト指向デザインパターンの定番である「状態パターン(State Pattern)」を、Dart 3の力でどう美しく、そして堅牢に置き換えられるのか、その極意を一緒に紐解いていきましょう!ここをクリアすれば、君のDartのコードは一気にモダンで洗練されたものになりますよ。
—
1. 従来の「状態パターン」が抱えていたモヤモヤ
例えば、アプリの「通信状態(Network State)」を管理する場面を想像してください。状態には「初期」「ローディング中」「成功(データあり)」「エラー」がありますよね。
これを昔ながらのクラシックなオブジェクト指向(クラスベースの状態パターン)で書こうとすると、こんな感じになります。
// 従来の抽象クラスを使ったアプローチ
abstract class NetworkState {
void handle();
}
class InitialState implements NetworkState {
@override
void handle() => print(‘待機中…’);
}
class LoadingState implements NetworkState {
@override
void handle() => print(‘読み込み中…’);
}
class SuccessState implements NetworkState {
final String data;
SuccessState(this.data);
@override
void handle() => print(‘成功: $data’);
}
class ErrorState implements NetworkState {
final String message;
ErrorState(this.message);
@override
void handle() => print(‘エラー: $message’);
}
…どうでしょう? たった4つの状態を表現するために、クラスが5つも出来上がってしまいました。新しい状態を追加するたびにボイラープレート(お決まりのコード)があちこちに散らばり、処理の全体像を把握するのにファイルを何枚も行ったり来たりしなければなりませんよね。「もっとシンプルに書けないものか…」とモヤモヤしたはずです。
—
2. Dart 3の `sealed class` と `switch式` で世界が変わる
ここで登場するのが、Dart 3の `sealed class`(シール1つで封印されたクラス) と `switch式` です。
これらを使うと、状態の定義は驚くほどコンパクトになり、処理のディスパッチ(分岐)も一箇所にすっきりと集約できます。実際のコードを見てみましょう!
// ① sealed classで「状態の家族」を定義する
sealed class NetworkState {}
class InitialState extends NetworkState {}
class LoadingState extends NetworkState {}
class SuccessState extends NetworkState {
final String data;
SuccessState(this.data);
}
class ErrorState extends NetworkState {
final String message;
ErrorState(this.message);
}
// ② switch式とパターンマッチングを使った処理
String getUIStateDescription(NetworkState state) {
return switch (state) {
// それぞれの状態を受け取りつつ、内部のデータも同時に取り出す(パターンマッチング)
InitialState() => ‘ボタンを押してね’,
LoadingState() => ‘通信中です。お待ちください…’,
SuccessState(data: String d) => ‘取得データ: $d’,
ErrorState(message: var msg) => ‘エラーが発生しました: $msg’,
};
}
たったこれだけです!クラスの数は最小限に抑えられ、それぞれの状態に対する振る舞いが `switch` 式の中に美しくまとまりました。
—
3. ここが凄い!Dartコンパイラが守ってくれる「網羅性(Exhaustiveness)」の魔法
さて、ここからがDartコアコミッターとしても声を大にして伝えたい、一番熱いポイントです。
もし将来、仕様変更があって「タイムアウト(TimeoutState)」という新しい状態を追加したとしましょう。従来の書き方だと、新しいクラスを作った後、どこでその状態の処理が漏れているのかを探すのは人間の目(あるいは実行時エラー)頼みでした。
しかし、Dart 3の `sealed class` は違います。コンパイラが「おっと、新しい状態の処理がswitch式に書かれていないよ!」とコンパイルエラーで教えてくれるのです。
// 秘技:コンパイラによる網羅性チェック
// もしここで TimeoutState の処理を switch 式に書き忘れると、
// Dartのコンパイラが赤く波線を引いて教えてくれます!
これにより、「新しい状態を追加したのに、UI側でハンドリングし忘れてクラッシュした…」という現場でありがちなバグが、開発中のコンパイルエラーとして100%防げるようになります。これが型安全性とパターンマッチングの真骨頂です。
—
4. 陥りがちな文法エラーと注意点
初学者のうちは、新しい構文で少しつまずくポイントがあります。よくあるエラーを事前に予習しておきましょう。
① `switch` 文ではなく `switch`「式」を使う
Dart 3のパターンマッチングの恩恵をフルに受けるには、文(Statement)ではなく、値を返す式(Expression)として書くのがコツです。
- NG例(文): `switch (state) { … }` の中で各ケースごとに変数に代入する。
- OK例(式): `return switch (state) { Case() => value, … };` のように、直接値を返す。コードが圧倒的に短くなります。
② プロパティのパターンマッチングの書き方
`SuccessState` が持つ `data` を取り出す際、以下のように書くとエラーになります。
// ❌ エラーになりやすい書き方
SuccessState(data) => …
// ⭕️ 正しい書き方(フィールド名と受け取る変数を明示する)
SuccessState(data: var d) => …
// もしくは型を明示する場合
SuccessState(data: String d) => …
Dartのパターンマッチングでは、オブジェクトのどのプロパティをどの変数にバインド(結びつけ)するかを明確に指定します。
—
5. まとめ
今回は、クラスベースの古い状態パターンを、Dart 3の `sealed class` と `switch式` でスマートに置き換える方法を解説しました。
- 従来のように、状態ごとに無理やりクラスやメソッドを細分化する必要はありません。
- `sealed class` で状態のバリエーションをピタリと閉じ込める。
- `switch` 式のパターンマッチングで、安全かつエレガントにデータを抽出しながら分岐する。
この書き方をマスターすれば、あなたの書くFlutter/Dartコードの品質は一段と跳ね上がります。「ここをこう書くんだっけ?」と迷ったら、いつでもこの記事に戻ってきてくださいね。
さあ、明日の開発から早速このモダンなパターンを取り入れて、クリーンなコードベースを楽しんでいきましょう!