【入門編】Dartの「extension types」とパターンマッチングを組み合わせた型安全なドメインモデル構築 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

こんにちは!FlutterやDartの開発現場で、日々コードと向き合っていますか?

今回は、Dart 3で導入された「パターンマッチング」と、さらに新しい機能である「Extension Types(拡張型)」を組み合わせて、パフォーマンスを一切犠牲にしない、極限まで型安全なドメインモデルを構築する方法について解説していきますね。

「他の言語(RustやTypeScriptなど)でパターンマッチングや高度な型システムを使っていたけれど、Dartではどう書くのが正解なの?」
「ドメイン駆動設計(DDD)をしたいけれど、プリミティブな型(Stringやint)まみれになってバグの温床になっていませんか?」

ここをクリアすれば、あなたのDartコードはワンランクもツーランクも洗練され、バグの入り込む隙間がなくなりますよ。さあ、一緒にDartの深淵を覗いてみましょう!

—

1. そもそもなぜ、プリミティブ型だけでは危険なのか?

ドメインモデル(ビジネスロジックの根幹となるデータ構造)を設計するとき、よくやりがちなのが次のようなコードです。

// 良くある危険なコード
void registerUser(String userId, String email) {
// …
}

一見、何の問題もないように見えますよね。でも、これだと次のような「ヒューマンエラー」をコンパイラが検知できません。

// 引数の順番を間違えても、型が同じ(String)なのでコンパイル通ってしまう!
registerUser(“user@example.com”, “ID_12345”);

これでは夜も眠れなくなってしまいますよね。かといって、これらを防ぐためにわざわざクラス(`class`)を乱立させるとどうなるでしょう?

クラスの乱立が引き起こす「ランタイムの代償」

通常の `class` や `mixin` を使うと、Dartのヒープ上に新しいオブジェクトがアロケート(確保)され、ガベージコレクタ(GC)の負荷になります。数百万件を扱うようなデータ処理や、ミリ秒単位のパフォーマンスが命のFlutterのUI描画において、これは小さくないコストです。

ここで登場するのが、Dart 3の Extension Types です!

—

2. Extension Types(拡張型)とは何か?

Extension Typesは、一言で言うと「ゼロコストの型ラッパー」です。

コンパイル時にのみ「別の型」として扱われ、実行時(Runtime)にはラップ元のプリミティブ型そのものとして振る舞います。つまり、メモリ上のアロケーションがゼロになります。

実際にドメインモデルでよくある「ユーザーID」と「メールアドレス」をExtension Typeで定義してみましょう。

// 1. ユーザーIDを表す拡張型
extension type const UserId(String value) {
// バリデーションなどのロジックも持たせられます
bool get isValid => value.startsWith(‘usr_’);
}

// 2. メールアドレスを表す拡張型
extension type const Email(String value) {
bool get isValid => value.contains(‘@’);
}

コンパイルと実行時の魔法

驚くべきことに、Dart VMは実行時に `UserId` や `Email` というクラスのインスタンスを生成しません。これらは単なる `String` としてメモリ上に存在します。

つまり、次のようなコードを書いたとしても……

void processUser(UserId id, Email email) {
print(‘Processing ${id.value}’);
}

コンパイル後の機械語やJavaScript/AOTバイナリでは、オーバーヘッドなしの素の `String` 操作に最適化されます。「型安全性(コンパイル時)」と「最高速のパフォーマンス(実行時)」のいいとこ取りができるというわけです。

—

3. パターンマッチングとの組み合わせで真価を発揮する

さて、型安全なドメインモデルを作ったら、次はそれを使ったビジネスロジックの分岐(状態管理など)です。ここでDart 3のパターンマッチング(`switch` 式や `1st class` なパターン)の出番です。

例えば、注文の状態(Order State)を表現するドメインモデルを考えてみましょう。

// 注文の状態を表すシールドクラス群
sealed class OrderStatus {}

class Pending extends OrderStatus {
final int waitingMinutes;
Pending(this.waitingMinutes);
}

class Shipped extends OrderStatus {
final String trackingNumber;
Shipped(this.trackingNumber);
}

class Delivered extends OrderStatus {}

ここで、先ほどの `UserId` や、金額を表す `Yen` というExtension Typeを組み合わせ、注文のステータスに応じた処理をパターンマッチングで安全に記述します。

extension type const Yen(int amount) {}

void handleOrder(UserId userId, OrderStatus status) {
// switch 式 による網羅的なパターンマッチング
String message = switch (status) {
Pending(waitingMinutes: var m) when m > 30 =>
‘ユーザー ${userId.value} 様: 混み合っております(待ち時間: $m分)’,

Pending() =>
‘ユーザー ${userId.value} 様: まもなく調理を開始します’,

Shipped(trackingNumber: var track) =>
‘お荷物(追跡番号: $track)を発送しました。お楽しみにお待ちください!’,

Delivered() =>
‘配達が完了しました。ご利用ありがとうございます。’,
};

print(message);
}

ここがスゴい!Dart 3の網羅性チェック(Exhaustiveness Checking)

もし将来、新しいステータス(例: `Cancelled`)が追加されたとき、上記の `switch` 式にそれを書き忘れたとします。
通常の言語や古いDartであれば、実行時エラー(`StateError`など)になって初めて気づくでしょう。

しかし、Dart 3のコンパイラは、`sealed class` に対するパターンマッチングで「網羅されていないケースがあるよ!」とコンパイルエラーを出して教えてくれます。
これにより、ドメインの仕様変更に強い、絶対に破綻しないコードベースが構築できるのです。

—

4. 現場でやりがち!陥りやすい文法エラーと注意点

非常に強力な Extension Types ですが、新しい機能であるがゆえに、初心者がハマりがちなポイントがあります。いくつか事前に押さえておきましょう。

注意点1: 暗黙的なキャストや継承はできない

Extension Typesは、あくまで「既存の型を包む(見せかけを変える)もの」です。通常のクラスのような継承関係(`extends`)は作れません。

// ❌ これはエラーになります
extension type const AdminId(UserId id) {} // 拡張型のベースに別の拡張型を指定することは制限があります

また、`UserId` は実行時には `String` なので、うっかり `String` を受け取る関数にそのまま渡そうとすると、明示的に `value` プロパティを取り出す必要がある場合があります(※コンテキストによりますが、基本的には厳格に区別されます)。

注意点2: `const` コンストラクタを意識する

ドメインモデルのプリミティブをラップする際は、できる限り `const` を付与しましょう。

// ⭕️ 推奨: コンパイル時定数として扱えるため、メモリ効率がさらに向上する
extension type const Price(int value) {}

Flutterのウィザードツリーや不変(Immutable)なデータ構造と組み合わせる際、`const` がついていることで不要な再描画やオブジェクト生成を防ぐことができます。

—

まとめ:Dartの基本をマスターしたあなたへ

今回は、Extension Types と パターンマッチング を組み合わせた、型安全でパフォーマンスの高いドメインモデル構築について解説しました。

  • Extension Types で、オーバーヘッドなしにドメイン固有の型(`UserId`, `Email` など)を作り、ヒューマンエラーをコンパイル時に防ぐ。
  • Dart 3のパターンマッチング(switch式) と `sealed class` を組み合わせて、ロジックの漏れをコンパイラに検知させる。

この2つを使いこなせるようになれば、あなたの書くDart/Flutterコードの信頼性は劇的に向上します。「動くけれど不安なコード」とはもうお別れです。

ここをクリアしたあなたなら、どんなに大規模なFlutterアーキテクチャの設計も怖くありません。ぜひ今日の開発から取り入れてみてくださいね!

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