こんにちは!FlutterやDartの開発現場で、日夜コードと向き合っている先輩エンジニアです。
皆さんは、Dart 3で導入された「パターンマッチング」や「Extension Types(拡張型)」、もう実際の開発で使ってみましたか?
「名前は聞いたことがあるけれど、なんだか難しそう……」
「今までのクラス(`class`)と何が違うの?」
そんな風に思っていませんか?ご安心ください!ここをクリアすれば、あなたの書くDartコードの安全性とパフォーマンスは、プロのアーキテクチャ水準に一気に引き上げられますよ。
今回は、この2つの強力な機能を組み合わせて、「実行時のオーバーヘッドをゼロにしつつ、型安全性を極限まで高めたドメインモデル」を構築する方法を、基礎から優しく紐解いていきましょう。
—
1. なぜ「型安全性」と「パフォーマンス」の両立が難しいのか?
まずは背景からお話ししますね。
堅牢なアプリを作ろうとすると、私たちはつい次のようなコードを書きがちです。
// 従来のクラスを使ったラップ手法
class UserId {
final String value;
UserId(this.value);
}
class OrderId {
final String value;
OrderId(this.value);
}
「ユーザーID」と「注文ID」をうっかり間違えて渡さないように、別々のクラスで包む(ラップする)のはドメイン駆動設計の基本です。
しかし、これをやるとDart VMのヒープ上に無数の小さなオブジェクトが生成され、メモリ効率やGC(ガベージコレクション)のプレッシャーが増すという代償を払うことになります。
「じゃあ、素の `String` や `int` のプリミティブ型に戻そうか……」
それだと今度は、引数の順番を間違えてバグを生む(型安全性の崩壊)というジレンマにぶつかりますよね。
この「開発時の型安全性(人間にとっての優しさ)」と「実行時のゼロコスト(マシンにとっての優しさ)」のジレンマを鮮やかに解決するのが、Dart 3の Extension Types なのです。
—
2. Extension Types とは何か?(脳内イメージ)
Extension Types(`extension type`)は、一言で言うと「見た目は独自の型だけど、コンパイルされると中身のプリミティブ型に完全に消去(擦り替え)される魔法の箱」です。
イメージ図で考えてみましょう。
[ 開発時の世界 ]
UserId 型 ( “user_123” を包んでいる )
↓ コンパイラがマジックをかける!
[ 実行時の世界(Dart VM) ]
ただの String 型 ( “user_123” )
そう、実行時にはオブジェクトのインスタンスが生成されません。 メモリ上のコストは完全にゼロです。
実際の書き方を見てみましょう。
// Extension Typeの定義
extension type const UserId(String value) {
// バリデーションなどのロジックも持たせられます
bool get isValid => value.startsWith(‘usr_’);
}
`const` コンストラクタを強制できるため、コンパイル時定数としても扱えます。最高にクールだと思いませんか?
—
3. Dart 3 パターンマッチングで型安全に「分解」する
さて、型で綺麗に包んだのはいいけれど、今度はその中身を取り出して「状態に応じた処理」を書きたい場面が出てきますよね。ここで登場するのが パターンマッチング です。
例えば、ECサイトの「注文ステータス」を考えてみましょう。
ステータスには「未払い」「出荷済み」「キャンセル」があり、それぞれ保持するデータが異なります。これを安全にハンドリングしてみます。
実用的なドメインモデルのコード例
以下のコードをよく見てください。コンパイルから実行までの流れがイメージできるように、たっぷりとコメントを載せています。
// 1. 各種IDをExtension Typeでゼロコストに定義
extension type const OrderId(String value) {}
extension type const CustomerId(String value) {}
// 2. 注文ステータスを表現するシーテッドクラス(Sealed class)
sealed class OrderStatus {}
class PendingPayment extends OrderStatus {
final int amount;
PendingPayment(this.amount);
}
class Shipped extends OrderStatus {
final String trackingNumber;
Shipped(this.trackingNumber);
}
class Canceled extends OrderStatus {
final String reason;
Canceled(this.reason);
}
// 3. ドメインロジック:パターンマッチングによる安全な分岐
void processOrder(OrderId orderId, OrderStatus status) {
// switch文が式(Expression)として使えるのもDart 3の魅力!
String message = switch (status) {
// PendingPayment型を分解しつつ、中身のamountを取り出す
PendingPayment(:final amount) =>
‘注文 [${orderId.value}] は未払いです。請求額: ¥$amount’,
// Shipped型を分解
Shipped(:final trackingNumber) =>
‘注文 [${orderId.value}] は発送済みです。追跡番号: $trackingNumber’,
// Canceled型を分解
Canceled(:final reason) =>
‘注文 [${orderId.value}] はキャンセルされました。理由: $reason’,
};
print(message);
}
// 4. 実行エントリポイント
void main() {
// 実行時には余計なオブジェクトは作られず、純粋なStringとして扱われます
var myOrderId = OrderId(‘ORD-2023-001’);
var status = Shipped(‘1234-5678-9012’);
processOrder(myOrderId, status);
// 出力結果:
// 注文 [ORD-2023-001] は発送済みです。追跡番号: 1234-5678-9012
}
—
4. ここでハッとする!陥りやすい文法エラーと注意点
Extension typesとパターンマッチングを組み合わせる際、初学者が必ずといっていいほどハマる罠がいくつかあります。事前に知っておけば怖くありませんよ!
注意点1: Extension typeは「継承(extends)」ができない
「`UserId` を継承して `SpecialUserId` を作りたい!」と思うかもしれませんが、Extension typeはあくまで「既存の型をラップして見せ方を変えるもの」です。クラスのような継承関係は作れません(インターフェースのimplementsは可能です)。
注意点2: スイッチの網羅性チェック(Exhaustiveness checking)
Dart 3の強力な機能として、`sealed class` と `switch` を組み合わせたとき、「すべてのパターンを網羅していないと、コンパイルエラーになる」という性質があります。
// もし Canceled のケースを書き忘れると…
String message = switch (status) {
PendingPayment(:final amount) => ‘未払い’,
Shipped(:final trackingNumber) => ‘発送済み’,
// エラー! Canceled が考慮されていません!というコンパイルエラーが出る
};
これにより、「うっかり新しいステータスを追加したのに、処理を書き忘れた!」というバグを、本番リリースどころかコンパイル段階で100%防ぐことができます。これぞ型安全の極みです。
—
5. まとめ:Dartを掌握するということ
今回は、Extension Typesによる「ゼロコストの型安全性」と、Dart 3のパターンマッチングによる「堅牢な状態分解」を組み合わせたアーキテクチャをご紹介しました。
- Extension Types: 実行時コストをかけずに、プリミティブ型にドメインの意味(型)を持たせる。
- パターンマッチング (Sealed class × switch): データの分解と網羅的な分岐を安全に行う。
この2つを使いこなせるようになると、あなたの書くDartコードは、パフォーマンスの妥協を一切せずに、圧倒的に堅牢で保守性の高いものへと生まれ変わります。
「ここをクリアすれば、Dartの基本はバッチリマスターできますよ!」
ぜひ、明日からのFlutter/Dart開発にこの知見を取り入れてみてくださいね。それではまた次回のテック記事でお会いしましょう!